From ccca31529c07970e89419fb85a9e8153a5396838 Mon Sep 17 00:00:00 2001 From: Eric Erfanian Date: Wed, 22 Feb 2017 16:32:36 -0800 Subject: Update dialer sources. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Test: Built package and system image. This change clobbers the old source, and is an export from an internal Google repository. The internal repository was forked form Android in March, and this change includes modifications since then, to near the v8 release. Since the fork, we've moved code from monolithic to independent modules. In addition, we've switched to Blaze/Bazel as the build sysetm. This export, however, still uses make. New dependencies have been added: - Dagger - Auto-Value - Glide - Libshortcutbadger Going forward, development will still be in Google3, and the Gerrit release will become an automated export, with the next drop happening in ~ two weeks. Android.mk includes local modifications from ToT. Abridged changelog: Bug fixes ● Not able to mute, add a call when using Phone app in multiwindow mode ● Double tap on keypad triggering multiple key and tones ● Reported spam numbers not showing as spam in the call log ● Crash when user tries to block number while Phone app is not set as default ● Crash when user picks a number from search auto-complete list Visual Voicemail (VVM) improvements ● Share Voicemail audio via standard exporting mechanisms that support file attachment (email, MMS, etc.) ● Make phone number, email and web sites in VVM transcript clickable ● Set PIN before declining VVM Terms of Service {Carrier} ● Set client type for outbound visual voicemail SMS {Carrier} New incoming call and incall UI on older devices (Android M) ● Updated Phone app icon ● New incall UI (large buttons, button labels) ● New and animated Answer/Reject gestures Accessibility ● Add custom answer/decline call buttons on answer screen for touch exploration accessibility services ● Increase size of touch target ● Add verbal feedback when a Voicemail fails to load ● Fix pressing of Phone buttons while in a phone call using Switch Access ● Fix selecting and opening contacts in talkback mode ● Split focus for ‘Learn More’ link in caller id & spam to help distinguish similar text Other ● Backup & Restore for App Preferences ● Prompt user to enable Wi-Fi calling if the call ends due to out of service and Wi-Fi is connected ● Rename “Dialpad” to “Keypad” ● Show "Private number" for restricted calls ● Delete unused items (vcard, add contact, call history) from Phone menu Change-Id: I2a7e53532a24c21bf308bf0a6d178d7ddbca4958 --- Android.mk | 276 ++- AndroidManifest.xml | 437 +---- CONTRIBUTING | 27 + InCallUI/AndroidManifest.xml | 24 - InCallUI/build.gradle | 14 - InCallUI/proguard.flags | 14 - InCallUI/res/anim/activity_open_enter.xml | 35 - InCallUI/res/anim/activity_open_exit.xml | 26 - InCallUI/res/anim/call_status_pulse.xml | 22 - InCallUI/res/anim/decelerate_cubic.xml | 21 - InCallUI/res/anim/decelerate_quint.xml | 21 - InCallUI/res/color/ota_title_color.xml | 21 - InCallUI/res/color/selectable_icon_tint.xml | 24 - InCallUI/res/drawable-hdpi/fab_blue.png | Bin 2805 -> 0 bytes InCallUI/res/drawable-hdpi/fab_ic_call.png | Bin 875 -> 0 bytes InCallUI/res/drawable-hdpi/fab_ic_end_call.png | Bin 852 -> 0 bytes InCallUI/res/drawable-hdpi/fab_ic_message.png | Bin 617 -> 0 bytes InCallUI/res/drawable-hdpi/fab_red.png | Bin 2783 -> 0 bytes .../res/drawable-hdpi/ic_block_grey600_24dp.png | Bin 518 -> 0 bytes .../res/drawable-hdpi/ic_business_white_24dp.png | Bin 152 -> 0 bytes .../res/drawable-hdpi/ic_call_end_white_24dp.png | Bin 454 -> 0 bytes .../res/drawable-hdpi/ic_call_split_white_24dp.png | Bin 326 -> 0 bytes InCallUI/res/drawable-hdpi/ic_call_white_24dp.png | Bin 451 -> 0 bytes .../res/drawable-hdpi/ic_close_grey600_24dp.png | Bin 225 -> 0 bytes .../res/drawable-hdpi/ic_forward_white_24dp.png | Bin 139 -> 0 bytes InCallUI/res/drawable-hdpi/ic_hd_24dp.png | Bin 236 -> 0 bytes .../drawable-hdpi/ic_location_on_white_24dp.png | Bin 371 -> 0 bytes .../res/drawable-hdpi/ic_lockscreen_glowdot.png | Bin 738 -> 0 bytes .../drawable-hdpi/ic_person_add_grey600_24dp.png | Bin 300 -> 0 bytes .../drawable-hdpi/ic_phone_paused_white_24dp.png | Bin 458 -> 0 bytes InCallUI/res/drawable-hdpi/ic_question_mark.png | Bin 941 -> 0 bytes .../res/drawable-hdpi/ic_report_white_36dp.png | Bin 312 -> 0 bytes .../res/drawable-hdpi/ic_schedule_white_24dp.png | Bin 575 -> 0 bytes InCallUI/res/drawable-hdpi/ic_toolbar_add_call.png | Bin 1230 -> 0 bytes .../drawable-hdpi/ic_toolbar_arrow_whitespace.png | Bin 489 -> 0 bytes .../drawable-hdpi/ic_toolbar_audio_bluetooth.png | Bin 833 -> 0 bytes .../drawable-hdpi/ic_toolbar_audio_headphones.png | Bin 1142 -> 0 bytes .../res/drawable-hdpi/ic_toolbar_audio_phone.png | Bin 1301 -> 0 bytes InCallUI/res/drawable-hdpi/ic_toolbar_dialpad.png | Bin 624 -> 0 bytes InCallUI/res/drawable-hdpi/ic_toolbar_hold.png | Bin 511 -> 0 bytes InCallUI/res/drawable-hdpi/ic_toolbar_merge.png | Bin 772 -> 0 bytes InCallUI/res/drawable-hdpi/ic_toolbar_mic_off.png | Bin 1155 -> 0 bytes .../res/drawable-hdpi/ic_toolbar_speaker_on.png | Bin 1118 -> 0 bytes InCallUI/res/drawable-hdpi/ic_toolbar_swap.png | Bin 1110 -> 0 bytes InCallUI/res/drawable-hdpi/ic_toolbar_video.png | Bin 711 -> 0 bytes .../res/drawable-hdpi/ic_toolbar_video_off.png | Bin 932 -> 0 bytes .../res/drawable-hdpi/ic_toolbar_video_switch.png | Bin 972 -> 0 bytes InCallUI/res/drawable-hdpi/img_business.png | Bin 3311 -> 0 bytes InCallUI/res/drawable-hdpi/img_conference.png | Bin 7037 -> 0 bytes InCallUI/res/drawable-hdpi/img_no_image.png | Bin 5362 -> 0 bytes InCallUI/res/drawable-hdpi/img_phone.png | Bin 6157 -> 0 bytes .../drawable-land/rounded_call_card_background.xml | 23 - InCallUI/res/drawable-mdpi/fab_blue.png | Bin 1841 -> 0 bytes InCallUI/res/drawable-mdpi/fab_ic_call.png | Bin 698 -> 0 bytes InCallUI/res/drawable-mdpi/fab_ic_end_call.png | Bin 668 -> 0 bytes InCallUI/res/drawable-mdpi/fab_ic_message.png | Bin 561 -> 0 bytes InCallUI/res/drawable-mdpi/fab_red.png | Bin 1843 -> 0 bytes .../res/drawable-mdpi/ic_block_grey600_24dp.png | Bin 348 -> 0 bytes .../res/drawable-mdpi/ic_business_white_24dp.png | Bin 105 -> 0 bytes .../res/drawable-mdpi/ic_call_end_white_24dp.png | Bin 315 -> 0 bytes .../res/drawable-mdpi/ic_call_split_white_24dp.png | Bin 256 -> 0 bytes InCallUI/res/drawable-mdpi/ic_call_white_24dp.png | Bin 348 -> 0 bytes .../res/drawable-mdpi/ic_close_grey600_24dp.png | Bin 178 -> 0 bytes .../res/drawable-mdpi/ic_forward_white_24dp.png | Bin 117 -> 0 bytes InCallUI/res/drawable-mdpi/ic_hd_24dp.png | Bin 154 -> 0 bytes .../drawable-mdpi/ic_location_on_white_24dp.png | Bin 265 -> 0 bytes .../res/drawable-mdpi/ic_lockscreen_glowdot.png | Bin 538 -> 0 bytes .../drawable-mdpi/ic_person_add_grey600_24dp.png | Bin 211 -> 0 bytes .../drawable-mdpi/ic_phone_paused_white_24dp.png | Bin 346 -> 0 bytes InCallUI/res/drawable-mdpi/ic_question_mark.png | Bin 619 -> 0 bytes .../res/drawable-mdpi/ic_report_white_36dp.png | Bin 240 -> 0 bytes .../res/drawable-mdpi/ic_schedule_white_24dp.png | Bin 377 -> 0 bytes InCallUI/res/drawable-mdpi/ic_toolbar_add_call.png | Bin 883 -> 0 bytes .../drawable-mdpi/ic_toolbar_arrow_whitespace.png | Bin 431 -> 0 bytes .../drawable-mdpi/ic_toolbar_audio_bluetooth.png | Bin 630 -> 0 bytes .../drawable-mdpi/ic_toolbar_audio_headphones.png | Bin 885 -> 0 bytes .../res/drawable-mdpi/ic_toolbar_audio_phone.png | Bin 921 -> 0 bytes InCallUI/res/drawable-mdpi/ic_toolbar_dialpad.png | Bin 527 -> 0 bytes InCallUI/res/drawable-mdpi/ic_toolbar_hold.png | Bin 455 -> 0 bytes InCallUI/res/drawable-mdpi/ic_toolbar_merge.png | Bin 669 -> 0 bytes InCallUI/res/drawable-mdpi/ic_toolbar_mic_off.png | Bin 822 -> 0 bytes .../res/drawable-mdpi/ic_toolbar_speaker_on.png | Bin 847 -> 0 bytes InCallUI/res/drawable-mdpi/ic_toolbar_swap.png | Bin 808 -> 0 bytes InCallUI/res/drawable-mdpi/ic_toolbar_video.png | Bin 607 -> 0 bytes .../res/drawable-mdpi/ic_toolbar_video_off.png | Bin 797 -> 0 bytes .../res/drawable-mdpi/ic_toolbar_video_switch.png | Bin 776 -> 0 bytes InCallUI/res/drawable-mdpi/img_business.png | Bin 2240 -> 0 bytes InCallUI/res/drawable-mdpi/img_conference.png | Bin 4629 -> 0 bytes InCallUI/res/drawable-mdpi/img_no_image.png | Bin 3509 -> 0 bytes InCallUI/res/drawable-mdpi/img_phone.png | Bin 3798 -> 0 bytes InCallUI/res/drawable-xhdpi/fab_blue.png | Bin 4085 -> 0 bytes InCallUI/res/drawable-xhdpi/fab_ic_call.png | Bin 1266 -> 0 bytes InCallUI/res/drawable-xhdpi/fab_ic_end_call.png | Bin 1215 -> 0 bytes InCallUI/res/drawable-xhdpi/fab_ic_message.png | Bin 795 -> 0 bytes InCallUI/res/drawable-xhdpi/fab_red.png | Bin 4047 -> 0 bytes .../res/drawable-xhdpi/ic_block_grey600_24dp.png | Bin 690 -> 0 bytes .../res/drawable-xhdpi/ic_business_white_24dp.png | Bin 112 -> 0 bytes .../res/drawable-xhdpi/ic_call_end_white_24dp.png | Bin 534 -> 0 bytes .../drawable-xhdpi/ic_call_split_white_24dp.png | Bin 377 -> 0 bytes InCallUI/res/drawable-xhdpi/ic_call_white_24dp.png | Bin 535 -> 0 bytes .../res/drawable-xhdpi/ic_close_grey600_24dp.png | Bin 261 -> 0 bytes .../res/drawable-xhdpi/ic_forward_white_24dp.png | Bin 159 -> 0 bytes InCallUI/res/drawable-xhdpi/ic_hd_24dp.png | Bin 201 -> 0 bytes .../drawable-xhdpi/ic_location_on_white_24dp.png | Bin 456 -> 0 bytes .../res/drawable-xhdpi/ic_lockscreen_glowdot.png | Bin 964 -> 0 bytes .../drawable-xhdpi/ic_person_add_grey600_24dp.png | Bin 341 -> 0 bytes .../drawable-xhdpi/ic_phone_paused_white_24dp.png | Bin 584 -> 0 bytes InCallUI/res/drawable-xhdpi/ic_question_mark.png | Bin 1170 -> 0 bytes .../res/drawable-xhdpi/ic_report_white_36dp.png | Bin 340 -> 0 bytes .../res/drawable-xhdpi/ic_schedule_white_24dp.png | Bin 737 -> 0 bytes .../res/drawable-xhdpi/ic_toolbar_add_call.png | Bin 1549 -> 0 bytes .../drawable-xhdpi/ic_toolbar_arrow_whitespace.png | Bin 543 -> 0 bytes .../drawable-xhdpi/ic_toolbar_audio_bluetooth.png | Bin 882 -> 0 bytes .../drawable-xhdpi/ic_toolbar_audio_headphones.png | Bin 1479 -> 0 bytes .../res/drawable-xhdpi/ic_toolbar_audio_phone.png | Bin 1837 -> 0 bytes InCallUI/res/drawable-xhdpi/ic_toolbar_dialpad.png | Bin 709 -> 0 bytes InCallUI/res/drawable-xhdpi/ic_toolbar_hold.png | Bin 565 -> 0 bytes InCallUI/res/drawable-xhdpi/ic_toolbar_merge.png | Bin 921 -> 0 bytes InCallUI/res/drawable-xhdpi/ic_toolbar_mic_off.png | Bin 1454 -> 0 bytes .../res/drawable-xhdpi/ic_toolbar_speaker_on.png | Bin 1505 -> 0 bytes InCallUI/res/drawable-xhdpi/ic_toolbar_swap.png | Bin 1487 -> 0 bytes InCallUI/res/drawable-xhdpi/ic_toolbar_video.png | Bin 830 -> 0 bytes .../res/drawable-xhdpi/ic_toolbar_video_off.png | Bin 1160 -> 0 bytes .../res/drawable-xhdpi/ic_toolbar_video_switch.png | Bin 1120 -> 0 bytes InCallUI/res/drawable-xhdpi/img_business.png | Bin 4759 -> 0 bytes InCallUI/res/drawable-xhdpi/img_conference.png | Bin 9517 -> 0 bytes InCallUI/res/drawable-xhdpi/img_no_image.png | Bin 7369 -> 0 bytes InCallUI/res/drawable-xhdpi/img_phone.png | Bin 8189 -> 0 bytes InCallUI/res/drawable-xxhdpi/fab_blue.png | Bin 7009 -> 0 bytes InCallUI/res/drawable-xxhdpi/fab_ic_call.png | Bin 2320 -> 0 bytes InCallUI/res/drawable-xxhdpi/fab_ic_end_call.png | Bin 2227 -> 0 bytes InCallUI/res/drawable-xxhdpi/fab_ic_message.png | Bin 1556 -> 0 bytes InCallUI/res/drawable-xxhdpi/fab_red.png | Bin 6965 -> 0 bytes .../res/drawable-xxhdpi/ic_block_grey600_24dp.png | Bin 1029 -> 0 bytes .../res/drawable-xxhdpi/ic_business_white_24dp.png | Bin 119 -> 0 bytes .../res/drawable-xxhdpi/ic_call_end_white_24dp.png | Bin 736 -> 0 bytes .../drawable-xxhdpi/ic_call_split_white_24dp.png | Bin 461 -> 0 bytes .../res/drawable-xxhdpi/ic_call_white_24dp.png | Bin 750 -> 0 bytes .../res/drawable-xxhdpi/ic_close_grey600_24dp.png | Bin 353 -> 0 bytes .../res/drawable-xxhdpi/ic_forward_white_24dp.png | Bin 204 -> 0 bytes InCallUI/res/drawable-xxhdpi/ic_hd_24dp.png | Bin 290 -> 0 bytes .../drawable-xxhdpi/ic_location_on_white_24dp.png | Bin 675 -> 0 bytes .../res/drawable-xxhdpi/ic_lockscreen_glowdot.png | Bin 1907 -> 0 bytes .../drawable-xxhdpi/ic_person_add_grey600_24dp.png | Bin 485 -> 0 bytes .../drawable-xxhdpi/ic_phone_paused_white_24dp.png | Bin 842 -> 0 bytes InCallUI/res/drawable-xxhdpi/ic_question_mark.png | Bin 1774 -> 0 bytes .../res/drawable-xxhdpi/ic_report_white_36dp.png | Bin 522 -> 0 bytes .../res/drawable-xxhdpi/ic_schedule_white_24dp.png | Bin 1107 -> 0 bytes .../res/drawable-xxhdpi/ic_toolbar_add_call.png | Bin 1874 -> 0 bytes .../ic_toolbar_arrow_whitespace.png | Bin 1188 -> 0 bytes .../drawable-xxhdpi/ic_toolbar_audio_bluetooth.png | Bin 1528 -> 0 bytes .../ic_toolbar_audio_headphones.png | Bin 1858 -> 0 bytes .../res/drawable-xxhdpi/ic_toolbar_audio_phone.png | Bin 2285 -> 0 bytes .../res/drawable-xxhdpi/ic_toolbar_dialpad.png | Bin 1449 -> 0 bytes InCallUI/res/drawable-xxhdpi/ic_toolbar_hold.png | Bin 1143 -> 0 bytes InCallUI/res/drawable-xxhdpi/ic_toolbar_merge.png | Bin 1385 -> 0 bytes .../res/drawable-xxhdpi/ic_toolbar_mic_off.png | Bin 1956 -> 0 bytes .../res/drawable-xxhdpi/ic_toolbar_speaker_on.png | Bin 2065 -> 0 bytes InCallUI/res/drawable-xxhdpi/ic_toolbar_swap.png | Bin 1970 -> 0 bytes InCallUI/res/drawable-xxhdpi/ic_toolbar_video.png | Bin 1347 -> 0 bytes .../res/drawable-xxhdpi/ic_toolbar_video_off.png | Bin 1538 -> 0 bytes .../drawable-xxhdpi/ic_toolbar_video_switch.png | Bin 1534 -> 0 bytes InCallUI/res/drawable-xxhdpi/img_business.png | Bin 6499 -> 0 bytes InCallUI/res/drawable-xxhdpi/img_conference.png | Bin 16306 -> 0 bytes InCallUI/res/drawable-xxhdpi/img_no_image.png | Bin 9850 -> 0 bytes InCallUI/res/drawable-xxhdpi/img_phone.png | Bin 10848 -> 0 bytes InCallUI/res/drawable-xxxhdpi/fab_blue.png | Bin 9807 -> 0 bytes InCallUI/res/drawable-xxxhdpi/fab_ic_call.png | Bin 2921 -> 0 bytes InCallUI/res/drawable-xxxhdpi/fab_ic_end_call.png | Bin 2567 -> 0 bytes InCallUI/res/drawable-xxxhdpi/fab_ic_message.png | Bin 1850 -> 0 bytes InCallUI/res/drawable-xxxhdpi/fab_red.png | Bin 9802 -> 0 bytes .../res/drawable-xxxhdpi/ic_block_grey600_24dp.png | Bin 1353 -> 0 bytes .../drawable-xxxhdpi/ic_business_white_24dp.png | Bin 114 -> 0 bytes .../drawable-xxxhdpi/ic_call_end_white_24dp.png | Bin 929 -> 0 bytes .../drawable-xxxhdpi/ic_call_split_white_24dp.png | Bin 646 -> 0 bytes .../res/drawable-xxxhdpi/ic_close_grey600_24dp.png | Bin 444 -> 0 bytes .../res/drawable-xxxhdpi/ic_forward_white_24dp.png | Bin 236 -> 0 bytes InCallUI/res/drawable-xxxhdpi/ic_hd_24dp.png | Bin 348 -> 0 bytes .../drawable-xxxhdpi/ic_location_on_white_24dp.png | Bin 869 -> 0 bytes .../ic_person_add_grey600_24dp.png | Bin 638 -> 0 bytes InCallUI/res/drawable-xxxhdpi/ic_question_mark.png | Bin 2370 -> 0 bytes .../res/drawable-xxxhdpi/ic_report_white_36dp.png | Bin 649 -> 0 bytes .../drawable-xxxhdpi/ic_schedule_white_24dp.png | Bin 1478 -> 0 bytes .../res/drawable-xxxhdpi/ic_toolbar_add_call.png | Bin 2271 -> 0 bytes .../ic_toolbar_arrow_whitespace.png | Bin 1262 -> 0 bytes .../ic_toolbar_audio_bluetooth.png | Bin 1728 -> 0 bytes .../ic_toolbar_audio_headphones.png | Bin 2158 -> 0 bytes .../drawable-xxxhdpi/ic_toolbar_audio_phone.png | Bin 2830 -> 0 bytes .../res/drawable-xxxhdpi/ic_toolbar_dialpad.png | Bin 1651 -> 0 bytes InCallUI/res/drawable-xxxhdpi/ic_toolbar_hold.png | Bin 1179 -> 0 bytes InCallUI/res/drawable-xxxhdpi/ic_toolbar_merge.png | Bin 1444 -> 0 bytes .../res/drawable-xxxhdpi/ic_toolbar_mic_off.png | Bin 2284 -> 0 bytes .../res/drawable-xxxhdpi/ic_toolbar_speaker_on.png | Bin 2532 -> 0 bytes InCallUI/res/drawable-xxxhdpi/ic_toolbar_swap.png | Bin 2370 -> 0 bytes InCallUI/res/drawable-xxxhdpi/ic_toolbar_video.png | Bin 1394 -> 0 bytes .../res/drawable-xxxhdpi/ic_toolbar_video_off.png | Bin 1703 -> 0 bytes .../drawable-xxxhdpi/ic_toolbar_video_switch.png | Bin 1610 -> 0 bytes InCallUI/res/drawable-xxxhdpi/img_business.png | Bin 10730 -> 0 bytes InCallUI/res/drawable-xxxhdpi/img_conference.png | Bin 19584 -> 0 bytes InCallUI/res/drawable-xxxhdpi/img_no_image.png | Bin 16251 -> 0 bytes InCallUI/res/drawable-xxxhdpi/img_phone.png | Bin 18635 -> 0 bytes InCallUI/res/drawable/btn_add.xml | 30 - InCallUI/res/drawable/btn_background.xml | 33 - InCallUI/res/drawable/btn_change_to_video.xml | 31 - InCallUI/res/drawable/btn_change_to_voice.xml | 31 - InCallUI/res/drawable/btn_compound_audio.xml | 93 - InCallUI/res/drawable/btn_compound_background.xml | 35 - InCallUI/res/drawable/btn_compound_dialpad.xml | 32 - InCallUI/res/drawable/btn_compound_hold.xml | 32 - InCallUI/res/drawable/btn_compound_mute.xml | 31 - InCallUI/res/drawable/btn_compound_video_off.xml | 33 - .../res/drawable/btn_compound_video_switch.xml | 33 - InCallUI/res/drawable/btn_merge.xml | 30 - InCallUI/res/drawable/btn_overflow.xml | 30 - InCallUI/res/drawable/btn_selected.xml | 25 - InCallUI/res/drawable/btn_selected_focused.xml | 29 - InCallUI/res/drawable/btn_swap.xml | 30 - InCallUI/res/drawable/btn_unselected.xml | 25 - InCallUI/res/drawable/btn_unselected_focused.xml | 28 - InCallUI/res/drawable/conference_ripple.xml | 25 - InCallUI/res/drawable/end_call_background.xml | 25 - InCallUI/res/drawable/ic_incall_audio_handle.xml | 40 - InCallUI/res/drawable/ic_incall_video_handle.xml | 41 - InCallUI/res/drawable/ic_lockscreen_answer.xml | 27 - .../ic_lockscreen_answer_activated_layer.xml | 25 - .../drawable/ic_lockscreen_answer_normal_layer.xml | 33 - .../res/drawable/ic_lockscreen_answer_video.xml | 28 - .../ic_lockscreen_answer_video_activated_layer.xml | 26 - .../ic_lockscreen_answer_video_normal_layer.xml | 34 - InCallUI/res/drawable/ic_lockscreen_decline.xml | 27 - .../ic_lockscreen_decline_activated_layer.xml | 24 - .../ic_lockscreen_decline_normal_layer.xml | 32 - .../res/drawable/ic_lockscreen_decline_video.xml | 28 - ...ic_lockscreen_decline_video_activated_layer.xml | 26 - .../ic_lockscreen_decline_video_normal_layer.xml | 34 - InCallUI/res/drawable/ic_lockscreen_outerring.xml | 22 - InCallUI/res/drawable/ic_lockscreen_text.xml | 27 - .../ic_lockscreen_text_activated_layer.xml | 32 - .../drawable/ic_lockscreen_text_normal_layer.xml | 33 - .../res/drawable/img_conference_automirrored.xml | 21 - .../res/drawable/img_no_image_automirrored.xml | 21 - InCallUI/res/drawable/incoming_sms_background.xml | 25 - InCallUI/res/drawable/outgoing_sms_background.xml | 25 - InCallUI/res/drawable/spam_notification_icon.xml | 31 - InCallUI/res/drawable/subject_bubble.xml | 22 - .../res/drawable/unknown_notification_icon.xml | 31 - InCallUI/res/layout-h400dp/call_card_fragment.xml | 172 -- .../manage_conference_call_button.xml | 61 - .../res/layout-w500dp-land/call_card_fragment.xml | 158 -- .../manage_conference_call_button.xml | 61 - InCallUI/res/layout/accessible_answer_fragment.xml | 104 -- InCallUI/res/layout/answer_fragment.xml | 42 - .../business_contact_context_list_header.xml | 40 - .../res/layout/business_context_info_list_item.xml | 48 - InCallUI/res/layout/call_button_fragment.xml | 171 -- InCallUI/res/layout/call_card_fragment.xml | 158 -- InCallUI/res/layout/caller_in_conference.xml | 116 -- .../res/layout/conference_manager_fragment.xml | 36 - InCallUI/res/layout/incall_dialpad_fragment.xml | 24 - InCallUI/res/layout/incall_screen.xml | 23 - .../res/layout/manage_conference_call_button.xml | 72 - InCallUI/res/layout/outgoing_call_animation.xml | 22 - .../res/layout/person_context_info_list_item.xml | 40 - InCallUI/res/layout/primary_call_info.xml | 231 --- InCallUI/res/layout/secondary_call_info.xml | 105 -- InCallUI/res/layout/video_call_fragment.xml | 28 - InCallUI/res/layout/video_call_views.xml | 66 - InCallUI/res/menu/incall_audio_mode_menu.xml | 39 - InCallUI/res/values-af/strings.xml | 195 -- InCallUI/res/values-am/strings.xml | 195 -- InCallUI/res/values-ar/strings.xml | 195 -- InCallUI/res/values-az/strings.xml | 195 -- InCallUI/res/values-b+sr+Latn/strings.xml | 195 -- InCallUI/res/values-be/strings.xml | 195 -- InCallUI/res/values-bg/strings.xml | 195 -- InCallUI/res/values-bn/strings.xml | 195 -- InCallUI/res/values-bs/strings.xml | 195 -- InCallUI/res/values-ca/strings.xml | 195 -- InCallUI/res/values-cs/strings.xml | 195 -- InCallUI/res/values-da/strings.xml | 195 -- InCallUI/res/values-de/strings.xml | 195 -- InCallUI/res/values-el/strings.xml | 195 -- InCallUI/res/values-en-rAU/strings.xml | 195 -- InCallUI/res/values-en-rGB/strings.xml | 195 -- InCallUI/res/values-en-rIN/strings.xml | 195 -- InCallUI/res/values-es-rUS/strings.xml | 195 -- InCallUI/res/values-es/strings.xml | 195 -- InCallUI/res/values-et/strings.xml | 195 -- InCallUI/res/values-eu/strings.xml | 195 -- InCallUI/res/values-fa/strings.xml | 195 -- InCallUI/res/values-fi/strings.xml | 195 -- InCallUI/res/values-fr-rCA/strings.xml | 195 -- InCallUI/res/values-fr/strings.xml | 195 -- InCallUI/res/values-gl/strings.xml | 195 -- InCallUI/res/values-gu/strings.xml | 195 -- InCallUI/res/values-h400dp/dimens.xml | 31 - InCallUI/res/values-hi/strings.xml | 195 -- InCallUI/res/values-hr/strings.xml | 195 -- InCallUI/res/values-hu/strings.xml | 195 -- InCallUI/res/values-hy/strings.xml | 195 -- InCallUI/res/values-in/strings.xml | 195 -- InCallUI/res/values-is/strings.xml | 195 -- InCallUI/res/values-it/strings.xml | 195 -- InCallUI/res/values-iw/strings.xml | 195 -- InCallUI/res/values-ja/strings.xml | 195 -- InCallUI/res/values-ka/strings.xml | 195 -- InCallUI/res/values-kk/strings.xml | 195 -- InCallUI/res/values-km/strings.xml | 195 -- InCallUI/res/values-kn/strings.xml | 195 -- InCallUI/res/values-ko/strings.xml | 195 -- InCallUI/res/values-ky/strings.xml | 195 -- InCallUI/res/values-lo/strings.xml | 195 -- InCallUI/res/values-lt/strings.xml | 195 -- InCallUI/res/values-lv/strings.xml | 195 -- InCallUI/res/values-mk/strings.xml | 195 -- InCallUI/res/values-ml/strings.xml | 195 -- InCallUI/res/values-mn/strings.xml | 195 -- InCallUI/res/values-mr/strings.xml | 195 -- InCallUI/res/values-ms/strings.xml | 195 -- InCallUI/res/values-my/strings.xml | 195 -- InCallUI/res/values-nb/strings.xml | 195 -- InCallUI/res/values-ne/strings.xml | 195 -- InCallUI/res/values-nl/strings.xml | 195 -- InCallUI/res/values-pa/strings.xml | 195 -- InCallUI/res/values-pl/strings.xml | 195 -- InCallUI/res/values-pt-rBR/strings.xml | 195 -- InCallUI/res/values-pt-rPT/strings.xml | 195 -- InCallUI/res/values-pt/strings.xml | 195 -- InCallUI/res/values-ro/strings.xml | 195 -- InCallUI/res/values-ru/strings.xml | 195 -- InCallUI/res/values-si/strings.xml | 195 -- InCallUI/res/values-sk/strings.xml | 195 -- InCallUI/res/values-sl/strings.xml | 195 -- InCallUI/res/values-sq/strings.xml | 195 -- InCallUI/res/values-sr/strings.xml | 195 -- InCallUI/res/values-sv/strings.xml | 195 -- InCallUI/res/values-sw/strings.xml | 195 -- InCallUI/res/values-sw360dp/dimens.xml | 35 - InCallUI/res/values-sw410dp/config.xml | 21 - InCallUI/res/values-ta/strings.xml | 195 -- InCallUI/res/values-te/strings.xml | 195 -- InCallUI/res/values-th/strings.xml | 195 -- InCallUI/res/values-tl/strings.xml | 195 -- InCallUI/res/values-tr/strings.xml | 195 -- InCallUI/res/values-uk/strings.xml | 195 -- InCallUI/res/values-ur/strings.xml | 195 -- InCallUI/res/values-uz/strings.xml | 195 -- InCallUI/res/values-vi/strings.xml | 195 -- InCallUI/res/values-w500dp-land/colors.xml | 21 - InCallUI/res/values-w500dp-land/dimens.xml | 35 - InCallUI/res/values-zh-rCN/strings.xml | 195 -- InCallUI/res/values-zh-rHK/strings.xml | 195 -- InCallUI/res/values-zh-rTW/strings.xml | 195 -- InCallUI/res/values-zu/strings.xml | 195 -- InCallUI/res/values/animation_constants.xml | 22 - InCallUI/res/values/array.xml | 135 -- InCallUI/res/values/attrs.xml | 71 - InCallUI/res/values/colors.xml | 133 -- InCallUI/res/values/config.xml | 27 - InCallUI/res/values/dimens.xml | 150 -- InCallUI/res/values/ids.xml | 20 - InCallUI/res/values/strings.xml | 540 ------ InCallUI/res/values/styles.xml | 100 - .../android/incallui/AccelerometerListener.java | 169 -- .../android/incallui/AccessibleAnswerFragment.java | 157 -- .../src/com/android/incallui/AnswerFragment.java | 307 ---- .../src/com/android/incallui/AnswerPresenter.java | 312 ---- .../com/android/incallui/AudioModeProvider.java | 105 -- .../src/com/android/incallui/BaseFragment.java | 84 - InCallUI/src/com/android/incallui/Call.java | 1023 ----------- .../com/android/incallui/CallButtonFragment.java | 819 --------- .../com/android/incallui/CallButtonPresenter.java | 486 ----- .../src/com/android/incallui/CallCardFragment.java | 1510 --------------- .../com/android/incallui/CallCardPresenter.java | 1181 ------------ InCallUI/src/com/android/incallui/CallList.java | 695 ------- InCallUI/src/com/android/incallui/CallTimer.java | 90 - InCallUI/src/com/android/incallui/CallerInfo.java | 585 ------ .../com/android/incallui/CallerInfoAsyncQuery.java | 599 ------ .../src/com/android/incallui/CallerInfoUtils.java | 234 --- .../android/incallui/CircularRevealFragment.java | 170 -- .../incallui/ConferenceManagerFragment.java | 139 -- .../incallui/ConferenceManagerPresenter.java | 144 -- .../incallui/ConferenceParticipantListAdapter.java | 533 ------ .../src/com/android/incallui/ContactInfoCache.java | 699 ------- .../src/com/android/incallui/ContactUtils.java | 48 - .../com/android/incallui/ContactsAsyncHelper.java | 258 --- .../incallui/ContactsPreferencesFactory.java | 61 - .../src/com/android/incallui/DialpadFragment.java | 563 ------ .../src/com/android/incallui/DialpadPresenter.java | 84 - .../src/com/android/incallui/DistanceHelper.java | 37 - .../src/com/android/incallui/ExternalCallList.java | 105 -- .../com/android/incallui/ExternalCallNotifier.java | 406 ---- .../android/incallui/FragmentDisplayManager.java | 23 - .../android/incallui/GlowPadAnswerFragment.java | 155 -- .../src/com/android/incallui/GlowPadWrapper.java | 158 -- .../src/com/android/incallui/InCallActivity.java | 980 ---------- .../com/android/incallui/InCallAnimationUtils.java | 184 -- .../com/android/incallui/InCallCameraManager.java | 184 -- .../incallui/InCallContactInteractions.java | 399 ---- .../src/com/android/incallui/InCallDateUtils.java | 53 - .../incallui/InCallOrientationEventListener.java | 178 -- .../src/com/android/incallui/InCallPresenter.java | 1938 -------------------- .../com/android/incallui/InCallServiceImpl.java | 100 - .../android/incallui/InCallServiceListener.java | 41 - .../incallui/InCallUIMaterialColorMapUtils.java | 55 - .../android/incallui/InCallVideoCallCallback.java | 156 -- .../incallui/InCallVideoCallCallbackNotifier.java | 284 --- .../src/com/android/incallui/LatencyReport.java | 145 -- InCallUI/src/com/android/incallui/Log.java | 176 -- .../com/android/incallui/NeededForReflection.java | 30 - .../incallui/NotificationBroadcastReceiver.java | 82 - .../android/incallui/PostCharDialogFragment.java | 95 - InCallUI/src/com/android/incallui/Presenter.java | 59 - .../src/com/android/incallui/ProximitySensor.java | 317 ---- .../com/android/incallui/StatusBarNotifier.java | 793 -------- .../src/com/android/incallui/TelecomAdapter.java | 226 --- InCallUI/src/com/android/incallui/Ui.java | 24 - .../com/android/incallui/VideoCallFragment.java | 901 --------- .../com/android/incallui/VideoCallPresenter.java | 1306 ------------- .../com/android/incallui/VideoPauseController.java | 420 ----- InCallUI/src/com/android/incallui/VideoUtils.java | 109 -- .../android/incallui/async/PausableExecutor.java | 61 - .../incallui/async/PausableExecutorImpl.java | 42 - .../incallui/ringtone/DialerRingtoneManager.java | 140 -- .../incallui/ringtone/InCallTonePlayer.java | 168 -- .../incallui/ringtone/ToneGeneratorFactory.java | 36 - .../incallui/service/PhoneNumberService.java | 67 - .../incallui/spam/SpamCallListListener.java | 117 -- .../android/incallui/util/AccessibilityUtil.java | 30 - .../com/android/incallui/util/TelecomCallUtil.java | 53 - .../incallui/widget/multiwaveview/Ease.java | 132 -- .../incallui/widget/multiwaveview/GlowPadView.java | 1473 --------------- .../incallui/widget/multiwaveview/PointCloud.java | 235 --- .../widget/multiwaveview/TargetDrawable.java | 250 --- .../incallui/widget/multiwaveview/Tweener.java | 178 -- .../com/android/incalluibind/ObjectFactory.java | 59 - .../android/incallui/CallCardPresenterTest.java | 121 -- .../tests/src/com/android/incallui/CallTest.java | 125 -- .../com/android/incallui/CallerInfoUtilsTest.java | 31 - .../incallui/ContactsPreferencesFactoryTest.java | 51 - .../com/android/incallui/ExternalCallListTest.java | 144 -- .../android/incallui/ExternalCallNotifierTest.java | 214 --- .../incallui/InCallContactInteractionsTest.java | 325 ---- .../com/android/incallui/InCallPresenterTest.java | 198 -- .../com/android/incallui/LatencyReportTest.java | 59 - .../com/android/incallui/MockCallListWrapper.java | 80 - .../com/android/incallui/ProximitySensorTest.java | 66 - .../android/incallui/StatusBarNotifierTest.java | 98 - .../src/com/android/incallui/TestTelecomCall.java | 161 -- .../incallui/async/SingleProdThreadExecutor.java | 69 - .../ringtone/DialerRingtoneManagerTest.java | 219 --- .../incallui/ringtone/InCallTonePlayerTest.java | 148 -- .../incallui/spam/SpamCallListListenerTest.java | 206 --- LICENSE | 190 ++ .../product_logo_avatar_anonymous_color_120.png | Bin 0 -> 2526 bytes ...oduct_logo_avatar_anonymous_white_color_120.png | Bin 0 -> 1132 bytes .../product_logo_avatar_anonymous_color_120.png | Bin 0 -> 1615 bytes ...oduct_logo_avatar_anonymous_white_color_120.png | Bin 0 -> 752 bytes .../product_logo_avatar_anonymous_color_120.png | Bin 0 -> 3400 bytes ...oduct_logo_avatar_anonymous_white_color_120.png | Bin 0 -> 1508 bytes .../product_logo_avatar_anonymous_color_120.png | Bin 0 -> 5093 bytes ...oduct_logo_avatar_anonymous_white_color_120.png | Bin 0 -> 2339 bytes .../product_logo_avatar_anonymous_color_120.png | Bin 0 -> 6753 bytes ...oduct_logo_avatar_anonymous_white_color_120.png | Bin 0 -> 3234 bytes .../quantum_ic_arrow_back_white_24.png | Bin 0 -> 148 bytes .../quantum_ic_arrow_drop_down_white_18.png | Bin 0 -> 121 bytes .../quantum_ic_bluetooth_audio_grey600_24.png | Bin 0 -> 448 bytes .../quantum_ic_bluetooth_audio_white_36.png | Bin 0 -> 579 bytes .../drawable-hdpi/quantum_ic_call_end_white_24.png | Bin 0 -> 314 bytes .../drawable-hdpi/quantum_ic_call_end_white_36.png | Bin 0 -> 424 bytes .../quantum_ic_call_merge_white_36.png | Bin 0 -> 258 bytes .../res/drawable-hdpi/quantum_ic_call_white_18.png | Bin 0 -> 276 bytes .../res/drawable-hdpi/quantum_ic_call_white_24.png | Bin 0 -> 340 bytes .../quantum_ic_camera_alt_white_24.png | Bin 0 -> 364 bytes .../quantum_ic_camera_alt_white_48.png | Bin 0 -> 666 bytes .../quantum_ic_camera_front_white_36.png | Bin 0 -> 417 bytes .../quantum_ic_camera_rear_white_36.png | Bin 0 -> 344 bytes .../drawable-hdpi/quantum_ic_check_black_24.png | Bin 0 -> 169 bytes .../drawable-hdpi/quantum_ic_check_black_36.png | Bin 0 -> 223 bytes .../drawable-hdpi/quantum_ic_check_white_48.png | Bin 0 -> 276 bytes .../drawable-hdpi/quantum_ic_close_white_24.png | Bin 0 -> 221 bytes .../drawable-hdpi/quantum_ic_close_white_36.png | Bin 0 -> 302 bytes .../drawable-hdpi/quantum_ic_dialpad_white_36.png | Bin 0 -> 549 bytes .../drawable-hdpi/quantum_ic_forward_white_24.png | Bin 0 -> 139 bytes .../quantum_ic_fullscreen_exit_white_48.png | Bin 0 -> 105 bytes .../quantum_ic_fullscreen_white_36.png | Bin 0 -> 132 bytes .../quantum_ic_fullscreen_white_48.png | Bin 0 -> 107 bytes .../drawable-hdpi/quantum_ic_group_white_36.png | Bin 0 -> 393 bytes .../res/drawable-hdpi/quantum_ic_hd_white_24.png | Bin 0 -> 236 bytes .../quantum_ic_headset_grey600_24.png | Bin 0 -> 371 bytes .../drawable-hdpi/quantum_ic_headset_white_36.png | Bin 0 -> 511 bytes .../drawable-hdpi/quantum_ic_message_white_24.png | Bin 0 -> 167 bytes .../drawable-hdpi/quantum_ic_mic_off_black_24.png | Bin 0 -> 402 bytes .../drawable-hdpi/quantum_ic_mic_off_white_36.png | Bin 0 -> 578 bytes .../quantum_ic_network_wifi_white_24.png | Bin 0 -> 427 bytes .../drawable-hdpi/quantum_ic_pause_white_36.png | Bin 0 -> 124 bytes .../quantum_ic_person_add_white_24.png | Bin 0 -> 289 bytes .../quantum_ic_photo_library_white_24.png | Bin 0 -> 249 bytes .../drawable-hdpi/quantum_ic_photo_white_24.png | Bin 0 -> 261 bytes .../drawable-hdpi/quantum_ic_photo_white_48.png | Bin 0 -> 450 bytes .../drawable-hdpi/quantum_ic_report_white_18.png | Bin 0 -> 212 bytes .../res/drawable-hdpi/quantum_ic_send_black_24.png | Bin 0 -> 290 bytes .../res/drawable-hdpi/quantum_ic_send_white_24.png | Bin 0 -> 293 bytes .../quantum_ic_swap_calls_white_36.png | Bin 0 -> 447 bytes .../quantum_ic_switch_camera_white_36.png | Bin 0 -> 318 bytes .../quantum_ic_switch_video_white_36.png | Bin 0 -> 288 bytes .../res/drawable-hdpi/quantum_ic_undo_white_48.png | Bin 0 -> 445 bytes .../quantum_ic_videocam_off_white_24.png | Bin 0 -> 271 bytes .../quantum_ic_videocam_off_white_36.png | Bin 0 -> 360 bytes .../drawable-hdpi/quantum_ic_videocam_white_18.png | Bin 0 -> 155 bytes .../drawable-hdpi/quantum_ic_videocam_white_24.png | Bin 0 -> 173 bytes .../drawable-hdpi/quantum_ic_videocam_white_36.png | Bin 0 -> 222 bytes .../quantum_ic_voicemail_white_18.png | Bin 0 -> 372 bytes .../quantum_ic_volume_up_grey600_24.png | Bin 0 -> 375 bytes .../quantum_ic_volume_up_white_36.png | Bin 0 -> 518 bytes .../quantum_ic_arrow_back_white_24.png | Bin 0 -> 149 bytes .../quantum_ic_send_black_24.png | Bin 0 -> 295 bytes .../quantum_ic_send_white_24.png | Bin 0 -> 295 bytes .../quantum_ic_undo_white_48.png | Bin 0 -> 447 bytes .../quantum_ic_arrow_back_white_24.png | Bin 0 -> 119 bytes .../quantum_ic_send_black_24.png | Bin 0 -> 229 bytes .../quantum_ic_send_white_24.png | Bin 0 -> 237 bytes .../quantum_ic_undo_white_48.png | Bin 0 -> 325 bytes .../quantum_ic_arrow_back_white_24.png | Bin 0 -> 140 bytes .../quantum_ic_send_black_24.png | Bin 0 -> 341 bytes .../quantum_ic_send_white_24.png | Bin 0 -> 352 bytes .../quantum_ic_undo_white_48.png | Bin 0 -> 571 bytes .../quantum_ic_arrow_back_white_24.png | Bin 0 -> 195 bytes .../quantum_ic_send_black_24.png | Bin 0 -> 458 bytes .../quantum_ic_send_white_24.png | Bin 0 -> 452 bytes .../quantum_ic_undo_white_48.png | Bin 0 -> 823 bytes .../quantum_ic_arrow_back_white_24.png | Bin 0 -> 200 bytes .../quantum_ic_send_black_24.png | Bin 0 -> 586 bytes .../quantum_ic_send_white_24.png | Bin 0 -> 580 bytes .../quantum_ic_undo_white_48.png | Bin 0 -> 1087 bytes .../quantum_ic_arrow_back_white_24.png | Bin 0 -> 115 bytes .../quantum_ic_arrow_drop_down_white_18.png | Bin 0 -> 89 bytes .../quantum_ic_bluetooth_audio_grey600_24.png | Bin 0 -> 307 bytes .../quantum_ic_bluetooth_audio_white_36.png | Bin 0 -> 438 bytes .../drawable-mdpi/quantum_ic_call_end_white_24.png | Bin 0 -> 235 bytes .../drawable-mdpi/quantum_ic_call_end_white_36.png | Bin 0 -> 314 bytes .../quantum_ic_call_merge_white_36.png | Bin 0 -> 208 bytes .../res/drawable-mdpi/quantum_ic_call_white_18.png | Bin 0 -> 202 bytes .../res/drawable-mdpi/quantum_ic_call_white_24.png | Bin 0 -> 246 bytes .../quantum_ic_camera_alt_white_24.png | Bin 0 -> 240 bytes .../quantum_ic_camera_alt_white_48.png | Bin 0 -> 446 bytes .../quantum_ic_camera_front_white_36.png | Bin 0 -> 274 bytes .../quantum_ic_camera_rear_white_36.png | Bin 0 -> 224 bytes .../drawable-mdpi/quantum_ic_check_black_24.png | Bin 0 -> 128 bytes .../drawable-mdpi/quantum_ic_check_black_36.png | Bin 0 -> 169 bytes .../drawable-mdpi/quantum_ic_check_white_48.png | Bin 0 -> 199 bytes .../drawable-mdpi/quantum_ic_close_white_24.png | Bin 0 -> 175 bytes .../drawable-mdpi/quantum_ic_close_white_36.png | Bin 0 -> 221 bytes .../drawable-mdpi/quantum_ic_dialpad_white_36.png | Bin 0 -> 264 bytes .../drawable-mdpi/quantum_ic_forward_white_24.png | Bin 0 -> 117 bytes .../quantum_ic_fullscreen_exit_white_48.png | Bin 0 -> 101 bytes .../quantum_ic_fullscreen_white_36.png | Bin 0 -> 101 bytes .../quantum_ic_fullscreen_white_48.png | Bin 0 -> 101 bytes .../drawable-mdpi/quantum_ic_group_white_36.png | Bin 0 -> 299 bytes .../res/drawable-mdpi/quantum_ic_hd_white_24.png | Bin 0 -> 154 bytes .../quantum_ic_headset_grey600_24.png | Bin 0 -> 245 bytes .../drawable-mdpi/quantum_ic_headset_white_36.png | Bin 0 -> 350 bytes .../drawable-mdpi/quantum_ic_message_white_24.png | Bin 0 -> 130 bytes .../drawable-mdpi/quantum_ic_mic_off_black_24.png | Bin 0 -> 271 bytes .../drawable-mdpi/quantum_ic_mic_off_white_36.png | Bin 0 -> 428 bytes .../quantum_ic_network_wifi_white_24.png | Bin 0 -> 299 bytes .../drawable-mdpi/quantum_ic_pause_white_36.png | Bin 0 -> 105 bytes .../quantum_ic_person_add_white_24.png | Bin 0 -> 204 bytes .../quantum_ic_photo_library_white_24.png | Bin 0 -> 193 bytes .../drawable-mdpi/quantum_ic_photo_white_24.png | Bin 0 -> 185 bytes .../drawable-mdpi/quantum_ic_photo_white_48.png | Bin 0 -> 304 bytes .../drawable-mdpi/quantum_ic_report_white_18.png | Bin 0 -> 163 bytes .../res/drawable-mdpi/quantum_ic_send_black_24.png | Bin 0 -> 240 bytes .../res/drawable-mdpi/quantum_ic_send_white_24.png | Bin 0 -> 237 bytes .../quantum_ic_swap_calls_white_36.png | Bin 0 -> 314 bytes .../quantum_ic_switch_camera_white_36.png | Bin 0 -> 234 bytes .../quantum_ic_switch_video_white_36.png | Bin 0 -> 225 bytes .../res/drawable-mdpi/quantum_ic_undo_white_48.png | Bin 0 -> 321 bytes .../quantum_ic_videocam_off_white_24.png | Bin 0 -> 198 bytes .../quantum_ic_videocam_off_white_36.png | Bin 0 -> 271 bytes .../drawable-mdpi/quantum_ic_videocam_white_18.png | Bin 0 -> 133 bytes .../drawable-mdpi/quantum_ic_videocam_white_24.png | Bin 0 -> 131 bytes .../drawable-mdpi/quantum_ic_videocam_white_36.png | Bin 0 -> 173 bytes .../quantum_ic_voicemail_white_18.png | Bin 0 -> 259 bytes .../quantum_ic_volume_up_grey600_24.png | Bin 0 -> 256 bytes .../quantum_ic_volume_up_white_36.png | Bin 0 -> 365 bytes .../quantum_ic_arrow_back_white_24.png | Bin 0 -> 131 bytes .../quantum_ic_arrow_drop_down_white_18.png | Bin 0 -> 123 bytes .../quantum_ic_bluetooth_audio_grey600_24.png | Bin 0 -> 518 bytes .../quantum_ic_bluetooth_audio_white_36.png | Bin 0 -> 778 bytes .../quantum_ic_call_end_white_24.png | Bin 0 -> 389 bytes .../quantum_ic_call_end_white_36.png | Bin 0 -> 553 bytes .../quantum_ic_call_merge_white_36.png | Bin 0 -> 287 bytes .../drawable-xhdpi/quantum_ic_call_white_18.png | Bin 0 -> 340 bytes .../drawable-xhdpi/quantum_ic_call_white_24.png | Bin 0 -> 420 bytes .../quantum_ic_camera_alt_white_24.png | Bin 0 -> 446 bytes .../quantum_ic_camera_alt_white_48.png | Bin 0 -> 894 bytes .../quantum_ic_camera_front_white_36.png | Bin 0 -> 444 bytes .../quantum_ic_camera_rear_white_36.png | Bin 0 -> 377 bytes .../drawable-xhdpi/quantum_ic_check_black_24.png | Bin 0 -> 188 bytes .../drawable-xhdpi/quantum_ic_check_black_36.png | Bin 0 -> 254 bytes .../drawable-xhdpi/quantum_ic_check_white_48.png | Bin 0 -> 308 bytes .../drawable-xhdpi/quantum_ic_close_white_24.png | Bin 0 -> 257 bytes .../drawable-xhdpi/quantum_ic_close_white_36.png | Bin 0 -> 347 bytes .../drawable-xhdpi/quantum_ic_dialpad_white_36.png | Bin 0 -> 362 bytes .../drawable-xhdpi/quantum_ic_forward_white_24.png | Bin 0 -> 159 bytes .../quantum_ic_fullscreen_exit_white_48.png | Bin 0 -> 106 bytes .../quantum_ic_fullscreen_white_36.png | Bin 0 -> 107 bytes .../quantum_ic_fullscreen_white_48.png | Bin 0 -> 109 bytes .../drawable-xhdpi/quantum_ic_group_white_36.png | Bin 0 -> 488 bytes .../res/drawable-xhdpi/quantum_ic_hd_white_24.png | Bin 0 -> 201 bytes .../quantum_ic_headset_grey600_24.png | Bin 0 -> 440 bytes .../drawable-xhdpi/quantum_ic_headset_white_36.png | Bin 0 -> 610 bytes .../drawable-xhdpi/quantum_ic_message_white_24.png | Bin 0 -> 204 bytes .../drawable-xhdpi/quantum_ic_mic_off_black_24.png | Bin 0 -> 454 bytes .../drawable-xhdpi/quantum_ic_mic_off_white_36.png | Bin 0 -> 713 bytes .../quantum_ic_network_wifi_white_24.png | Bin 0 -> 538 bytes .../drawable-xhdpi/quantum_ic_pause_white_36.png | Bin 0 -> 92 bytes .../quantum_ic_person_add_white_24.png | Bin 0 -> 329 bytes .../quantum_ic_photo_library_white_24.png | Bin 0 -> 309 bytes .../drawable-xhdpi/quantum_ic_photo_white_24.png | Bin 0 -> 304 bytes .../drawable-xhdpi/quantum_ic_photo_white_48.png | Bin 0 -> 570 bytes .../drawable-xhdpi/quantum_ic_report_white_18.png | Bin 0 -> 240 bytes .../drawable-xhdpi/quantum_ic_send_black_24.png | Bin 0 -> 333 bytes .../drawable-xhdpi/quantum_ic_send_white_24.png | Bin 0 -> 344 bytes .../quantum_ic_swap_calls_white_36.png | Bin 0 -> 484 bytes .../quantum_ic_switch_camera_white_36.png | Bin 0 -> 379 bytes .../quantum_ic_switch_video_white_36.png | Bin 0 -> 309 bytes .../drawable-xhdpi/quantum_ic_undo_white_48.png | Bin 0 -> 563 bytes .../quantum_ic_videocam_off_white_24.png | Bin 0 -> 296 bytes .../quantum_ic_videocam_off_white_36.png | Bin 0 -> 412 bytes .../quantum_ic_videocam_white_18.png | Bin 0 -> 173 bytes .../quantum_ic_videocam_white_24.png | Bin 0 -> 178 bytes .../quantum_ic_videocam_white_36.png | Bin 0 -> 234 bytes .../quantum_ic_voicemail_white_18.png | Bin 0 -> 478 bytes .../quantum_ic_volume_up_grey600_24.png | Bin 0 -> 459 bytes .../quantum_ic_volume_up_white_36.png | Bin 0 -> 654 bytes .../quantum_ic_arrow_back_white_24.png | Bin 0 -> 191 bytes .../quantum_ic_arrow_drop_down_white_18.png | Bin 0 -> 126 bytes .../quantum_ic_bluetooth_audio_grey600_24.png | Bin 0 -> 794 bytes .../quantum_ic_bluetooth_audio_white_36.png | Bin 0 -> 1080 bytes .../quantum_ic_call_end_white_24.png | Bin 0 -> 553 bytes .../quantum_ic_call_end_white_36.png | Bin 0 -> 778 bytes .../quantum_ic_call_merge_white_36.png | Bin 0 -> 388 bytes .../drawable-xxhdpi/quantum_ic_call_white_18.png | Bin 0 -> 491 bytes .../drawable-xxhdpi/quantum_ic_call_white_24.png | Bin 0 -> 597 bytes .../quantum_ic_camera_alt_white_24.png | Bin 0 -> 666 bytes .../quantum_ic_camera_alt_white_48.png | Bin 0 -> 1309 bytes .../quantum_ic_camera_front_white_36.png | Bin 0 -> 676 bytes .../quantum_ic_camera_rear_white_36.png | Bin 0 -> 568 bytes .../drawable-xxhdpi/quantum_ic_check_black_24.png | Bin 0 -> 254 bytes .../drawable-xxhdpi/quantum_ic_check_black_36.png | Bin 0 -> 295 bytes .../drawable-xxhdpi/quantum_ic_check_white_48.png | Bin 0 -> 386 bytes .../drawable-xxhdpi/quantum_ic_close_white_24.png | Bin 0 -> 347 bytes .../drawable-xxhdpi/quantum_ic_close_white_36.png | Bin 0 -> 454 bytes .../quantum_ic_dialpad_white_36.png | Bin 0 -> 452 bytes .../quantum_ic_forward_white_24.png | Bin 0 -> 204 bytes .../quantum_ic_fullscreen_exit_white_48.png | Bin 0 -> 123 bytes .../quantum_ic_fullscreen_white_36.png | Bin 0 -> 114 bytes .../quantum_ic_fullscreen_white_48.png | Bin 0 -> 123 bytes .../drawable-xxhdpi/quantum_ic_group_white_36.png | Bin 0 -> 705 bytes .../res/drawable-xxhdpi/quantum_ic_hd_white_24.png | Bin 0 -> 290 bytes .../quantum_ic_headset_grey600_24.png | Bin 0 -> 635 bytes .../quantum_ic_headset_white_36.png | Bin 0 -> 936 bytes .../quantum_ic_message_white_24.png | Bin 0 -> 269 bytes .../quantum_ic_mic_off_black_24.png | Bin 0 -> 671 bytes .../quantum_ic_mic_off_white_36.png | Bin 0 -> 1044 bytes .../quantum_ic_network_wifi_white_24.png | Bin 0 -> 786 bytes .../drawable-xxhdpi/quantum_ic_pause_white_36.png | Bin 0 -> 158 bytes .../quantum_ic_person_add_white_24.png | Bin 0 -> 464 bytes .../quantum_ic_photo_library_white_24.png | Bin 0 -> 431 bytes .../drawable-xxhdpi/quantum_ic_photo_white_24.png | Bin 0 -> 450 bytes .../drawable-xxhdpi/quantum_ic_photo_white_48.png | Bin 0 -> 859 bytes .../drawable-xxhdpi/quantum_ic_report_white_18.png | Bin 0 -> 312 bytes .../drawable-xxhdpi/quantum_ic_send_black_24.png | Bin 0 -> 455 bytes .../drawable-xxhdpi/quantum_ic_send_white_24.png | Bin 0 -> 446 bytes .../quantum_ic_swap_calls_white_36.png | Bin 0 -> 827 bytes .../quantum_ic_switch_camera_white_36.png | Bin 0 -> 544 bytes .../quantum_ic_switch_video_white_36.png | Bin 0 -> 469 bytes .../drawable-xxhdpi/quantum_ic_undo_white_48.png | Bin 0 -> 815 bytes .../quantum_ic_videocam_off_white_24.png | Bin 0 -> 412 bytes .../quantum_ic_videocam_off_white_36.png | Bin 0 -> 570 bytes .../quantum_ic_videocam_white_18.png | Bin 0 -> 222 bytes .../quantum_ic_videocam_white_24.png | Bin 0 -> 234 bytes .../quantum_ic_videocam_white_36.png | Bin 0 -> 350 bytes .../quantum_ic_voicemail_white_18.png | Bin 0 -> 701 bytes .../quantum_ic_volume_up_grey600_24.png | Bin 0 -> 673 bytes .../quantum_ic_volume_up_white_36.png | Bin 0 -> 998 bytes .../quantum_ic_arrow_back_white_24.png | Bin 0 -> 194 bytes .../quantum_ic_arrow_drop_down_white_18.png | Bin 0 -> 152 bytes .../quantum_ic_bluetooth_audio_grey600_24.png | Bin 0 -> 952 bytes .../quantum_ic_bluetooth_audio_white_36.png | Bin 0 -> 1391 bytes .../quantum_ic_call_end_white_24.png | Bin 0 -> 712 bytes .../quantum_ic_call_end_white_36.png | Bin 0 -> 1039 bytes .../quantum_ic_call_merge_white_36.png | Bin 0 -> 435 bytes .../drawable-xxxhdpi/quantum_ic_call_white_18.png | Bin 0 -> 597 bytes .../drawable-xxxhdpi/quantum_ic_call_white_24.png | Bin 0 -> 778 bytes .../quantum_ic_camera_alt_white_24.png | Bin 0 -> 894 bytes .../quantum_ic_camera_alt_white_48.png | Bin 0 -> 1837 bytes .../quantum_ic_camera_front_white_36.png | Bin 0 -> 875 bytes .../quantum_ic_camera_rear_white_36.png | Bin 0 -> 745 bytes .../drawable-xxxhdpi/quantum_ic_check_black_24.png | Bin 0 -> 277 bytes .../drawable-xxxhdpi/quantum_ic_check_black_36.png | Bin 0 -> 345 bytes .../drawable-xxxhdpi/quantum_ic_check_white_48.png | Bin 0 -> 466 bytes .../drawable-xxxhdpi/quantum_ic_close_white_24.png | Bin 0 -> 436 bytes .../drawable-xxxhdpi/quantum_ic_close_white_36.png | Bin 0 -> 524 bytes .../quantum_ic_dialpad_white_36.png | Bin 0 -> 754 bytes .../quantum_ic_forward_white_24.png | Bin 0 -> 236 bytes .../quantum_ic_fullscreen_exit_white_48.png | Bin 0 -> 125 bytes .../quantum_ic_fullscreen_white_36.png | Bin 0 -> 123 bytes .../quantum_ic_fullscreen_white_48.png | Bin 0 -> 124 bytes .../drawable-xxxhdpi/quantum_ic_group_white_36.png | Bin 0 -> 980 bytes .../drawable-xxxhdpi/quantum_ic_hd_white_24.png | Bin 0 -> 348 bytes .../quantum_ic_headset_grey600_24.png | Bin 0 -> 856 bytes .../quantum_ic_headset_white_36.png | Bin 0 -> 1246 bytes .../quantum_ic_message_white_24.png | Bin 0 -> 342 bytes .../quantum_ic_mic_off_black_24.png | Bin 0 -> 832 bytes .../quantum_ic_mic_off_white_36.png | Bin 0 -> 1326 bytes .../quantum_ic_network_wifi_white_24.png | Bin 0 -> 1043 bytes .../drawable-xxxhdpi/quantum_ic_pause_white_36.png | Bin 0 -> 110 bytes .../quantum_ic_person_add_white_24.png | Bin 0 -> 610 bytes .../quantum_ic_photo_library_white_24.png | Bin 0 -> 553 bytes .../drawable-xxxhdpi/quantum_ic_photo_white_24.png | Bin 0 -> 570 bytes .../drawable-xxxhdpi/quantum_ic_photo_white_48.png | Bin 0 -> 1178 bytes .../quantum_ic_report_white_18.png | Bin 0 -> 340 bytes .../drawable-xxxhdpi/quantum_ic_send_black_24.png | Bin 0 -> 585 bytes .../drawable-xxxhdpi/quantum_ic_send_white_24.png | Bin 0 -> 576 bytes .../quantum_ic_swap_calls_white_36.png | Bin 0 -> 1008 bytes .../quantum_ic_switch_camera_white_36.png | Bin 0 -> 713 bytes .../quantum_ic_switch_video_white_36.png | Bin 0 -> 569 bytes .../drawable-xxxhdpi/quantum_ic_undo_white_48.png | Bin 0 -> 1072 bytes .../quantum_ic_videocam_off_white_24.png | Bin 0 -> 495 bytes .../quantum_ic_videocam_off_white_36.png | Bin 0 -> 716 bytes .../quantum_ic_videocam_white_18.png | Bin 0 -> 234 bytes .../quantum_ic_videocam_white_24.png | Bin 0 -> 290 bytes .../quantum_ic_videocam_white_36.png | Bin 0 -> 437 bytes .../quantum_ic_voicemail_white_18.png | Bin 0 -> 625 bytes .../quantum_ic_volume_up_grey600_24.png | Bin 0 -> 895 bytes .../quantum_ic_volume_up_white_36.png | Bin 0 -> 1304 bytes build-app.gradle | 39 - build-library.gradle | 39 - .../android/contacts/common/AndroidManifest.xml | 39 + java/com/android/contacts/common/Bindings.java | 52 + .../android/contacts/common/ClipboardUtils.java | 55 + java/com/android/contacts/common/Collapser.java | 95 + .../contacts/common/ContactPhotoManager.java | 487 +++++ .../contacts/common/ContactPhotoManagerImpl.java | 1262 +++++++++++++ .../contacts/common/ContactPresenceIconUtil.java | 46 + .../android/contacts/common/ContactStatusUtil.java | 44 + .../contacts/common/ContactTileLoaderFactory.java | 64 + .../com/android/contacts/common/ContactsUtils.java | 265 +++ java/com/android/contacts/common/GeoUtil.java | 55 + .../com/android/contacts/common/GroupMetaData.java | 76 + .../android/contacts/common/MoreContactUtils.java | 251 +++ .../common/bindings/ContactsCommonBindings.java | 25 + .../bindings/ContactsCommonBindingsFactory.java | 24 + .../bindings/ContactsCommonBindingsStub.java | 27 + .../android/contacts/common/compat/CallCompat.java | 45 + .../contacts/common/compat/CallableCompat.java | 36 + .../contacts/common/compat/ContactsCompat.java | 57 + .../contacts/common/compat/DirectoryCompat.java | 51 + .../contacts/common/compat/PhoneAccountCompat.java | 104 ++ .../contacts/common/compat/PhoneCompat.java | 36 + .../common/compat/PhoneNumberUtilsCompat.java | 174 ++ .../common/compat/TelephonyManagerCompat.java | 213 +++ .../compat/telecom/TelecomManagerCompat.java | 302 +++ .../common/database/ContactUpdateUtils.java | 49 + .../contacts/common/database/EmptyCursor.java | 84 + .../database/NoNullCursorAsyncQueryHandler.java | 73 + .../contacts/common/dialog/CallSubjectDialog.java | 607 ++++++ .../common/dialog/ClearFrequentsDialog.java | 88 + .../common/extensions/PhoneDirectoryExtender.java | 28 + .../extensions/PhoneDirectoryExtenderAccessor.java | 45 + .../extensions/PhoneDirectoryExtenderFactory.java | 27 + .../extensions/PhoneDirectoryExtenderStub.java | 29 + .../contacts/common/format/FormatUtils.java | 181 ++ .../contacts/common/format/TextHighlighter.java | 93 + .../common/format/testing/SpannedTestUtils.java | 85 + .../common/lettertiles/LetterTileDrawable.java | 382 ++++ .../contacts/common/list/AutoScrollListView.java | 125 ++ .../android/contacts/common/list/ContactEntry.java | 57 + .../common/list/ContactEntryListAdapter.java | 742 ++++++++ .../common/list/ContactEntryListFragment.java | 862 +++++++++ .../contacts/common/list/ContactListAdapter.java | 232 +++ .../contacts/common/list/ContactListFilter.java | 297 +++ .../common/list/ContactListFilterController.java | 170 ++ .../contacts/common/list/ContactListItemView.java | 1513 +++++++++++++++ .../common/list/ContactListPinnedHeaderView.java | 70 + .../contacts/common/list/ContactTileView.java | 171 ++ .../common/list/ContactsSectionIndexer.java | 119 ++ .../common/list/DefaultContactListAdapter.java | 216 +++ .../contacts/common/list/DirectoryListLoader.java | 201 ++ .../contacts/common/list/DirectoryPartition.java | 179 ++ .../contacts/common/list/IndexerListAdapter.java | 214 +++ .../list/OnPhoneNumberPickerActionListener.java | 39 + .../common/list/PhoneNumberListAdapter.java | 583 ++++++ .../common/list/PhoneNumberPickerFragment.java | 402 ++++ .../common/list/PinnedHeaderListAdapter.java | 159 ++ .../contacts/common/list/PinnedHeaderListView.java | 563 ++++++ .../contacts/common/list/ViewPagerTabStrip.java | 109 ++ .../contacts/common/list/ViewPagerTabs.java | 317 ++++ .../contacts/common/location/CountryDetector.java | 221 +++ .../common/location/UpdateCountryService.java | 104 ++ .../contacts/common/model/AccountTypeManager.java | 813 ++++++++ .../contacts/common/model/BuilderWrapper.java | 53 + .../android/contacts/common/model/CPOWrapper.java | 50 + .../com/android/contacts/common/model/Contact.java | 384 ++++ .../contacts/common/model/ContactLoader.java | 998 ++++++++++ .../android/contacts/common/model/RawContact.java | 351 ++++ .../contacts/common/model/account/AccountType.java | 501 +++++ .../model/account/AccountTypeWithDataSet.java | 103 ++ .../common/model/account/AccountWithDataSet.java | 229 +++ .../common/model/account/BaseAccountType.java | 1890 +++++++++++++++++++ .../common/model/account/ExchangeAccountType.java | 365 ++++ .../common/model/account/ExternalAccountType.java | 443 +++++ .../common/model/account/FallbackAccountType.java | 77 + .../common/model/account/GoogleAccountType.java | 206 +++ .../common/model/account/SamsungAccountType.java | 235 +++ .../contacts/common/model/dataitem/DataItem.java | 258 +++ .../contacts/common/model/dataitem/DataKind.java | 132 ++ .../common/model/dataitem/EmailDataItem.java | 47 + .../common/model/dataitem/EventDataItem.java | 62 + .../model/dataitem/GroupMembershipDataItem.java | 40 + .../common/model/dataitem/IdentityDataItem.java | 39 + .../contacts/common/model/dataitem/ImDataItem.java | 109 ++ .../common/model/dataitem/NicknameDataItem.java | 39 + .../common/model/dataitem/NoteDataItem.java | 35 + .../model/dataitem/OrganizationDataItem.java | 64 + .../common/model/dataitem/PhoneDataItem.java | 76 + .../common/model/dataitem/PhotoDataItem.java | 39 + .../common/model/dataitem/RelationDataItem.java | 62 + .../common/model/dataitem/SipAddressDataItem.java | 40 + .../model/dataitem/StructuredNameDataItem.java | 100 + .../model/dataitem/StructuredPostalDataItem.java | 68 + .../common/model/dataitem/WebsiteDataItem.java | 39 + .../common/preference/ContactsPreferences.java | 269 +++ .../common/preference/DisplayOrderPreference.java | 89 + .../common/preference/SortOrderPreference.java | 89 + .../contacts/common/res/color/popup_menu_color.xml | 20 + .../contacts/common/res/color/tab_text_color.xml | 21 + .../common/res/drawable-hdpi/ic_ab_search.png | Bin 0 -> 1115 bytes .../res/drawable-hdpi/ic_arrow_back_24dp.png | Bin 0 -> 612 bytes .../res/drawable-hdpi/ic_business_white_120dp.png | Bin 0 -> 2477 bytes .../common/res/drawable-hdpi/ic_call_24dp.png | Bin 0 -> 340 bytes .../res/drawable-hdpi/ic_call_note_white_24dp.png | Bin 0 -> 373 bytes .../common/res/drawable-hdpi/ic_close_dk.png | Bin 0 -> 609 bytes .../common/res/drawable-hdpi/ic_create_24dp.png | Bin 0 -> 370 bytes .../res/drawable-hdpi/ic_group_white_24dp.png | Bin 0 -> 389 bytes .../ic_history_white_drawable_24dp.png | Bin 0 -> 525 bytes .../res/drawable-hdpi/ic_info_outline_24dp.png | Bin 0 -> 485 bytes .../common/res/drawable-hdpi/ic_menu_back.png | Bin 0 -> 799 bytes .../common/res/drawable-hdpi/ic_menu_group_dk.png | Bin 0 -> 1954 bytes .../common/res/drawable-hdpi/ic_menu_group_lt.png | Bin 0 -> 1922 bytes .../res/drawable-hdpi/ic_menu_overflow_lt.png | Bin 0 -> 220 bytes .../common/res/drawable-hdpi/ic_menu_person_dk.png | Bin 0 -> 1439 bytes .../common/res/drawable-hdpi/ic_menu_person_lt.png | Bin 0 -> 1416 bytes .../ic_menu_remove_field_holo_light.png | Bin 0 -> 515 bytes .../common/res/drawable-hdpi/ic_menu_star_dk.png | Bin 0 -> 1438 bytes .../res/drawable-hdpi/ic_menu_star_holo_light.png | Bin 0 -> 1211 bytes .../common/res/drawable-hdpi/ic_menu_star_lt.png | Bin 0 -> 1414 bytes .../common/res/drawable-hdpi/ic_message_24dp.png | Bin 0 -> 167 bytes .../common/res/drawable-hdpi/ic_person_24dp.png | Bin 0 -> 273 bytes .../res/drawable-hdpi/ic_person_add_24dp.png | Bin 0 -> 289 bytes .../common/res/drawable-hdpi/ic_phone_attach.png | Bin 0 -> 828 bytes .../common/res/drawable-hdpi/ic_rx_videocam.png | Bin 0 -> 413 bytes .../common/res/drawable-hdpi/ic_scroll_handle.png | Bin 0 -> 544 bytes .../common/res/drawable-hdpi/ic_tx_videocam.png | Bin 0 -> 370 bytes .../common/res/drawable-hdpi/ic_videocam.png | Bin 0 -> 269 bytes .../res/drawable-hdpi/ic_voicemail_avatar.png | Bin 0 -> 3607 bytes .../res/drawable-hdpi/list_activated_holo.9.png | Bin 0 -> 154 bytes .../res/drawable-hdpi/list_background_holo.9.png | Bin 0 -> 224 bytes .../res/drawable-hdpi/list_focused_holo.9.png | Bin 0 -> 235 bytes .../list_longpressed_holo_light.9.png | Bin 0 -> 158 bytes .../drawable-hdpi/list_pressed_holo_light.9.png | Bin 0 -> 159 bytes .../list_section_divider_holo_custom.9.png | Bin 0 -> 205 bytes .../common/res/drawable-hdpi/list_title_holo.9.png | Bin 0 -> 267 bytes .../drawable-ldrtl-hdpi/list_background_holo.9.png | Bin 0 -> 219 bytes .../drawable-ldrtl-hdpi/list_focused_holo.9.png | Bin 0 -> 234 bytes .../list_section_divider_holo_custom.9.png | Bin 0 -> 191 bytes .../res/drawable-ldrtl-hdpi/list_title_holo.9.png | Bin 0 -> 258 bytes .../drawable-ldrtl-mdpi/list_background_holo.9.png | Bin 0 -> 178 bytes .../drawable-ldrtl-mdpi/list_focused_holo.9.png | Bin 0 -> 234 bytes .../list_section_divider_holo_custom.9.png | Bin 0 -> 180 bytes .../res/drawable-ldrtl-mdpi/list_title_holo.9.png | Bin 0 -> 186 bytes .../list_activated_holo.9.png | Bin 0 -> 1666 bytes .../list_activated_holo.9.png | Bin 0 -> 1034 bytes .../list_activated_holo.9.png | Bin 0 -> 2486 bytes .../list_background_holo.9.png | Bin 0 -> 243 bytes .../drawable-ldrtl-xhdpi/list_focused_holo.9.png | Bin 0 -> 234 bytes .../list_section_divider_holo_custom.9.png | Bin 0 -> 196 bytes .../res/drawable-ldrtl-xhdpi/list_title_holo.9.png | Bin 0 -> 255 bytes .../common/res/drawable-mdpi/ic_ab_search.png | Bin 0 -> 781 bytes .../res/drawable-mdpi/ic_arrow_back_24dp.png | Bin 0 -> 578 bytes .../res/drawable-mdpi/ic_business_white_120dp.png | Bin 0 -> 2040 bytes .../common/res/drawable-mdpi/ic_call_24dp.png | Bin 0 -> 246 bytes .../res/drawable-mdpi/ic_call_note_white_24dp.png | Bin 0 -> 266 bytes .../common/res/drawable-mdpi/ic_close_dk.png | Bin 0 -> 572 bytes .../common/res/drawable-mdpi/ic_create_24dp.png | Bin 0 -> 290 bytes .../res/drawable-mdpi/ic_group_white_24dp.png | Bin 0 -> 297 bytes .../ic_history_white_drawable_24dp.png | Bin 0 -> 340 bytes .../res/drawable-mdpi/ic_info_outline_24dp.png | Bin 0 -> 320 bytes .../common/res/drawable-mdpi/ic_menu_back.png | Bin 0 -> 607 bytes .../common/res/drawable-mdpi/ic_menu_group_dk.png | Bin 0 -> 1266 bytes .../common/res/drawable-mdpi/ic_menu_group_lt.png | Bin 0 -> 1270 bytes .../res/drawable-mdpi/ic_menu_overflow_lt.png | Bin 0 -> 171 bytes .../common/res/drawable-mdpi/ic_menu_person_dk.png | Bin 0 -> 1052 bytes .../common/res/drawable-mdpi/ic_menu_person_lt.png | Bin 0 -> 1021 bytes .../ic_menu_remove_field_holo_light.png | Bin 0 -> 424 bytes .../common/res/drawable-mdpi/ic_menu_star_dk.png | Bin 0 -> 1034 bytes .../common/res/drawable-mdpi/ic_menu_star_lt.png | Bin 0 -> 1018 bytes .../common/res/drawable-mdpi/ic_message_24dp.png | Bin 0 -> 130 bytes .../common/res/drawable-mdpi/ic_person_24dp.png | Bin 0 -> 188 bytes .../res/drawable-mdpi/ic_person_add_24dp.png | Bin 0 -> 204 bytes .../common/res/drawable-mdpi/ic_phone_attach.png | Bin 0 -> 476 bytes .../common/res/drawable-mdpi/ic_rx_videocam.png | Bin 0 -> 299 bytes .../common/res/drawable-mdpi/ic_scroll_handle.png | Bin 0 -> 504 bytes .../common/res/drawable-mdpi/ic_tx_videocam.png | Bin 0 -> 265 bytes .../common/res/drawable-mdpi/ic_videocam.png | Bin 0 -> 216 bytes .../res/drawable-mdpi/ic_voicemail_avatar.png | Bin 0 -> 2120 bytes .../res/drawable-mdpi/list_activated_holo.9.png | Bin 0 -> 151 bytes .../res/drawable-mdpi/list_background_holo.9.png | Bin 0 -> 188 bytes .../res/drawable-mdpi/list_focused_holo.9.png | Bin 0 -> 235 bytes .../list_longpressed_holo_light.9.png | Bin 0 -> 155 bytes .../drawable-mdpi/list_pressed_holo_light.9.png | Bin 0 -> 158 bytes .../list_section_divider_holo_custom.9.png | Bin 0 -> 198 bytes .../common/res/drawable-mdpi/list_title_holo.9.png | Bin 0 -> 199 bytes .../list_activated_holo.9.png | Bin 0 -> 1659 bytes .../list_activated_holo.9.png | Bin 0 -> 1005 bytes .../list_activated_holo.9.png | Bin 0 -> 2478 bytes .../common/res/drawable-xhdpi/ic_ab_search.png | Bin 0 -> 1451 bytes .../res/drawable-xhdpi/ic_arrow_back_24dp.png | Bin 0 -> 765 bytes .../res/drawable-xhdpi/ic_business_white_120dp.png | Bin 0 -> 2916 bytes .../common/res/drawable-xhdpi/ic_call_24dp.png | Bin 0 -> 420 bytes .../res/drawable-xhdpi/ic_call_note_white_24dp.png | Bin 0 -> 449 bytes .../common/res/drawable-xhdpi/ic_close_dk.png | Bin 0 -> 814 bytes .../common/res/drawable-xhdpi/ic_create_24dp.png | Bin 0 -> 426 bytes .../res/drawable-xhdpi/ic_group_white_24dp.png | Bin 0 -> 461 bytes .../ic_history_white_drawable_24dp.png | Bin 0 -> 659 bytes .../res/drawable-xhdpi/ic_info_outline_24dp.png | Bin 0 -> 655 bytes .../common/res/drawable-xhdpi/ic_menu_back.png | Bin 0 -> 1034 bytes .../common/res/drawable-xhdpi/ic_menu_group_dk.png | Bin 0 -> 2650 bytes .../common/res/drawable-xhdpi/ic_menu_group_lt.png | Bin 0 -> 2632 bytes .../res/drawable-xhdpi/ic_menu_overflow_lt.png | Bin 0 -> 287 bytes .../res/drawable-xhdpi/ic_menu_person_dk.png | Bin 0 -> 1844 bytes .../res/drawable-xhdpi/ic_menu_person_lt.png | Bin 0 -> 1815 bytes .../ic_menu_remove_field_holo_light.png | Bin 0 -> 593 bytes .../common/res/drawable-xhdpi/ic_menu_star_dk.png | Bin 0 -> 1830 bytes .../res/drawable-xhdpi/ic_menu_star_holo_light.png | Bin 0 -> 1607 bytes .../common/res/drawable-xhdpi/ic_menu_star_lt.png | Bin 0 -> 1827 bytes .../common/res/drawable-xhdpi/ic_message_24dp.png | Bin 0 -> 204 bytes .../common/res/drawable-xhdpi/ic_person_24dp.png | Bin 0 -> 312 bytes .../res/drawable-xhdpi/ic_person_add_24dp.png | Bin 0 -> 329 bytes .../common/res/drawable-xhdpi/ic_phone_attach.png | Bin 0 -> 1009 bytes .../common/res/drawable-xhdpi/ic_rx_videocam.png | Bin 0 -> 439 bytes .../common/res/drawable-xhdpi/ic_scroll_handle.png | Bin 0 -> 620 bytes .../common/res/drawable-xhdpi/ic_tx_videocam.png | Bin 0 -> 405 bytes .../common/res/drawable-xhdpi/ic_videocam.png | Bin 0 -> 301 bytes .../res/drawable-xhdpi/ic_voicemail_avatar.png | Bin 0 -> 4894 bytes .../res/drawable-xhdpi/list_activated_holo.9.png | Bin 0 -> 158 bytes .../res/drawable-xhdpi/list_background_holo.9.png | Bin 0 -> 245 bytes .../res/drawable-xhdpi/list_focused_holo.9.png | Bin 0 -> 235 bytes .../list_longpressed_holo_light.9.png | Bin 0 -> 162 bytes .../drawable-xhdpi/list_pressed_holo_light.9.png | Bin 0 -> 163 bytes .../list_section_divider_holo_custom.9.png | Bin 0 -> 210 bytes .../res/drawable-xhdpi/list_title_holo.9.png | Bin 0 -> 267 bytes .../common/res/drawable-xxhdpi/ic_ab_search.png | Bin 0 -> 2100 bytes .../res/drawable-xxhdpi/ic_arrow_back_24dp.png | Bin 0 -> 1376 bytes .../drawable-xxhdpi/ic_business_white_120dp.png | Bin 0 -> 2541 bytes .../common/res/drawable-xxhdpi/ic_call_24dp.png | Bin 0 -> 597 bytes .../drawable-xxhdpi/ic_call_note_white_24dp.png | Bin 0 -> 647 bytes .../common/res/drawable-xxhdpi/ic_close_dk.png | Bin 0 -> 1465 bytes .../common/res/drawable-xxhdpi/ic_create_24dp.png | Bin 0 -> 668 bytes .../res/drawable-xxhdpi/ic_group_white_24dp.png | Bin 0 -> 604 bytes .../ic_history_white_drawable_24dp.png | Bin 0 -> 971 bytes .../res/drawable-xxhdpi/ic_info_outline_24dp.png | Bin 0 -> 953 bytes .../common/res/drawable-xxhdpi/ic_menu_back.png | Bin 0 -> 1546 bytes .../res/drawable-xxhdpi/ic_menu_group_dk.png | Bin 0 -> 3338 bytes .../res/drawable-xxhdpi/ic_menu_group_lt.png | Bin 0 -> 3381 bytes .../res/drawable-xxhdpi/ic_menu_overflow_lt.png | Bin 0 -> 414 bytes .../res/drawable-xxhdpi/ic_menu_person_dk.png | Bin 0 -> 2357 bytes .../res/drawable-xxhdpi/ic_menu_person_lt.png | Bin 0 -> 2363 bytes .../ic_menu_remove_field_holo_light.png | Bin 0 -> 1381 bytes .../common/res/drawable-xxhdpi/ic_menu_star_dk.png | Bin 0 -> 2111 bytes .../drawable-xxhdpi/ic_menu_star_holo_light.png | Bin 0 -> 2119 bytes .../common/res/drawable-xxhdpi/ic_menu_star_lt.png | Bin 0 -> 2117 bytes .../common/res/drawable-xxhdpi/ic_message_24dp.png | Bin 0 -> 269 bytes .../common/res/drawable-xxhdpi/ic_person_24dp.png | Bin 0 -> 440 bytes .../res/drawable-xxhdpi/ic_person_add_24dp.png | Bin 0 -> 464 bytes .../common/res/drawable-xxhdpi/ic_phone_attach.png | Bin 0 -> 1517 bytes .../common/res/drawable-xxhdpi/ic_rx_videocam.png | Bin 0 -> 603 bytes .../res/drawable-xxhdpi/ic_scroll_handle.png | Bin 0 -> 837 bytes .../common/res/drawable-xxhdpi/ic_tx_videocam.png | Bin 0 -> 551 bytes .../common/res/drawable-xxhdpi/ic_videocam.png | Bin 0 -> 398 bytes .../res/drawable-xxhdpi/ic_voicemail_avatar.png | Bin 0 -> 7976 bytes .../res/drawable-xxhdpi/list_activated_holo.9.png | Bin 0 -> 1140 bytes .../res/drawable-xxhdpi/list_focused_holo.9.png | Bin 0 -> 1147 bytes .../list_longpressed_holo_light.9.png | Bin 0 -> 1051 bytes .../drawable-xxhdpi/list_pressed_holo_light.9.png | Bin 0 -> 1051 bytes .../res/drawable-xxhdpi/list_title_holo.9.png | Bin 0 -> 465 bytes .../common/res/drawable-xxxhdpi/ic_ab_search.png | Bin 0 -> 2571 bytes .../res/drawable-xxxhdpi/ic_arrow_back_24dp.png | Bin 0 -> 1512 bytes .../drawable-xxxhdpi/ic_business_white_120dp.png | Bin 0 -> 2915 bytes .../common/res/drawable-xxxhdpi/ic_call_24dp.png | Bin 0 -> 778 bytes .../drawable-xxxhdpi/ic_call_note_white_24dp.png | Bin 0 -> 853 bytes .../common/res/drawable-xxxhdpi/ic_close_dk.png | Bin 0 -> 1688 bytes .../common/res/drawable-xxxhdpi/ic_create_24dp.png | Bin 0 -> 612 bytes .../ic_history_white_drawable_24dp.png | Bin 0 -> 1311 bytes .../res/drawable-xxxhdpi/ic_info_outline_24dp.png | Bin 0 -> 1279 bytes .../res/drawable-xxxhdpi/ic_message_24dp.png | Bin 0 -> 342 bytes .../common/res/drawable-xxxhdpi/ic_person_24dp.png | Bin 0 -> 577 bytes .../res/drawable-xxxhdpi/ic_person_add_24dp.png | Bin 0 -> 610 bytes .../res/drawable-xxxhdpi/ic_phone_attach.png | Bin 0 -> 2135 bytes .../res/drawable-xxxhdpi/ic_scroll_handle.png | Bin 0 -> 1579 bytes .../common/res/drawable-xxxhdpi/ic_videocam.png | Bin 0 -> 481 bytes .../res/drawable-xxxhdpi/ic_voicemail_avatar.png | Bin 0 -> 11277 bytes .../res/drawable/dialog_background_material.xml | 23 + .../common/res/drawable/fastscroll_thumb.xml | 19 + .../contacts/common/res/drawable/ic_back_arrow.xml | 20 + .../contacts/common/res/drawable/ic_call.xml | 19 + .../common/res/drawable/ic_message_24dp.xml | 19 + .../contacts/common/res/drawable/ic_more_vert.xml | 9 + .../res/drawable/ic_person_add_tinted_24dp.xml | 20 + .../res/drawable/ic_scroll_handle_default.xml | 20 + .../res/drawable/ic_scroll_handle_pressed.xml | 20 + .../common/res/drawable/ic_search_add_contact.xml | 20 + .../common/res/drawable/ic_search_video_call.xml | 21 + .../contacts/common/res/drawable/ic_tab_all.xml | 21 + .../contacts/common/res/drawable/ic_tab_groups.xml | 21 + .../common/res/drawable/ic_tab_starred.xml | 21 + .../common/res/drawable/ic_work_profile.xml | 16 + .../item_background_material_borderless_dark.xml | 19 + .../res/drawable/item_background_material_dark.xml | 23 + .../drawable/item_background_material_light.xml | 23 + .../drawable/list_item_activated_background.xml | 20 + ...t_selector_background_transition_holo_light.xml | 20 + .../res/drawable/searchedittext_custom_cursor.xml | 7 + .../res/drawable/unread_count_background.xml | 21 + .../res/drawable/view_pager_tab_background.xml | 22 + .../common/res/layout-ldrtl/unread_count_tab.xml | 48 + .../common/res/layout/account_filter_header.xml | 44 + .../res/layout/account_selector_list_item.xml | 57 + .../account_selector_list_item_condensed.xml | 56 + .../common/res/layout/call_subject_history.xml | 33 + .../res/layout/call_subject_history_list_item.xml | 29 + .../res/layout/contact_detail_list_padding.xml | 27 + .../common/res/layout/contact_list_card.xml | 39 + .../common/res/layout/contact_list_content.xml | 61 + .../common/res/layout/default_account_checkbox.xml | 36 + .../common/res/layout/dialog_call_subject.xml | 159 ++ .../common/res/layout/directory_header.xml | 55 + .../contacts/common/res/layout/list_separator.xml | 27 + .../common/res/layout/search_bar_expanded.xml | 62 + .../common/res/layout/select_account_list_item.xml | 56 + .../common/res/layout/unread_count_tab.xml | 43 + .../res/mipmap-hdpi/ic_contacts_launcher.png | Bin 0 -> 3169 bytes .../res/mipmap-mdpi/ic_contacts_launcher.png | Bin 0 -> 2062 bytes .../res/mipmap-xhdpi/ic_contacts_launcher.png | Bin 0 -> 4430 bytes .../res/mipmap-xxhdpi/ic_contacts_launcher.png | Bin 0 -> 7228 bytes .../res/mipmap-xxxhdpi/ic_contacts_launcher.png | Bin 0 -> 10065 bytes .../common/res/values-ja/donottranslate_config.xml | 20 + .../common/res/values-ko/donottranslate_config.xml | 17 + .../contacts/common/res/values-land/integers.xml | 22 + .../common/res/values-sw600dp-land/integers.xml | 22 + .../contacts/common/res/values-sw600dp/dimens.xml | 29 + .../common/res/values-sw600dp/integers.xml | 24 + .../common/res/values-sw720dp-land/integers.xml | 22 + .../common/res/values-sw720dp/integers.xml | 22 + .../res/values-zh-rCN/donottranslate_config.xml | 17 + .../res/values-zh-rTW/donottranslate_config.xml | 17 + .../common/res/values/animation_constants.xml | 19 + .../android/contacts/common/res/values/attrs.xml | 83 + .../android/contacts/common/res/values/colors.xml | 158 ++ .../android/contacts/common/res/values/dimens.xml | 161 ++ .../common/res/values/donottranslate_config.xml | 95 + .../com/android/contacts/common/res/values/ids.xml | 30 + .../contacts/common/res/values/integers.xml | 39 + .../android/contacts/common/res/values/strings.xml | 798 ++++++++ .../android/contacts/common/res/values/styles.xml | 97 + .../contacts/common/testing/InjectedServices.java | 65 + .../contacts/common/util/AccountFilterUtil.java | 125 ++ .../android/contacts/common/util/BitmapUtil.java | 167 ++ .../contacts/common/util/CommonDateUtils.java | 37 + .../android/contacts/common/util/Constants.java | 28 + .../contacts/common/util/ContactDisplayUtils.java | 307 ++++ .../contacts/common/util/ContactListViewUtils.java | 89 + .../contacts/common/util/ContactLoaderUtils.java | 78 + .../android/contacts/common/util/DateUtils.java | 283 +++ java/com/android/contacts/common/util/FabUtil.java | 71 + .../common/util/MaterialColorMapUtils.java | 181 ++ .../contacts/common/util/NameConverter.java | 242 +++ .../android/contacts/common/util/SearchUtil.java | 198 ++ .../android/contacts/common/util/StopWatch.java | 100 + .../common/util/TelephonyManagerUtils.java | 45 + .../contacts/common/util/TrafficStatsTags.java | 22 + .../com/android/contacts/common/util/UriUtils.java | 90 + .../common/widget/ActivityTouchLinearLayout.java | 43 + .../widget/FloatingActionButtonController.java | 226 +++ .../common/widget/LayoutSuppressingImageView.java | 39 + .../widget/SelectPhoneAccountDialogFragment.java | 297 +++ java/com/android/dialer/animation/AnimUtils.java | 247 +++ .../dialer/animation/AnimationListenerAdapter.java | 39 + java/com/android/dialer/app/AndroidManifest.xml | 116 ++ java/com/android/dialer/app/Bindings.java | 77 + .../com/android/dialer/app/CallDetailActivity.java | 480 +++++ java/com/android/dialer/app/DialerApplication.java | 77 + java/com/android/dialer/app/DialtactsActivity.java | 1484 +++++++++++++++ .../dialer/app/FloatingActionButtonBehavior.java | 50 + java/com/android/dialer/app/PhoneCallDetails.java | 207 +++ .../android/dialer/app/SpecialCharSequenceMgr.java | 493 +++++ .../com/android/dialer/app/alert/AlertManager.java | 30 + .../dialer/app/bindings/DialerBindings.java | 25 + .../dialer/app/bindings/DialerBindingsFactory.java | 26 + .../dialer/app/bindings/DialerBindingsStub.java | 48 + .../app/calllog/BlockReportSpamListener.java | 212 +++ .../app/calllog/CallDetailHistoryAdapter.java | 214 +++ .../android/dialer/app/calllog/CallLogAdapter.java | 915 +++++++++ .../dialer/app/calllog/CallLogAlertManager.java | 90 + .../android/dialer/app/calllog/CallLogAsync.java | 96 + .../dialer/app/calllog/CallLogAsyncTaskUtil.java | 376 ++++ .../dialer/app/calllog/CallLogFragment.java | 528 ++++++ .../dialer/app/calllog/CallLogGroupBuilder.java | 274 +++ .../dialer/app/calllog/CallLogListItemHelper.java | 277 +++ .../app/calllog/CallLogListItemViewHolder.java | 966 ++++++++++ .../app/calllog/CallLogModalAlertManager.java | 74 + .../app/calllog/CallLogNotificationsHelper.java | 299 +++ .../app/calllog/CallLogNotificationsService.java | 203 ++ .../dialer/app/calllog/CallLogReceiver.java | 77 + .../android/dialer/app/calllog/CallTypeHelper.java | 136 ++ .../dialer/app/calllog/CallTypeIconsView.java | 221 +++ .../dialer/app/calllog/ClearCallLogDialog.java | 98 + .../app/calllog/DefaultVoicemailNotifier.java | 273 +++ .../dialer/app/calllog/GroupingListAdapter.java | 153 ++ .../android/dialer/app/calllog/IntentProvider.java | 198 ++ .../calllog/MissedCallNotificationReceiver.java | 50 + .../dialer/app/calllog/MissedCallNotifier.java | 330 ++++ .../dialer/app/calllog/PhoneAccountUtils.java | 104 ++ .../dialer/app/calllog/PhoneCallDetailsHelper.java | 352 ++++ .../dialer/app/calllog/PhoneCallDetailsViews.java | 75 + .../dialer/app/calllog/PhoneNumberDisplayUtil.java | 85 + .../calllog/VisualVoicemailCallLogFragment.java | 132 ++ .../dialer/app/calllog/VoicemailQueryHandler.java | 74 + .../app/calllog/calllogcache/CallLogCache.java | 105 ++ .../calllog/calllogcache/CallLogCacheLollipop.java | 74 + .../calllogcache/CallLogCacheLollipopMr1.java | 116 ++ .../dialer/app/contactinfo/ContactInfoCache.java | 357 ++++ .../dialer/app/contactinfo/ContactInfoRequest.java | 122 ++ .../dialer/app/contactinfo/ContactPhotoLoader.java | 129 ++ .../ExpirableCacheHeadlessFragment.java | 67 + .../app/contactinfo/NumberWithCountryIso.java | 57 + .../dialer/app/dialpad/DialpadFragment.java | 1689 +++++++++++++++++ .../app/dialpad/PseudoEmergencyAnimator.java | 161 ++ .../dialer/app/dialpad/SmartDialCursorLoader.java | 202 ++ .../app/dialpad/UnicodeDialerKeyListener.java | 56 + .../app/filterednumber/BlockedNumbersAdapter.java | 97 + .../app/filterednumber/BlockedNumbersFragment.java | 271 +++ .../BlockedNumbersSettingsActivity.java | 146 ++ .../dialer/app/filterednumber/NumbersAdapter.java | 138 ++ .../filterednumber/ViewNumbersToImportAdapter.java | 56 + .../ViewNumbersToImportFragment.java | 130 ++ .../app/legacybindings/DialerLegacyBindings.java | 47 + .../DialerLegacyBindingsFactory.java | 26 + .../legacybindings/DialerLegacyBindingsStub.java | 53 + .../dialer/app/list/AllContactsFragment.java | 209 +++ .../dialer/app/list/BlockedListSearchAdapter.java | 84 + .../dialer/app/list/BlockedListSearchFragment.java | 245 +++ .../dialer/app/list/ContentChangedFilter.java | 56 + .../app/list/DialerPhoneNumberListAdapter.java | 228 +++ .../dialer/app/list/DragDropController.java | 106 ++ .../com/android/dialer/app/list/ListsFragment.java | 587 ++++++ .../dialer/app/list/OnDragDropListener.java | 58 + .../app/list/OnListFragmentScrolledListener.java | 27 + .../dialer/app/list/PhoneFavoriteListView.java | 315 ++++ .../app/list/PhoneFavoriteSquareTileView.java | 119 ++ .../dialer/app/list/PhoneFavoriteTileView.java | 155 ++ .../dialer/app/list/PhoneFavoritesTileAdapter.java | 627 +++++++ .../dialer/app/list/RegularSearchFragment.java | 146 ++ .../dialer/app/list/RegularSearchListAdapter.java | 126 ++ java/com/android/dialer/app/list/RemoveView.java | 105 ++ .../android/dialer/app/list/SearchFragment.java | 425 +++++ .../app/list/SmartDialNumberListAdapter.java | 117 ++ .../dialer/app/list/SmartDialSearchFragment.java | 120 ++ .../android/dialer/app/list/SpeedDialFragment.java | 512 ++++++ .../app/manifests/activities/AndroidManifest.xml | 129 ++ .../app/res/color/settings_text_color_primary.xml | 23 + .../res/color/settings_text_color_secondary.xml | 23 + .../app/res/drawable-hdpi/empty_call_log.png | Bin 0 -> 3538 bytes .../app/res/drawable-hdpi/empty_contacts.png | Bin 0 -> 2461 bytes .../app/res/drawable-hdpi/empty_speed_dial.png | Bin 0 -> 6041 bytes .../dialer/app/res/drawable-hdpi/fab_ic_dial.png | Bin 0 -> 1028 bytes .../res/drawable-hdpi/ic_archive_white_24dp.png | Bin 0 -> 247 bytes .../dialer/app/res/drawable-hdpi/ic_call_arrow.png | Bin 0 -> 538 bytes .../app/res/drawable-hdpi/ic_content_copy_24dp.png | Bin 0 -> 203 bytes .../app/res/drawable-hdpi/ic_delete_24dp.png | Bin 0 -> 242 bytes .../res/drawable-hdpi/ic_dialer_fork_add_call.png | Bin 0 -> 1649 bytes .../drawable-hdpi/ic_dialer_fork_current_call.png | Bin 0 -> 2305 bytes .../res/drawable-hdpi/ic_dialer_fork_tt_keypad.png | Bin 0 -> 2419 bytes .../dialer/app/res/drawable-hdpi/ic_grade_24dp.png | Bin 0 -> 370 bytes .../dialer/app/res/drawable-hdpi/ic_handle.png | Bin 0 -> 543 bytes .../app/res/drawable-hdpi/ic_menu_history_lt.png | Bin 0 -> 1565 bytes .../app/res/drawable-hdpi/ic_mic_grey600.png | Bin 0 -> 377 bytes .../app/res/drawable-hdpi/ic_more_vert_24dp.png | Bin 0 -> 134 bytes .../ic_not_interested_googblue_24dp.png | Bin 0 -> 565 bytes .../dialer/app/res/drawable-hdpi/ic_not_spam.png | Bin 0 -> 858 bytes .../dialer/app/res/drawable-hdpi/ic_pause_24dp.png | Bin 0 -> 105 bytes .../app/res/drawable-hdpi/ic_people_24dp.png | Bin 0 -> 299 bytes .../dialer/app/res/drawable-hdpi/ic_phone_24dp.png | Bin 0 -> 347 bytes .../app/res/drawable-hdpi/ic_play_arrow_24dp.png | Bin 0 -> 195 bytes .../dialer/app/res/drawable-hdpi/ic_remove.png | Bin 0 -> 884 bytes .../app/res/drawable-hdpi/ic_results_phone.png | Bin 0 -> 1084 bytes .../app/res/drawable-hdpi/ic_schedule_24dp.png | Bin 0 -> 575 bytes .../app/res/drawable-hdpi/ic_share_white_24dp.png | Bin 0 -> 397 bytes .../dialer/app/res/drawable-hdpi/ic_star.png | Bin 0 -> 732 bytes .../dialer/app/res/drawable-hdpi/ic_unblock.png | Bin 0 -> 1049 bytes .../app/res/drawable-hdpi/ic_vm_sound_off_dis.png | Bin 0 -> 1339 bytes .../app/res/drawable-hdpi/ic_vm_sound_off_dk.png | Bin 0 -> 1337 bytes .../app/res/drawable-hdpi/ic_vm_sound_on_dis.png | Bin 0 -> 1755 bytes .../app/res/drawable-hdpi/ic_vm_sound_on_dk.png | Bin 0 -> 1750 bytes .../app/res/drawable-hdpi/ic_voicemail_24dp.png | Bin 0 -> 478 bytes .../app/res/drawable-hdpi/ic_volume_down_24dp.png | Bin 0 -> 186 bytes .../app/res/drawable-hdpi/ic_volume_up_24dp.png | Bin 0 -> 365 bytes .../app/res/drawable-hdpi/search_shadow.9.png | Bin 0 -> 183 bytes .../app/res/drawable-hdpi/shadow_contact_photo.png | Bin 0 -> 960 bytes .../app/res/drawable-mdpi/empty_call_log.png | Bin 0 -> 2463 bytes .../app/res/drawable-mdpi/empty_contacts.png | Bin 0 -> 1778 bytes .../app/res/drawable-mdpi/empty_speed_dial.png | Bin 0 -> 4119 bytes .../dialer/app/res/drawable-mdpi/fab_ic_dial.png | Bin 0 -> 905 bytes .../res/drawable-mdpi/ic_archive_white_24dp.png | Bin 0 -> 181 bytes .../dialer/app/res/drawable-mdpi/ic_call_arrow.png | Bin 0 -> 455 bytes .../app/res/drawable-mdpi/ic_content_copy_24dp.png | Bin 0 -> 134 bytes .../app/res/drawable-mdpi/ic_delete_24dp.png | Bin 0 -> 195 bytes .../res/drawable-mdpi/ic_dialer_fork_add_call.png | Bin 0 -> 1309 bytes .../drawable-mdpi/ic_dialer_fork_current_call.png | Bin 0 -> 1581 bytes .../res/drawable-mdpi/ic_dialer_fork_tt_keypad.png | Bin 0 -> 1586 bytes .../dialer/app/res/drawable-mdpi/ic_grade_24dp.png | Bin 0 -> 271 bytes .../dialer/app/res/drawable-mdpi/ic_handle.png | Bin 0 -> 454 bytes .../app/res/drawable-mdpi/ic_menu_history_lt.png | Bin 0 -> 1086 bytes .../app/res/drawable-mdpi/ic_mic_grey600.png | Bin 0 -> 252 bytes .../app/res/drawable-mdpi/ic_more_vert_24dp.png | Bin 0 -> 112 bytes .../ic_not_interested_googblue_24dp.png | Bin 0 -> 377 bytes .../dialer/app/res/drawable-mdpi/ic_not_spam.png | Bin 0 -> 627 bytes .../dialer/app/res/drawable-mdpi/ic_pause_24dp.png | Bin 0 -> 83 bytes .../app/res/drawable-mdpi/ic_people_24dp.png | Bin 0 -> 210 bytes .../dialer/app/res/drawable-mdpi/ic_phone_24dp.png | Bin 0 -> 262 bytes .../app/res/drawable-mdpi/ic_play_arrow_24dp.png | Bin 0 -> 157 bytes .../dialer/app/res/drawable-mdpi/ic_remove.png | Bin 0 -> 728 bytes .../app/res/drawable-mdpi/ic_results_phone.png | Bin 0 -> 801 bytes .../app/res/drawable-mdpi/ic_schedule_24dp.png | Bin 0 -> 377 bytes .../app/res/drawable-mdpi/ic_share_white_24dp.png | Bin 0 -> 268 bytes .../dialer/app/res/drawable-mdpi/ic_star.png | Bin 0 -> 531 bytes .../dialer/app/res/drawable-mdpi/ic_unblock.png | Bin 0 -> 746 bytes .../app/res/drawable-mdpi/ic_vm_sound_off_dis.png | Bin 0 -> 948 bytes .../app/res/drawable-mdpi/ic_vm_sound_off_dk.png | Bin 0 -> 945 bytes .../app/res/drawable-mdpi/ic_vm_sound_on_dis.png | Bin 0 -> 1166 bytes .../app/res/drawable-mdpi/ic_vm_sound_on_dk.png | Bin 0 -> 1192 bytes .../app/res/drawable-mdpi/ic_voicemail_24dp.png | Bin 0 -> 221 bytes .../app/res/drawable-mdpi/ic_volume_down_24dp.png | Bin 0 -> 139 bytes .../app/res/drawable-mdpi/ic_volume_up_24dp.png | Bin 0 -> 251 bytes .../app/res/drawable-mdpi/search_shadow.9.png | Bin 0 -> 159 bytes .../app/res/drawable-mdpi/shadow_contact_photo.png | Bin 0 -> 948 bytes .../app/res/drawable-xhdpi/empty_call_log.png | Bin 0 -> 4860 bytes .../app/res/drawable-xhdpi/empty_contacts.png | Bin 0 -> 3352 bytes .../app/res/drawable-xhdpi/empty_speed_dial.png | Bin 0 -> 8689 bytes .../dialer/app/res/drawable-xhdpi/fab_ic_dial.png | Bin 0 -> 1699 bytes .../res/drawable-xhdpi/ic_archive_white_24dp.png | Bin 0 -> 267 bytes .../app/res/drawable-xhdpi/ic_call_arrow.png | Bin 0 -> 627 bytes .../res/drawable-xhdpi/ic_content_copy_24dp.png | Bin 0 -> 188 bytes .../app/res/drawable-xhdpi/ic_delete_24dp.png | Bin 0 -> 271 bytes .../res/drawable-xhdpi/ic_dialer_fork_add_call.png | Bin 0 -> 2150 bytes .../drawable-xhdpi/ic_dialer_fork_current_call.png | Bin 0 -> 3154 bytes .../drawable-xhdpi/ic_dialer_fork_tt_keypad.png | Bin 0 -> 3298 bytes .../app/res/drawable-xhdpi/ic_grade_24dp.png | Bin 0 -> 479 bytes .../dialer/app/res/drawable-xhdpi/ic_handle.png | Bin 0 -> 681 bytes .../app/res/drawable-xhdpi/ic_menu_history_lt.png | Bin 0 -> 2237 bytes .../app/res/drawable-xhdpi/ic_mic_grey600.png | Bin 0 -> 454 bytes .../app/res/drawable-xhdpi/ic_more_vert_24dp.png | Bin 0 -> 158 bytes .../ic_not_interested_googblue_24dp.png | Bin 0 -> 755 bytes .../dialer/app/res/drawable-xhdpi/ic_not_spam.png | Bin 0 -> 996 bytes .../app/res/drawable-xhdpi/ic_pause_24dp.png | Bin 0 -> 90 bytes .../app/res/drawable-xhdpi/ic_people_24dp.png | Bin 0 -> 368 bytes .../app/res/drawable-xhdpi/ic_phone_24dp.png | Bin 0 -> 439 bytes .../app/res/drawable-xhdpi/ic_play_arrow_24dp.png | Bin 0 -> 220 bytes .../dialer/app/res/drawable-xhdpi/ic_remove.png | Bin 0 -> 1237 bytes .../app/res/drawable-xhdpi/ic_results_phone.png | Bin 0 -> 1376 bytes .../app/res/drawable-xhdpi/ic_schedule_24dp.png | Bin 0 -> 737 bytes .../app/res/drawable-xhdpi/ic_share_white_24dp.png | Bin 0 -> 496 bytes .../dialer/app/res/drawable-xhdpi/ic_star.png | Bin 0 -> 889 bytes .../dialer/app/res/drawable-xhdpi/ic_unblock.png | Bin 0 -> 1356 bytes .../app/res/drawable-xhdpi/ic_vm_sound_off_dis.png | Bin 0 -> 1794 bytes .../app/res/drawable-xhdpi/ic_vm_sound_off_dk.png | Bin 0 -> 1794 bytes .../app/res/drawable-xhdpi/ic_vm_sound_on_dis.png | Bin 0 -> 2354 bytes .../app/res/drawable-xhdpi/ic_vm_sound_on_dk.png | Bin 0 -> 2339 bytes .../app/res/drawable-xhdpi/ic_voicemail_24dp.png | Bin 0 -> 487 bytes .../app/res/drawable-xhdpi/ic_volume_down_24dp.png | Bin 0 -> 212 bytes .../app/res/drawable-xhdpi/ic_volume_up_24dp.png | Bin 0 -> 455 bytes .../app/res/drawable-xhdpi/search_shadow.9.png | Bin 0 -> 198 bytes .../res/drawable-xhdpi/shadow_contact_photo.png | Bin 0 -> 965 bytes .../app/res/drawable-xxhdpi/empty_call_log.png | Bin 0 -> 6226 bytes .../app/res/drawable-xxhdpi/empty_contacts.png | Bin 0 -> 3686 bytes .../app/res/drawable-xxhdpi/empty_speed_dial.png | Bin 0 -> 11039 bytes .../dialer/app/res/drawable-xxhdpi/fab_ic_dial.png | Bin 0 -> 3042 bytes .../res/drawable-xxhdpi/ic_archive_white_24dp.png | Bin 0 -> 390 bytes .../app/res/drawable-xxhdpi/ic_call_arrow.png | Bin 0 -> 1203 bytes .../res/drawable-xxhdpi/ic_content_copy_24dp.png | Bin 0 -> 266 bytes .../app/res/drawable-xxhdpi/ic_delete_24dp.png | Bin 0 -> 323 bytes .../drawable-xxhdpi/ic_dialer_fork_add_call.png | Bin 0 -> 2583 bytes .../ic_dialer_fork_current_call.png | Bin 0 -> 3622 bytes .../drawable-xxhdpi/ic_dialer_fork_tt_keypad.png | Bin 0 -> 3229 bytes .../app/res/drawable-xxhdpi/ic_grade_24dp.png | Bin 0 -> 676 bytes .../dialer/app/res/drawable-xxhdpi/ic_handle.png | Bin 0 -> 1431 bytes .../app/res/drawable-xxhdpi/ic_menu_history_lt.png | Bin 0 -> 2945 bytes .../app/res/drawable-xxhdpi/ic_mic_grey600.png | Bin 0 -> 631 bytes .../app/res/drawable-xxhdpi/ic_more_vert_24dp.png | Bin 0 -> 216 bytes .../ic_not_interested_googblue_24dp.png | Bin 0 -> 1112 bytes .../dialer/app/res/drawable-xxhdpi/ic_not_spam.png | Bin 0 -> 1340 bytes .../app/res/drawable-xxhdpi/ic_pause_24dp.png | Bin 0 -> 92 bytes .../app/res/drawable-xxhdpi/ic_people_24dp.png | Bin 0 -> 488 bytes .../app/res/drawable-xxhdpi/ic_phone_24dp.png | Bin 0 -> 619 bytes .../app/res/drawable-xxhdpi/ic_play_arrow_24dp.png | Bin 0 -> 283 bytes .../dialer/app/res/drawable-xxhdpi/ic_remove.png | Bin 0 -> 1942 bytes .../app/res/drawable-xxhdpi/ic_results_phone.png | Bin 0 -> 2090 bytes .../app/res/drawable-xxhdpi/ic_schedule_24dp.png | Bin 0 -> 1107 bytes .../res/drawable-xxhdpi/ic_share_white_24dp.png | Bin 0 -> 698 bytes .../dialer/app/res/drawable-xxhdpi/ic_star.png | Bin 0 -> 1539 bytes .../dialer/app/res/drawable-xxhdpi/ic_unblock.png | Bin 0 -> 1990 bytes .../res/drawable-xxhdpi/ic_vm_sound_off_dis.png | Bin 0 -> 2316 bytes .../app/res/drawable-xxhdpi/ic_vm_sound_off_dk.png | Bin 0 -> 2319 bytes .../app/res/drawable-xxhdpi/ic_vm_sound_on_dis.png | Bin 0 -> 2878 bytes .../app/res/drawable-xxhdpi/ic_vm_sound_on_dk.png | Bin 0 -> 2879 bytes .../app/res/drawable-xxhdpi/ic_voicemail_24dp.png | Bin 0 -> 625 bytes .../res/drawable-xxhdpi/ic_volume_down_24dp.png | Bin 0 -> 291 bytes .../app/res/drawable-xxhdpi/ic_volume_up_24dp.png | Bin 0 -> 654 bytes .../app/res/drawable-xxhdpi/search_shadow.9.png | Bin 0 -> 1148 bytes .../res/drawable-xxhdpi/shadow_contact_photo.png | Bin 0 -> 970 bytes .../app/res/drawable-xxxhdpi/empty_call_log.png | Bin 0 -> 8761 bytes .../app/res/drawable-xxxhdpi/empty_contacts.png | Bin 0 -> 5204 bytes .../app/res/drawable-xxxhdpi/fab_ic_dial.png | Bin 0 -> 3800 bytes .../res/drawable-xxxhdpi/ic_archive_white_24dp.png | Bin 0 -> 489 bytes .../app/res/drawable-xxxhdpi/ic_call_arrow.png | Bin 0 -> 1344 bytes .../res/drawable-xxxhdpi/ic_content_copy_24dp.png | Bin 0 -> 329 bytes .../app/res/drawable-xxxhdpi/ic_delete_24dp.png | Bin 0 -> 1394 bytes .../app/res/drawable-xxxhdpi/ic_grade_24dp.png | Bin 0 -> 887 bytes .../dialer/app/res/drawable-xxxhdpi/ic_handle.png | Bin 0 -> 1687 bytes .../app/res/drawable-xxxhdpi/ic_mic_grey600.png | Bin 0 -> 853 bytes .../app/res/drawable-xxxhdpi/ic_more_vert_24dp.png | Bin 0 -> 305 bytes .../ic_not_interested_googblue_24dp.png | Bin 0 -> 1458 bytes .../app/res/drawable-xxxhdpi/ic_not_spam.png | Bin 0 -> 1752 bytes .../app/res/drawable-xxxhdpi/ic_pause_24dp.png | Bin 0 -> 94 bytes .../app/res/drawable-xxxhdpi/ic_people_24dp.png | Bin 0 -> 636 bytes .../app/res/drawable-xxxhdpi/ic_phone_24dp.png | Bin 0 -> 837 bytes .../res/drawable-xxxhdpi/ic_play_arrow_24dp.png | Bin 0 -> 343 bytes .../app/res/drawable-xxxhdpi/ic_results_phone.png | Bin 0 -> 2281 bytes .../app/res/drawable-xxxhdpi/ic_schedule_24dp.png | Bin 0 -> 1478 bytes .../res/drawable-xxxhdpi/ic_share_white_24dp.png | Bin 0 -> 938 bytes .../dialer/app/res/drawable-xxxhdpi/ic_unblock.png | Bin 0 -> 1389 bytes .../app/res/drawable-xxxhdpi/ic_voicemail_24dp.png | Bin 0 -> 971 bytes .../res/drawable-xxxhdpi/ic_volume_down_24dp.png | Bin 0 -> 356 bytes .../app/res/drawable-xxxhdpi/ic_volume_up_24dp.png | Bin 0 -> 878 bytes .../app/res/drawable/background_dial_holo_dark.xml | 22 + .../app/res/drawable/floating_action_button.xml | 25 + .../res/drawable/ic_call_detail_content_copy.xml | 20 + .../app/res/drawable/ic_call_detail_edit.xml | 20 + .../app/res/drawable/ic_call_detail_report.xml | 20 + .../app/res/drawable/ic_call_detail_unblock.xml | 20 + .../android/dialer/app/res/drawable/ic_pause.xml | 31 + .../dialer/app/res/drawable/ic_play_arrow.xml | 32 + .../dialer/app/res/drawable/ic_search_phone.xml | 20 + .../app/res/drawable/ic_speakerphone_off.xml | 20 + .../dialer/app/res/drawable/ic_speakerphone_on.xml | 20 + .../app/res/drawable/ic_voicemail_seek_handle.xml | 20 + .../drawable/ic_voicemail_seek_handle_disabled.xml | 20 + .../dialer/app/res/drawable/oval_ripple.xml | 26 + .../dialer/app/res/drawable/overflow_menu.xml | 20 + .../dialer/app/res/drawable/rounded_corner.xml | 22 + .../dialer/app/res/drawable/seekbar_drawable.xml | 63 + .../drawable/selectable_primary_flat_button.xml | 31 + .../dialer/app/res/drawable/shadow_fade_left.xml | 24 + .../dialer/app/res/drawable/shadow_fade_up.xml | 24 + .../app/res/layout-land/dialpad_fragment.xml | 90 + .../empty_content_view_dialpad_search.xml | 71 + .../account_filter_header_for_phone_favorite.xml | 47 + .../app/res/layout/all_contacts_activity.xml | 26 + .../app/res/layout/all_contacts_fragment.xml | 54 + .../app/res/layout/blocked_number_footer.xml | 38 + .../app/res/layout/blocked_number_fragment.xml | 30 + .../app/res/layout/blocked_number_header.xml | 220 +++ .../dialer/app/res/layout/blocked_number_item.xml | 72 + .../app/res/layout/blocked_numbers_activity.xml | 22 + .../android/dialer/app/res/layout/call_detail.xml | 32 + .../dialer/app/res/layout/call_detail_footer.xml | 52 + .../dialer/app/res/layout/call_detail_header.xml | 89 + .../app/res/layout/call_detail_history_item.xml | 56 + .../dialer/app/res/layout/call_log_alert_item.xml | 22 + .../dialer/app/res/layout/call_log_fragment.xml | 48 + .../dialer/app/res/layout/call_log_list_item.xml | 176 ++ .../app/res/layout/call_log_list_item_actions.xml | 230 +++ .../app/res/layout/dialpad_chooser_list_item.xml | 38 + .../dialer/app/res/layout/dialpad_fragment.xml | 78 + .../dialer/app/res/layout/dialtacts_activity.xml | 73 + .../dialer/app/res/layout/empty_content_view.xml | 54 + .../layout/empty_content_view_dialpad_search.xml | 56 + .../dialer/app/res/layout/keyguard_preview.xml | 30 + .../dialer/app/res/layout/lists_fragment.xml | 98 + .../app/res/layout/phone_favorite_tile_view.xml | 128 ++ .../dialer/app/res/layout/search_edittext.xml | 71 + .../dialer/app/res/layout/speed_dial_fragment.xml | 51 + .../res/layout/view_numbers_to_import_fragment.xml | 58 + .../app/res/layout/voicemail_playback_layout.xml | 115 ++ .../dialer/app/res/menu/dialpad_options.xml | 30 + .../dialer/app/res/menu/dialtacts_options.xml | 28 + .../app/res/mipmap-hdpi/ic_launcher_phone.png | Bin 0 -> 2780 bytes .../app/res/mipmap-mdpi/ic_launcher_phone.png | Bin 0 -> 1778 bytes .../app/res/mipmap-xhdpi/ic_launcher_phone.png | Bin 0 -> 3939 bytes .../app/res/mipmap-xxhdpi/ic_launcher_phone.png | Bin 0 -> 6251 bytes .../app/res/mipmap-xxxhdpi/ic_launcher_phone.png | Bin 0 -> 8793 bytes .../dialer/app/res/values/animation_constants.xml | 30 + java/com/android/dialer/app/res/values/attrs.xml | 21 + java/com/android/dialer/app/res/values/colors.xml | 115 ++ java/com/android/dialer/app/res/values/dimens.xml | 148 ++ .../app/res/values/donottranslate_config.xml | 37 + java/com/android/dialer/app/res/values/ids.xml | 28 + java/com/android/dialer/app/res/values/strings.xml | 960 ++++++++++ java/com/android/dialer/app/res/values/styles.xml | 279 +++ .../app/res/xml/display_options_settings.xml | 31 + java/com/android/dialer/app/res/xml/file_paths.xml | 24 + java/com/android/dialer/app/res/xml/searchable.xml | 22 + .../android/dialer/app/res/xml/sound_settings.xml | 46 + .../app/settings/AppCompatPreferenceActivity.java | 155 ++ .../app/settings/DefaultRingtonePreference.java | 64 + .../app/settings/DialerSettingsActivity.java | 187 ++ .../settings/DisplayOptionsSettingsFragment.java | 30 + .../dialer/app/settings/SoundSettingsFragment.java | 242 +++ .../app/voicemail/VoicemailAudioManager.java | 252 +++ .../app/voicemail/VoicemailErrorManager.java | 129 ++ .../app/voicemail/VoicemailPlaybackLayout.java | 449 +++++ .../app/voicemail/VoicemailPlaybackPresenter.java | 1050 +++++++++++ .../dialer/app/voicemail/WiredHeadsetManager.java | 88 + .../dialer/app/voicemail/error/AndroidManifest.xml | 5 + .../error/OmtpVoicemailMessageCreator.java | 177 ++ .../app/voicemail/error/VoicemailErrorAlert.java | 165 ++ .../app/voicemail/error/VoicemailErrorMessage.java | 178 ++ .../error/VoicemailErrorMessageCreator.java | 45 + .../app/voicemail/error/VoicemailStatus.java | 260 +++ .../error/VoicemailStatusCorruptionHandler.java | 114 ++ .../app/voicemail/error/VoicemailStatusReader.java | 25 + .../app/voicemail/error/VoicemailTosMessage.java | 25 + .../error/Vvm3VoicemailMessageCreator.java | 428 +++++ .../res/layout/voicemai_error_message_fragment.xml | 114 ++ .../error/res/layout/voicemail_tos_fragment.xml | 72 + .../app/voicemail/error/res/values/dimens.xml | 12 + .../app/voicemail/error/res/values/strings.xml | 176 ++ .../app/voicemail/error/res/values/styles.xml | 26 + .../dialer/app/widget/ActionBarController.java | 247 +++ .../app/widget/DialpadSearchEmptyContentView.java | 43 + .../dialer/app/widget/EmptyContentView.java | 121 ++ .../dialer/app/widget/SearchEditTextLayout.java | 324 ++++ java/com/android/dialer/backup/AndroidManifest.xml | 27 + .../android/dialer/backup/DialerBackupAgent.java | 276 +++ .../android/dialer/backup/DialerBackupUtils.java | 320 ++++ .../android/dialer/backup/proto/VoicemailInfo.java | 377 ++++ .../android/dialer/blocking/AndroidManifest.xml | 13 + .../dialer/blocking/BlockNumberDialogFragment.java | 328 ++++ .../dialer/blocking/BlockReportSpamDialogs.java | 305 +++ .../blocking/BlockedNumbersAutoMigrator.java | 110 ++ .../dialer/blocking/BlockedNumbersMigrator.java | 159 ++ .../blocking/FilteredNumberAsyncQueryHandler.java | 428 +++++ .../dialer/blocking/FilteredNumberCompat.java | 320 ++++ .../dialer/blocking/FilteredNumberProvider.java | 176 ++ .../dialer/blocking/FilteredNumbersUtil.java | 380 ++++ .../MigrateBlockedNumbersDialogFragment.java | 113 ++ .../blocking/res/drawable-hdpi/ic_block_24dp.png | Bin 0 -> 478 bytes .../blocking/res/drawable-hdpi/ic_report_24dp.png | Bin 0 -> 240 bytes .../res/drawable-hdpi/ic_report_white_36dp.png | Bin 0 -> 312 bytes .../blocking/res/drawable-mdpi/ic_block_24dp.png | Bin 0 -> 335 bytes .../blocking/res/drawable-mdpi/ic_report_24dp.png | Bin 0 -> 174 bytes .../res/drawable-mdpi/ic_report_white_36dp.png | Bin 0 -> 240 bytes .../blocking/res/drawable-xhdpi/ic_block_24dp.png | Bin 0 -> 665 bytes .../blocking/res/drawable-xhdpi/ic_report_24dp.png | Bin 0 -> 272 bytes .../res/drawable-xhdpi/ic_report_white_36dp.png | Bin 0 -> 340 bytes .../blocking/res/drawable-xxhdpi/ic_block_24dp.png | Bin 0 -> 973 bytes .../res/drawable-xxhdpi/ic_report_24dp.png | Bin 0 -> 340 bytes .../res/drawable-xxhdpi/ic_report_white_36dp.png | Bin 0 -> 522 bytes .../res/drawable-xxxhdpi/ic_block_24dp.png | Bin 0 -> 1295 bytes .../res/drawable-xxxhdpi/ic_report_24dp.png | Bin 0 -> 450 bytes .../res/drawable-xxxhdpi/ic_report_white_36dp.png | Bin 0 -> 649 bytes .../blocking/res/drawable/blocked_contact.xml | 36 + .../res/layout/block_report_spam_dialog.xml | 36 + .../android/dialer/blocking/res/values/colors.xml | 24 + .../android/dialer/blocking/res/values/dimens.xml | 18 + .../android/dialer/blocking/res/values/strings.xml | 122 ++ java/com/android/dialer/buildtype/BuildType.java | 62 + .../dialer/buildtype/BuildTypeAccessor.java | 31 + .../buildtype/dogfood/BuildTypeAccessorImpl.java | 30 + .../dialer/callcomposer/AndroidManifest.xml | 28 + .../dialer/callcomposer/CallComposerActivity.java | 728 ++++++++ .../dialer/callcomposer/CallComposerFragment.java | 125 ++ .../callcomposer/CallComposerPagerAdapter.java | 57 + .../callcomposer/CameraComposerFragment.java | 378 ++++ .../callcomposer/GalleryComposerFragment.java | 256 +++ .../dialer/callcomposer/GalleryCursorLoader.java | 54 + .../dialer/callcomposer/GalleryGridAdapter.java | 118 ++ .../dialer/callcomposer/GalleryGridItemData.java | 91 + .../dialer/callcomposer/GalleryGridItemView.java | 126 ++ .../callcomposer/MessageComposerFragment.java | 143 ++ .../dialer/callcomposer/camera/AndroidManifest.xml | 16 + .../dialer/callcomposer/camera/CameraManager.java | 822 +++++++++ .../dialer/callcomposer/camera/CameraPreview.java | 177 ++ .../callcomposer/camera/HardwareCameraPreview.java | 125 ++ .../callcomposer/camera/ImagePersistTask.java | 143 ++ .../callcomposer/camera/SoftwareCameraPreview.java | 120 ++ .../camera/camerafocus/AndroidManifest.xml | 16 + .../camera/camerafocus/FocusIndicator.java | 28 + .../camera/camerafocus/FocusOverlayManager.java | 482 +++++ .../camera/camerafocus/OverlayRenderer.java | 97 + .../callcomposer/camera/camerafocus/PieItem.java | 179 ++ .../camera/camerafocus/PieRenderer.java | 816 +++++++++ .../camera/camerafocus/RenderOverlay.java | 153 ++ .../camera/camerafocus/res/values/dimens.xml | 26 + .../camera/exif/CountedDataInputStream.java | 129 ++ .../dialer/callcomposer/camera/exif/ExifData.java | 89 + .../callcomposer/camera/exif/ExifInterface.java | 374 ++++ .../camera/exif/ExifInvalidFormatException.java | 24 + .../callcomposer/camera/exif/ExifParser.java | 846 +++++++++ .../callcomposer/camera/exif/ExifReader.java | 81 + .../dialer/callcomposer/camera/exif/ExifTag.java | 619 +++++++ .../dialer/callcomposer/camera/exif/IfdData.java | 126 ++ .../dialer/callcomposer/camera/exif/IfdId.java | 28 + .../callcomposer/camera/exif/JpegHeader.java | 38 + .../dialer/callcomposer/camera/exif/Rational.java | 70 + .../callcomposer/cameraui/AndroidManifest.xml | 16 + .../cameraui/CameraMediaChooserView.java | 107 ++ .../cameraui/res/drawable-hdpi/ic_capture.png | Bin 0 -> 2690 bytes .../cameraui/res/drawable-mdpi/ic_capture.png | Bin 0 -> 1851 bytes .../cameraui/res/drawable-xhdpi/ic_capture.png | Bin 0 -> 3636 bytes .../cameraui/res/drawable-xxhdpi/ic_capture.png | Bin 0 -> 5449 bytes .../cameraui/res/drawable-xxxhdpi/ic_capture.png | Bin 0 -> 7354 bytes .../res/drawable/transparent_button_background.xml | 26 + .../cameraui/res/layout/camera_view.xml | 121 ++ .../callcomposer/cameraui/res/values/colors.xml | 4 + .../callcomposer/cameraui/res/values/dimens.xml | 22 + .../callcomposer/cameraui/res/values/strings.xml | 17 + .../callcomposer/nano/CallComposerContact.java | 220 +++ .../res/drawable/call_composer_contact_border.xml | 30 + .../res/drawable/gallery_background.xml | 22 + .../drawable/gallery_grid_checkbox_background.xml | 22 + .../drawable/gallery_grid_item_view_background.xml | 22 + .../drawable/gallery_item_selected_drawable.xml | 37 + .../res/layout/call_composer_activity.xml | 147 ++ .../res/layout/fragment_camera_composer.xml | 33 + .../res/layout/fragment_gallery_composer.xml | 38 + .../res/layout/fragment_message_composer.xml | 79 + .../res/layout/gallery_grid_item_view.xml | 57 + .../callcomposer/res/layout/permission_view.xml | 52 + .../dialer/callcomposer/res/values/colors.xml | 24 + .../dialer/callcomposer/res/values/dimens.xml | 63 + .../dialer/callcomposer/res/values/strings.xml | 42 + .../dialer/callcomposer/res/values/styles.xml | 50 + .../callcomposer/util/CopyAndResizeImageTask.java | 124 ++ .../dialer/callintent/CallIntentBuilder.java | 108 ++ .../dialer/callintent/CallIntentParser.java | 54 + java/com/android/dialer/callintent/Constants.java | 31 + .../dialer/callintent/nano/CallInitiationType.java | 101 + .../callintent/nano/CallSpecificAppData.java | 143 ++ java/com/android/dialer/common/AndroidManifest.xml | 3 + java/com/android/dialer/common/Assert.java | 185 ++ .../android/dialer/common/AsyncTaskExecutor.java | 51 + .../android/dialer/common/AsyncTaskExecutors.java | 91 + ...Value_FallibleAsyncTask_FallibleTaskResult.java | 79 + java/com/android/dialer/common/ConfigProvider.java | 27 + .../dialer/common/ConfigProviderBindings.java | 68 + .../dialer/common/ConfigProviderFactory.java | 26 + java/com/android/dialer/common/DpUtil.java | 31 + .../android/dialer/common/FallibleAsyncTask.java | 94 + java/com/android/dialer/common/FragmentUtils.java | 98 + java/com/android/dialer/common/LogUtil.java | 214 +++ java/com/android/dialer/common/MathUtil.java | 57 + java/com/android/dialer/common/NetworkUtil.java | 192 ++ java/com/android/dialer/common/UiUtil.java | 41 + .../android/dialer/common/res/values/strings.xml | 5 + java/com/android/dialer/compat/ActivityCompat.java | 29 + .../android/dialer/compat/AppCompatConstants.java | 33 + java/com/android/dialer/compat/CompatUtils.java | 222 +++ .../dialer/compat/PathInterpolatorCompat.java | 120 ++ .../android/dialer/compat/SdkVersionOverride.java | 43 + java/com/android/dialer/constants/Constants.java | 47 + .../android/dialer/constants/ScheduledJobIds.java | 31 + .../dialer/constants/aospdialer/ConstantsImpl.java | 37 + .../dialer/database/CallLogQueryHandler.java | 369 ++++ java/com/android/dialer/database/Database.java | 49 + .../android/dialer/database/DatabaseBindings.java | 25 + .../dialer/database/DatabaseBindingsFactory.java | 26 + .../dialer/database/DatabaseBindingsStub.java | 35 + .../dialer/database/DialerDatabaseHelper.java | 1242 +++++++++++++ .../dialer/database/FilteredNumberContract.java | 137 ++ .../dialer/database/VoicemailStatusQuery.java | 91 + java/com/android/dialer/debug/AndroidManifest.xml | 3 + .../dialer/debug/bindings/impl/DebugBindings.java | 32 + .../android/dialer/debug/impl/AndroidManifest.xml | 18 + .../android/dialer/debug/impl/DebugConnection.java | 55 + .../dialer/debug/impl/DebugConnectionService.java | 103 ++ .../android/dialer/dialpadview/AndroidManifest.xml | 3 + .../dialer/dialpadview/DialpadKeyButton.java | 231 +++ .../dialer/dialpadview/DialpadTextView.java | 71 + .../android/dialer/dialpadview/DialpadView.java | 464 +++++ .../android/dialer/dialpadview/DigitsEditText.java | 57 + .../res/anim/dialpad_slide_in_bottom.xml | 19 + .../dialpadview/res/anim/dialpad_slide_in_left.xml | 22 + .../res/anim/dialpad_slide_in_right.xml | 20 + .../res/anim/dialpad_slide_out_bottom.xml | 19 + .../res/anim/dialpad_slide_out_left.xml | 22 + .../res/anim/dialpad_slide_out_right.xml | 20 + .../dialpadview/res/drawable-hdpi/dialer_fab.png | Bin 0 -> 3273 bytes .../dialpadview/res/drawable-hdpi/fab_green.png | Bin 0 -> 2798 bytes .../dialpadview/res/drawable-hdpi/fab_ic_call.png | Bin 0 -> 875 bytes .../res/drawable-hdpi/ic_close_black_24dp.png | Bin 0 -> 207 bytes .../res/drawable-hdpi/ic_dialpad_delete.png | Bin 0 -> 805 bytes .../res/drawable-hdpi/ic_dialpad_voicemail.png | Bin 0 -> 623 bytes .../res/drawable-hdpi/ic_overflow_menu.png | Bin 0 -> 503 bytes .../dialpadview/res/drawable-mdpi/dialer_fab.png | Bin 0 -> 1945 bytes .../dialpadview/res/drawable-mdpi/fab_green.png | Bin 0 -> 1845 bytes .../dialpadview/res/drawable-mdpi/fab_ic_call.png | Bin 0 -> 698 bytes .../res/drawable-mdpi/ic_close_black_24dp.png | Bin 0 -> 164 bytes .../res/drawable-mdpi/ic_dialpad_delete.png | Bin 0 -> 669 bytes .../res/drawable-mdpi/ic_dialpad_voicemail.png | Bin 0 -> 504 bytes .../res/drawable-mdpi/ic_overflow_menu.png | Bin 0 -> 424 bytes .../dialpadview/res/drawable-xhdpi/dialer_fab.png | Bin 0 -> 4872 bytes .../dialpadview/res/drawable-xhdpi/fab_green.png | Bin 0 -> 4092 bytes .../dialpadview/res/drawable-xhdpi/fab_ic_call.png | Bin 0 -> 1266 bytes .../res/drawable-xhdpi/ic_close_black_24dp.png | Bin 0 -> 235 bytes .../res/drawable-xhdpi/ic_dialpad_delete.png | Bin 0 -> 1110 bytes .../res/drawable-xhdpi/ic_dialpad_voicemail.png | Bin 0 -> 787 bytes .../res/drawable-xhdpi/ic_overflow_menu.png | Bin 0 -> 550 bytes .../dialpadview/res/drawable-xxhdpi/dialer_fab.png | Bin 0 -> 8621 bytes .../dialpadview/res/drawable-xxhdpi/fab_green.png | Bin 0 -> 7004 bytes .../res/drawable-xxhdpi/fab_ic_call.png | Bin 0 -> 2321 bytes .../res/drawable-xxhdpi/ic_close_black_24dp.png | Bin 0 -> 309 bytes .../res/drawable-xxhdpi/ic_dialpad_delete.png | Bin 0 -> 1745 bytes .../res/drawable-xxhdpi/ic_dialpad_voicemail.png | Bin 0 -> 1578 bytes .../res/drawable-xxhdpi/ic_overflow_menu.png | Bin 0 -> 1384 bytes .../res/drawable-xxxhdpi/dialer_fab.png | Bin 0 -> 12782 bytes .../dialpadview/res/drawable-xxxhdpi/fab_green.png | Bin 0 -> 9900 bytes .../res/drawable-xxxhdpi/fab_ic_call.png | Bin 0 -> 2921 bytes .../res/drawable-xxxhdpi/ic_close_black_24dp.png | Bin 0 -> 377 bytes .../res/drawable-xxxhdpi/ic_dialpad_delete.png | Bin 0 -> 2128 bytes .../res/drawable-xxxhdpi/ic_dialpad_voicemail.png | Bin 0 -> 1829 bytes .../res/drawable-xxxhdpi/ic_overflow_menu.png | Bin 0 -> 1785 bytes .../dialpadview/res/drawable/btn_dialpad_key.xml | 18 + .../dialpadview/res/drawable/dialpad_scrim.xml | 7 + .../dialpadview/res/layout-land/dialpad_key.xml | 44 + .../res/layout-land/dialpad_key_one.xml | 44 + .../res/layout-land/dialpad_key_pound.xml | 33 + .../res/layout-land/dialpad_key_star.xml | 33 + .../res/layout-land/dialpad_key_zero.xml | 44 + .../dialer/dialpadview/res/layout/dialpad.xml | 99 + .../dialer/dialpadview/res/layout/dialpad_key.xml | 35 + .../dialpadview/res/layout/dialpad_key_one.xml | 41 + .../dialpadview/res/layout/dialpad_key_pound.xml | 26 + .../dialpadview/res/layout/dialpad_key_star.xml | 26 + .../dialpadview/res/layout/dialpad_key_zero.xml | 37 + .../dialer/dialpadview/res/layout/dialpad_view.xml | 23 + .../res/layout/dialpad_view_unthemed.xml | 153 ++ .../dialer/dialpadview/res/values-land/dimens.xml | 27 + .../dialer/dialpadview/res/values-land/styles.xml | 37 + .../dialpadview/res/values/animation_constants.xml | 20 + .../dialer/dialpadview/res/values/attrs.xml | 39 + .../dialer/dialpadview/res/values/colors.xml | 27 + .../dialer/dialpadview/res/values/dimens.xml | 48 + .../dialer/dialpadview/res/values/strings.xml | 53 + .../dialer/dialpadview/res/values/styles.xml | 118 ++ java/com/android/dialer/disabled_lint_checks.txt | 1 + .../AutoValue_EnrichedCallCapabilities.java | 76 + .../AutoValue_OutgoingCallComposerData.java | 127 ++ .../enrichedcall/EnrichedCallCapabilities.java | 36 + .../dialer/enrichedcall/EnrichedCallManager.java | 225 +++ .../enrichedcall/EnrichedCallManagerStub.java | 84 + .../enrichedcall/OutgoingCallComposerData.java | 94 + java/com/android/dialer/enrichedcall/Session.java | 63 + .../enrichedcall/StubEnrichedCallModule.java | 32 + .../enrichedcall/extensions/StateExtension.java | 54 + .../android/dialer/inject/ApplicationModule.java | 39 + .../android/dialer/inject/DialerAppComponent.java | 29 + .../dialer/interactions/AndroidManifest.xml | 20 + .../dialer/interactions/ContactUpdateService.java | 48 + .../interactions/PhoneNumberInteraction.java | 557 ++++++ .../interactions/UndemoteOutgoingCallReceiver.java | 107 ++ .../res/layout/phone_disambig_item.xml | 43 + .../res/layout/set_primary_checkbox.xml | 32 + .../dialer/interactions/res/values/strings.xml | 29 + java/com/android/dialer/logging/Logger.java | 49 + .../android/dialer/logging/LoggingBindings.java | 59 + .../dialer/logging/LoggingBindingsFactory.java | 24 + .../dialer/logging/LoggingBindingsStub.java | 36 + .../dialer/logging/nano/ContactLookupResult.java | 91 + .../android/dialer/logging/nano/ContactSource.java | 90 + .../dialer/logging/nano/DialerImpression.java | 178 ++ .../dialer/logging/nano/InteractionEvent.java | 95 + .../dialer/logging/nano/ReportingLocation.java | 87 + .../android/dialer/logging/nano/ScreenEvent.java | 104 ++ .../multimedia/AutoValue_MultimediaData.java | 165 ++ .../android/dialer/multimedia/MultimediaData.java | 100 + .../android/dialer/p13n/inference/P13nRanking.java | 75 + .../dialer/p13n/inference/protocol/P13nRanker.java | 75 + .../p13n/inference/protocol/P13nRankerFactory.java | 26 + .../android/dialer/p13n/logging/P13nLogger.java | 35 + .../dialer/p13n/logging/P13nLoggerFactory.java | 29 + .../android/dialer/p13n/logging/P13nLogging.java | 60 + .../CachedNumberLookupService.java | 77 + .../dialer/phonenumbercache/CallLogQuery.java | 107 ++ .../dialer/phonenumbercache/ContactInfo.java | 165 ++ .../dialer/phonenumbercache/ContactInfoHelper.java | 586 ++++++ .../dialer/phonenumbercache/PhoneLookupUtil.java | 40 + .../dialer/phonenumbercache/PhoneNumberCache.java | 50 + .../phonenumbercache/PhoneNumberCacheBindings.java | 26 + .../PhoneNumberCacheBindingsFactory.java | 26 + .../PhoneNumberCacheBindingsStub.java | 29 + .../dialer/phonenumbercache/PhoneQuery.java | 96 + .../dialer/phonenumberutil/AndroidManifest.xml | 3 + .../dialer/phonenumberutil/PhoneNumberHelper.java | 276 +++ .../dialer/phonenumberutil/res/values/strings.xml | 27 + .../android/dialer/proguard/UsedByReflection.java | 34 + java/com/android/dialer/protos/ProtoParsers.java | 167 ++ .../android/dialer/shortcuts/AndroidManifest.xml | 50 + .../dialer/shortcuts/AutoValue_DialerShortcut.java | 161 ++ .../dialer/shortcuts/CallContactActivity.java | 133 ++ .../android/dialer/shortcuts/DialerShortcut.java | 190 ++ .../android/dialer/shortcuts/DynamicShortcuts.java | 243 +++ java/com/android/dialer/shortcuts/IconFactory.java | 112 ++ .../dialer/shortcuts/PeriodicJobService.java | 118 ++ .../android/dialer/shortcuts/PinnedShortcuts.java | 159 ++ .../dialer/shortcuts/RefreshShortcutsTask.java | 71 + .../dialer/shortcuts/ShortcutInfoFactory.java | 100 + .../dialer/shortcuts/ShortcutRefresher.java | 86 + .../dialer/shortcuts/ShortcutUsageReporter.java | 132 ++ java/com/android/dialer/shortcuts/Shortcuts.java | 34 + .../dialer/shortcuts/ShortcutsJobScheduler.java | 48 + .../res/drawable/ic_shortcut_add_contact.xml | 39 + .../android/dialer/shortcuts/res/values/colors.xml | 20 + .../android/dialer/shortcuts/res/values/dimens.xml | 19 + .../dialer/shortcuts/res/values/strings.xml | 37 + .../android/dialer/shortcuts/res/values/themes.xml | 39 + .../android/dialer/shortcuts/res/xml/shortcuts.xml | 31 + java/com/android/dialer/simulator/Simulator.java | 27 + .../dialer/simulator/impl/AndroidManifest.xml | 18 + .../impl/AutoValue_SimulatorCallLog_CallEntry.java | 160 ++ .../impl/AutoValue_SimulatorContacts_Contact.java | 231 +++ .../AutoValue_SimulatorVoicemail_Voicemail.java | 184 ++ .../simulator/impl/SimulatorActionProvider.java | 88 + .../dialer/simulator/impl/SimulatorCallLog.java | 139 ++ .../dialer/simulator/impl/SimulatorConnection.java | 56 + .../simulator/impl/SimulatorConnectionService.java | 87 + .../dialer/simulator/impl/SimulatorContacts.java | 319 ++++ .../dialer/simulator/impl/SimulatorModule.java | 34 + .../dialer/simulator/impl/SimulatorVoiceCall.java | 47 + .../dialer/simulator/impl/SimulatorVoicemail.java | 154 ++ .../dialer/smartdial/LatinSmartDialMap.java | 784 ++++++++ .../com/android/dialer/smartdial/SmartDialMap.java | 60 + .../dialer/smartdial/SmartDialMatchPosition.java | 70 + .../dialer/smartdial/SmartDialNameMatcher.java | 434 +++++ .../android/dialer/smartdial/SmartDialPrefix.java | 605 ++++++ java/com/android/dialer/spam/Spam.java | 49 + java/com/android/dialer/spam/SpamBindings.java | 146 ++ .../android/dialer/spam/SpamBindingsFactory.java | 26 + java/com/android/dialer/spam/SpamBindingsStub.java | 92 + java/com/android/dialer/telecom/TelecomUtil.java | 212 +++ java/com/android/dialer/theme/AndroidManifest.xml | 3 + .../anim/front_back_switch_button_animation.xml | 14 + .../res/animator/activated_button_elevation.xml | 21 + .../dialer/theme/res/animator/button_elevation.xml | 21 + .../res/drawable/front_back_switch_button.xml | 75 + .../front_back_switch_button_animation.xml | 8 + .../com/android/dialer/theme/res/values/colors.xml | 64 + .../com/android/dialer/theme/res/values/dimens.xml | 28 + .../android/dialer/theme/res/values/strings.xml | 27 + .../com/android/dialer/theme/res/values/styles.xml | 56 + .../com/android/dialer/theme/res/values/themes.xml | 21 + java/com/android/dialer/util/AndroidManifest.xml | 3 + java/com/android/dialer/util/CallUtil.java | 135 ++ java/com/android/dialer/util/DialerUtils.java | 246 +++ .../com/android/dialer/util/DrawableConverter.java | 97 + java/com/android/dialer/util/ExpirableCache.java | 269 +++ java/com/android/dialer/util/IntentUtil.java | 78 + java/com/android/dialer/util/MoreStrings.java | 64 + java/com/android/dialer/util/OrientationUtil.java | 30 + java/com/android/dialer/util/PermissionsUtil.java | 121 ++ java/com/android/dialer/util/SettingsUtil.java | 95 + .../com/android/dialer/util/TouchPointManager.java | 60 + .../dialer/util/TransactionSafeActivity.java | 64 + java/com/android/dialer/util/ViewUtil.java | 129 ++ .../com/android/dialer/util/res/values/strings.xml | 42 + .../dialer/voicemailstatus/AndroidManifest.xml | 3 + .../VisualVoicemailEnabledChecker.java | 111 ++ .../voicemailstatus/VoicemailStatusHelper.java | 96 + .../voicemailstatus/VoicemailStatusHelperImpl.java | 278 +++ .../dialer/voicemailstatus/res/values/strings.xml | 41 + java/com/android/dialer/widget/AndroidManifest.xml | 3 + .../dialer/widget/ResizingTextEditText.java | 51 + .../dialer/widget/ResizingTextTextView.java | 51 + .../com/android/dialer/widget/res/values/attrs.xml | 23 + .../android/incallui/AccelerometerListener.java | 173 ++ java/com/android/incallui/AndroidManifest.xml | 121 ++ .../android/incallui/AnswerScreenPresenter.java | 110 ++ .../incallui/AnswerScreenPresenterStub.java | 44 + java/com/android/incallui/AudioModeProvider.java | 69 + java/com/android/incallui/Bindings.java | 52 + java/com/android/incallui/CallButtonPresenter.java | 515 ++++++ java/com/android/incallui/CallCardPresenter.java | 1110 +++++++++++ java/com/android/incallui/CallerInfo.java | 573 ++++++ .../com/android/incallui/CallerInfoAsyncQuery.java | 638 +++++++ java/com/android/incallui/CallerInfoUtils.java | 279 +++ .../incallui/ConferenceManagerFragment.java | 106 ++ .../incallui/ConferenceManagerPresenter.java | 139 ++ .../incallui/ConferenceParticipantListAdapter.java | 523 ++++++ java/com/android/incallui/ContactInfoCache.java | 759 ++++++++ java/com/android/incallui/ContactsAsyncHelper.java | 269 +++ .../incallui/ContactsPreferencesFactory.java | 56 + java/com/android/incallui/DialpadFragment.java | 461 +++++ java/com/android/incallui/DialpadPresenter.java | 91 + .../com/android/incallui/ExternalCallNotifier.java | 465 +++++ java/com/android/incallui/InCallActivity.java | 756 ++++++++ .../com/android/incallui/InCallActivityCommon.java | 820 +++++++++ java/com/android/incallui/InCallCameraManager.java | 173 ++ .../incallui/InCallOrientationEventListener.java | 194 ++ java/com/android/incallui/InCallPresenter.java | 1679 +++++++++++++++++ java/com/android/incallui/InCallServiceImpl.java | 99 + .../incallui/InCallUIMaterialColorMapUtils.java | 67 + java/com/android/incallui/Log.java | 145 ++ .../android/incallui/ManageConferenceActivity.java | 86 + .../incallui/NotificationBroadcastReceiver.java | 165 ++ .../android/incallui/PostCharDialogFragment.java | 96 + java/com/android/incallui/ProximitySensor.java | 292 +++ java/com/android/incallui/StatusBarNotifier.java | 842 +++++++++ java/com/android/incallui/ThemeColorManager.java | 142 ++ .../incallui/TransactionSafeFragmentActivity.java | 64 + java/com/android/incallui/VideoCallPresenter.java | 1289 +++++++++++++ .../com/android/incallui/VideoPauseController.java | 416 +++++ .../incallui/answer/bindings/AnswerBindings.java | 29 + .../answer/impl/AffordanceHolderLayout.java | 178 ++ .../incallui/answer/impl/AndroidManifest.xml | 3 + .../incallui/answer/impl/AnswerFragment.java | 981 ++++++++++ .../answer/impl/AnswerVideoCallScreen.java | 127 ++ .../answer/impl/CreateCustomSmsDialogFragment.java | 137 ++ .../android/incallui/answer/impl/PillDrawable.java | 43 + .../answer/impl/SmsBottomSheetFragment.java | 136 ++ .../answer/impl/affordance/AndroidManifest.xml | 3 + .../answer/impl/affordance/SwipeButtonHelper.java | 642 +++++++ .../answer/impl/affordance/SwipeButtonView.java | 505 +++++ .../answer/impl/affordance/res/values/dimens.xml | 23 + .../answer/impl/answermethod/AndroidManifest.xml | 3 + .../answer/impl/answermethod/AnswerMethod.java | 45 + .../impl/answermethod/AnswerMethodFactory.java | 52 + .../impl/answermethod/AnswerMethodHolder.java | 47 + .../impl/answermethod/FlingUpDownMethod.java | 1149 ++++++++++++ .../impl/answermethod/FlingUpDownTouchHandler.java | 496 +++++ .../answer/impl/answermethod/TwoButtonMethod.java | 268 +++ .../impl/answermethod/res/drawable/call_answer.xml | 19 + .../res/drawable/circular_background.xml | 6 + .../res/layout/swipe_up_down_method.xml | 115 ++ .../answermethod/res/layout/two_button_method.xml | 97 + .../impl/answermethod/res/values-h240dp/values.xml | 20 + .../impl/answermethod/res/values-h280dp/dimens.xml | 21 + .../impl/answermethod/res/values-h480dp/dimens.xml | 20 + .../answer/impl/answermethod/res/values/dimens.xml | 27 + .../answer/impl/answermethod/res/values/ids.xml | 5 + .../impl/answermethod/res/values/strings.xml | 14 + .../answer/impl/answermethod/res/values/styles.xml | 7 + .../answer/impl/answermethod/res/values/values.xml | 25 + .../impl/classifier/AccelerationClassifier.java | 99 + .../answer/impl/classifier/AnglesClassifier.java | 193 ++ .../impl/classifier/AnglesPercentageEvaluator.java | 33 + .../impl/classifier/AnglesVarianceEvaluator.java | 42 + .../answer/impl/classifier/Classifier.java | 35 + .../answer/impl/classifier/ClassifierData.java | 96 + .../impl/classifier/DirectionClassifier.java | 37 + .../answer/impl/classifier/DirectionEvaluator.java | 23 + .../impl/classifier/DurationCountClassifier.java | 35 + .../impl/classifier/DurationCountEvaluator.java | 39 + .../impl/classifier/EndPointLengthClassifier.java | 36 + .../impl/classifier/EndPointLengthEvaluator.java | 42 + .../impl/classifier/EndPointRatioClassifier.java | 43 + .../impl/classifier/EndPointRatioEvaluator.java | 42 + .../answer/impl/classifier/FalsingManager.java | 140 ++ .../answer/impl/classifier/GestureClassifier.java | 31 + .../answer/impl/classifier/HistoryEvaluator.java | 115 ++ .../classifier/HumanInteractionClassifier.java | 142 ++ .../impl/classifier/LengthCountClassifier.java | 39 + .../impl/classifier/LengthCountEvaluator.java | 45 + .../incallui/answer/impl/classifier/Point.java | 95 + .../impl/classifier/PointerCountClassifier.java | 51 + .../impl/classifier/PointerCountEvaluator.java | 23 + .../impl/classifier/ProximityClassifier.java | 97 + .../answer/impl/classifier/ProximityEvaluator.java | 28 + .../impl/classifier/SpeedAnglesClassifier.java | 147 ++ .../classifier/SpeedAnglesPercentageEvaluator.java | 33 + .../answer/impl/classifier/SpeedClassifier.java | 40 + .../answer/impl/classifier/SpeedEvaluator.java | 36 + .../impl/classifier/SpeedRatioEvaluator.java | 39 + .../impl/classifier/SpeedVarianceEvaluator.java | 36 + .../incallui/answer/impl/classifier/Stroke.java | 72 + .../answer/impl/classifier/StrokeClassifier.java | 28 + .../incallui/answer/impl/hint/AndroidManifest.xml | 13 + .../incallui/answer/impl/hint/AnswerHint.java | 46 + .../answer/impl/hint/AnswerHintFactory.java | 133 ++ .../incallui/answer/impl/hint/DotAnswerHint.java | 283 +++ .../incallui/answer/impl/hint/EmptyAnswerHint.java | 39 + .../incallui/answer/impl/hint/EventAnswerHint.java | 235 +++ .../answer/impl/hint/EventPayloadLoader.java | 30 + .../answer/impl/hint/EventPayloadLoaderImpl.java | 118 ++ .../answer/impl/hint/EventSecretCodeListener.java | 67 + .../impl/hint/res/drawable/answer_hint_large.xml | 4 + .../impl/hint/res/drawable/answer_hint_mid.xml | 4 + .../impl/hint/res/drawable/answer_hint_small.xml | 5 + .../answer/impl/hint/res/layout/dot_hint.xml | 30 + .../answer/impl/hint/res/layout/event_hint.xml | 36 + .../answer/impl/hint/res/values/dimens.xml | 12 + .../answer/impl/hint/res/values/strings.xml | 5 + .../impl/res/anim/incoming_unlocked_icon_entry.xml | 19 + .../impl/res/anim/incoming_unlocked_text_entry.xml | 9 + .../answer/impl/res/layout/fragment_avatar.xml | 26 + .../impl/res/layout/fragment_custom_sms_dialog.xml | 14 + .../impl/res/layout/fragment_incoming_call.xml | 152 ++ .../answer/impl/res/values-h240dp/dimens.xml | 21 + .../answer/impl/res/values-h300dp/dimens.xml | 20 + .../answer/impl/res/values-h480dp/dimens.xml | 22 + .../answer/impl/res/values-h540dp/dimens.xml | 21 + .../incallui/answer/impl/res/values/dimens.xml | 25 + .../incallui/answer/impl/res/values/strings.xml | 26 + .../answer/impl/utils/FlingAnimationUtils.java | 293 +++ .../incallui/answer/impl/utils/Interpolators.java | 30 + .../incallui/answer/protocol/AnswerScreen.java | 38 + .../answer/protocol/AnswerScreenDelegate.java | 44 + .../protocol/AnswerScreenDelegateFactory.java | 23 + .../AnswerProximitySensor.java | 150 ++ .../AnswerProximityWakeLock.java | 37 + .../PseudoProximityWakeLock.java | 85 + .../answerproximitysensor/PseudoScreenState.java | 66 + .../SystemProximityWakeLock.java | 90 + .../android/incallui/async/PausableExecutor.java | 56 + .../incallui/async/PausableExecutorImpl.java | 40 + .../incallui/audioroute/AndroidManifest.xml | 3 + .../AudioRouteSelectorDialogFragment.java | 114 ++ .../drawable-hdpi/ic_phone_audio_grey600_24dp.png | Bin 0 -> 990 bytes .../drawable-mdpi/ic_phone_audio_grey600_24dp.png | Bin 0 -> 632 bytes .../drawable-xhdpi/ic_phone_audio_grey600_24dp.png | Bin 0 -> 1297 bytes .../ic_phone_audio_grey600_24dp.png | Bin 0 -> 1979 bytes .../audioroute/res/layout/audioroute_selector.xml | 37 + .../incallui/audioroute/res/values/strings.xml | 7 + .../incallui/audioroute/res/values/styles.xml | 14 + .../incallui/autoresizetext/AndroidManifest.xml | 25 + .../autoresizetext/AutoResizeTextView.java | 316 ++++ .../incallui/autoresizetext/res/values/attrs.xml | 47 + java/com/android/incallui/baseui/BaseFragment.java | 75 + java/com/android/incallui/baseui/Presenter.java | 54 + java/com/android/incallui/baseui/Ui.java | 20 + .../android/incallui/bindings/ContactUtils.java | 33 + .../android/incallui/bindings/DistanceHelper.java | 36 + .../incallui/bindings/InCallUiBindings.java | 48 + .../incallui/bindings/InCallUiBindingsFactory.java | 26 + .../incallui/bindings/InCallUiBindingsStub.java | 81 + .../incallui/bindings/PhoneNumberService.java | 77 + java/com/android/incallui/call/CallList.java | 763 ++++++++ java/com/android/incallui/call/DialerCall.java | 1401 ++++++++++++++ .../android/incallui/call/DialerCallDelegate.java | 25 + .../android/incallui/call/DialerCallListener.java | 39 + .../android/incallui/call/ExternalCallList.java | 136 ++ .../incallui/call/InCallServiceListener.java | 40 + .../incallui/call/InCallUiLegacyBindings.java | 26 + .../call/InCallUiLegacyBindingsFactory.java | 26 + .../incallui/call/InCallUiLegacyBindingsStub.java | 24 + .../incallui/call/InCallVideoCallCallback.java | 197 ++ .../call/InCallVideoCallCallbackNotifier.java | 279 +++ java/com/android/incallui/call/TelecomAdapter.java | 160 ++ java/com/android/incallui/call/VideoUtils.java | 151 ++ .../incallui/commontheme/AndroidManifest.xml | 3 + .../commontheme/res/animator/button_state.xml | 30 + .../commontheme/res/animator/disabled_alpha.xml | 22 + .../commontheme/res/color/incall_button_ripple.xml | 5 + .../commontheme/res/color/incall_button_white.xml | 5 + .../drawable-hdpi/ic_phone_audio_white_36dp.png | Bin 0 -> 1010 bytes .../drawable-mdpi/ic_phone_audio_white_36dp.png | Bin 0 -> 682 bytes .../drawable-xhdpi/ic_phone_audio_white_36dp.png | Bin 0 -> 1362 bytes .../drawable-xxhdpi/ic_phone_audio_white_36dp.png | Bin 0 -> 2259 bytes .../drawable-xxxhdpi/ic_phone_audio_white_36dp.png | Bin 0 -> 3156 bytes .../res/drawable/answer_answer_background.xml | 10 + .../res/drawable/answer_decline_background.xml | 10 + .../res/drawable/incall_end_call_background.xml | 10 + .../res/values-w260dp-h520dp/dimens.xml | 21 + .../res/values-w520dp-h260dp-land/dimens.xml | 21 + .../incallui/commontheme/res/values/colors.xml | 5 + .../incallui/commontheme/res/values/dimens.xml | 22 + .../incallui/commontheme/res/values/strings.xml | 35 + .../incallui/commontheme/res/values/styles.xml | 58 + .../incallui/contactgrid/AndroidManifest.xml | 3 + .../android/incallui/contactgrid/BottomRow.java | 142 ++ .../incallui/contactgrid/ContactGridManager.java | 315 ++++ java/com/android/incallui/contactgrid/TopRow.java | 168 ++ .../res/layout/incall_contactgrid_bottom_row.xml | 71 + .../res/layout/incall_contactgrid_top_row.xml | 26 + .../incallui/contactgrid/res/values/ids.xml | 31 + .../incallui/contactgrid/res/values/strings.xml | 69 + java/com/android/incallui/hold/AndroidManifest.xml | 3 + java/com/android/incallui/hold/OnHoldFragment.java | 102 ++ .../hold/res/layout/incall_on_hold_banner.xml | 46 + .../android/incallui/hold/res/values/strings.xml | 6 + .../incallui/incall/bindings/InCallBindings.java | 28 + .../incallui/incall/impl/AndroidManifest.xml | 3 + .../AutoValue_MappedButtonConfig_MappingInfo.java | 135 ++ .../incallui/incall/impl/ButtonChooser.java | 114 ++ .../incallui/incall/impl/ButtonChooserFactory.java | 100 + .../incallui/incall/impl/ButtonController.java | 584 ++++++ .../incall/impl/CheckableLabeledButton.java | 286 +++ .../incall/impl/InCallButtonGridFragment.java | 137 ++ .../incallui/incall/impl/InCallFragment.java | 501 +++++ .../incallui/incall/impl/InCallPagerAdapter.java | 59 + .../incallui/incall/impl/MappedButtonConfig.java | 193 ++ .../impl/res/animator/incall_button_elevation.xml | 31 + .../incall/impl/res/color/incall_button_icon.xml | 5 + .../impl/res/drawable-mdpi/ic_addcall_white.png | Bin 0 -> 708 bytes .../impl/res/drawable-xhdpi/ic_addcall_white.png | Bin 0 -> 1259 bytes .../impl/res/drawable/incall_button_background.xml | 22 + .../drawable/incall_button_background_checked.xml | 5 + .../res/drawable/incall_button_background_more.xml | 30 + .../incall_button_background_unchecked.xml | 5 + .../impl/res/drawable/incall_ic_add_call.xml | 4 + .../incall/impl/res/drawable/incall_ic_dialpad.xml | 4 + .../incall/impl/res/drawable/incall_ic_manage.xml | 4 + .../incall/impl/res/drawable/incall_ic_merge.xml | 4 + .../incall/impl/res/drawable/incall_ic_pause.xml | 4 + .../impl/res/drawable/tab_indicator_default.xml | 12 + .../impl/res/drawable/tab_indicator_selected.xml | 12 + .../incall/impl/res/drawable/tab_selector.xml | 6 + .../res/layout/call_composer_data_fragment.xml | 15 + .../incall/impl/res/layout/frag_incall_voice.xml | 104 ++ .../incall/impl/res/layout/incall_button_grid.xml | 77 + .../incall/impl/res/values-h320dp/dimens.xml | 5 + .../incall/impl/res/values-h385dp/dimens.xml | 5 + .../incall/impl/res/values-h480dp/dimens.xml | 4 + .../incall/impl/res/values-h580dp/dimens.xml | 4 + .../incall/impl/res/values-h580dp/styles.xml | 24 + .../impl/res/values-w260dp-h520dp/dimens.xml | 7 + .../impl/res/values-w300dp-h540dp/dimens.xml | 5 + .../incallui/incall/impl/res/values/attrs.xml | 8 + .../incallui/incall/impl/res/values/dimens.xml | 17 + .../incallui/incall/impl/res/values/ids.xml | 6 + .../incallui/incall/impl/res/values/strings.xml | 56 + .../incallui/incall/impl/res/values/styles.xml | 23 + .../incallui/incall/protocol/ContactPhotoType.java | 35 + .../incallui/incall/protocol/InCallButtonIds.java | 59 + .../incall/protocol/InCallButtonIdsExtension.java | 61 + .../incallui/incall/protocol/InCallButtonUi.java | 50 + .../incall/protocol/InCallButtonUiDelegate.java | 67 + .../protocol/InCallButtonUiDelegateFactory.java | 23 + .../incallui/incall/protocol/InCallScreen.java | 53 + .../incall/protocol/InCallScreenDelegate.java | 43 + .../protocol/InCallScreenDelegateFactory.java | 23 + .../incallui/incall/protocol/PrimaryCallState.java | 114 ++ .../incallui/incall/protocol/PrimaryInfo.java | 112 ++ .../incallui/incall/protocol/SecondaryInfo.java | 109 ++ .../incallui/latencyreport/LatencyReport.java | 140 ++ .../BlockedNumberContentObserver.java | 105 ++ .../legacyblocking/DeleteBlockedCallTask.java | 124 ++ .../android/incallui/maps/StaticMapBinding.java | 51 + .../android/incallui/maps/StaticMapFactory.java | 28 + .../incallui/res/anim/activity_open_enter.xml | 43 + .../incallui/res/anim/activity_open_exit.xml | 31 + .../android/incallui/res/anim/decelerate_cubic.xml | 21 + .../android/incallui/res/anim/decelerate_quint.xml | 21 + .../android/incallui/res/anim/on_going_call.xml | 31 + .../android/incallui/res/color/ota_title_color.xml | 21 + .../res/drawable-hdpi/ic_block_grey600_24dp.png | Bin 0 -> 518 bytes .../res/drawable-hdpi/ic_call_end_white_24dp.png | Bin 0 -> 454 bytes .../res/drawable-hdpi/ic_call_split_white_24dp.png | Bin 0 -> 326 bytes .../res/drawable-hdpi/ic_close_grey600_24dp.png | Bin 0 -> 225 bytes .../drawable-hdpi/ic_location_on_white_24dp.png | Bin 0 -> 371 bytes .../res/drawable-hdpi/ic_ongoing_phone_24px_01.png | Bin 0 -> 577 bytes .../res/drawable-hdpi/ic_ongoing_phone_24px_02.png | Bin 0 -> 650 bytes .../res/drawable-hdpi/ic_ongoing_phone_24px_03.png | Bin 0 -> 803 bytes .../res/drawable-hdpi/ic_ongoing_phone_24px_04.png | Bin 0 -> 1009 bytes .../res/drawable-hdpi/ic_ongoing_phone_24px_05.png | Bin 0 -> 946 bytes .../res/drawable-hdpi/ic_ongoing_phone_24px_06.png | Bin 0 -> 856 bytes .../res/drawable-hdpi/ic_ongoing_phone_24px_07.png | Bin 0 -> 577 bytes .../res/drawable-hdpi/ic_ongoing_phone_24px_08.png | Bin 0 -> 577 bytes .../res/drawable-hdpi/ic_ongoing_phone_24px_09.png | Bin 0 -> 577 bytes .../drawable-hdpi/ic_person_add_grey600_24dp.png | Bin 0 -> 300 bytes .../drawable-hdpi/ic_phone_paused_white_24dp.png | Bin 0 -> 458 bytes .../res/drawable-hdpi/ic_question_mark.png | Bin 0 -> 845 bytes .../res/drawable-hdpi/ic_schedule_white_24dp.png | Bin 0 -> 575 bytes .../incallui/res/drawable-hdpi/img_business.png | Bin 0 -> 3311 bytes .../incallui/res/drawable-hdpi/img_conference.png | Bin 0 -> 7037 bytes .../incallui/res/drawable-hdpi/img_no_image.png | Bin 0 -> 5362 bytes .../incallui/res/drawable-hdpi/img_phone.png | Bin 0 -> 6157 bytes .../res/drawable-mdpi/ic_block_grey600_24dp.png | Bin 0 -> 348 bytes .../res/drawable-mdpi/ic_call_end_white_24dp.png | Bin 0 -> 315 bytes .../res/drawable-mdpi/ic_call_split_white_24dp.png | Bin 0 -> 256 bytes .../res/drawable-mdpi/ic_close_grey600_24dp.png | Bin 0 -> 178 bytes .../drawable-mdpi/ic_location_on_white_24dp.png | Bin 0 -> 265 bytes .../res/drawable-mdpi/ic_ongoing_phone_24px_01.png | Bin 0 -> 375 bytes .../res/drawable-mdpi/ic_ongoing_phone_24px_02.png | Bin 0 -> 401 bytes .../res/drawable-mdpi/ic_ongoing_phone_24px_03.png | Bin 0 -> 501 bytes .../res/drawable-mdpi/ic_ongoing_phone_24px_04.png | Bin 0 -> 638 bytes .../res/drawable-mdpi/ic_ongoing_phone_24px_05.png | Bin 0 -> 572 bytes .../res/drawable-mdpi/ic_ongoing_phone_24px_06.png | Bin 0 -> 548 bytes .../res/drawable-mdpi/ic_ongoing_phone_24px_07.png | Bin 0 -> 375 bytes .../res/drawable-mdpi/ic_ongoing_phone_24px_08.png | Bin 0 -> 375 bytes .../res/drawable-mdpi/ic_ongoing_phone_24px_09.png | Bin 0 -> 375 bytes .../drawable-mdpi/ic_person_add_grey600_24dp.png | Bin 0 -> 211 bytes .../drawable-mdpi/ic_phone_paused_white_24dp.png | Bin 0 -> 346 bytes .../res/drawable-mdpi/ic_question_mark.png | Bin 0 -> 569 bytes .../res/drawable-mdpi/ic_schedule_white_24dp.png | Bin 0 -> 377 bytes .../incallui/res/drawable-mdpi/img_business.png | Bin 0 -> 2240 bytes .../incallui/res/drawable-mdpi/img_conference.png | Bin 0 -> 4629 bytes .../incallui/res/drawable-mdpi/img_no_image.png | Bin 0 -> 3509 bytes .../incallui/res/drawable-mdpi/img_phone.png | Bin 0 -> 3798 bytes .../res/drawable-xhdpi/ic_block_grey600_24dp.png | Bin 0 -> 690 bytes .../res/drawable-xhdpi/ic_call_end_white_24dp.png | Bin 0 -> 534 bytes .../drawable-xhdpi/ic_call_split_white_24dp.png | Bin 0 -> 377 bytes .../res/drawable-xhdpi/ic_close_grey600_24dp.png | Bin 0 -> 261 bytes .../drawable-xhdpi/ic_location_on_white_24dp.png | Bin 0 -> 456 bytes .../drawable-xhdpi/ic_ongoing_phone_24px_01.png | Bin 0 -> 730 bytes .../drawable-xhdpi/ic_ongoing_phone_24px_02.png | Bin 0 -> 806 bytes .../drawable-xhdpi/ic_ongoing_phone_24px_03.png | Bin 0 -> 1017 bytes .../drawable-xhdpi/ic_ongoing_phone_24px_04.png | Bin 0 -> 1313 bytes .../drawable-xhdpi/ic_ongoing_phone_24px_05.png | Bin 0 -> 1218 bytes .../drawable-xhdpi/ic_ongoing_phone_24px_06.png | Bin 0 -> 1098 bytes .../drawable-xhdpi/ic_ongoing_phone_24px_07.png | Bin 0 -> 730 bytes .../drawable-xhdpi/ic_ongoing_phone_24px_08.png | Bin 0 -> 730 bytes .../drawable-xhdpi/ic_ongoing_phone_24px_09.png | Bin 0 -> 730 bytes .../drawable-xhdpi/ic_person_add_grey600_24dp.png | Bin 0 -> 341 bytes .../drawable-xhdpi/ic_phone_paused_white_24dp.png | Bin 0 -> 584 bytes .../res/drawable-xhdpi/ic_question_mark.png | Bin 0 -> 1094 bytes .../res/drawable-xhdpi/ic_schedule_white_24dp.png | Bin 0 -> 737 bytes .../incallui/res/drawable-xhdpi/img_business.png | Bin 0 -> 4759 bytes .../incallui/res/drawable-xhdpi/img_conference.png | Bin 0 -> 9517 bytes .../incallui/res/drawable-xhdpi/img_no_image.png | Bin 0 -> 7369 bytes .../incallui/res/drawable-xhdpi/img_phone.png | Bin 0 -> 8189 bytes .../res/drawable-xxhdpi/ic_block_grey600_24dp.png | Bin 0 -> 1029 bytes .../res/drawable-xxhdpi/ic_call_end_white_24dp.png | Bin 0 -> 736 bytes .../drawable-xxhdpi/ic_call_split_white_24dp.png | Bin 0 -> 461 bytes .../res/drawable-xxhdpi/ic_close_grey600_24dp.png | Bin 0 -> 353 bytes .../drawable-xxhdpi/ic_location_on_white_24dp.png | Bin 0 -> 675 bytes .../drawable-xxhdpi/ic_ongoing_phone_24px_01.png | Bin 0 -> 1051 bytes .../drawable-xxhdpi/ic_ongoing_phone_24px_02.png | Bin 0 -> 1198 bytes .../drawable-xxhdpi/ic_ongoing_phone_24px_03.png | Bin 0 -> 1524 bytes .../drawable-xxhdpi/ic_ongoing_phone_24px_04.png | Bin 0 -> 2045 bytes .../drawable-xxhdpi/ic_ongoing_phone_24px_05.png | Bin 0 -> 1900 bytes .../drawable-xxhdpi/ic_ongoing_phone_24px_06.png | Bin 0 -> 1675 bytes .../drawable-xxhdpi/ic_ongoing_phone_24px_07.png | Bin 0 -> 1051 bytes .../drawable-xxhdpi/ic_ongoing_phone_24px_08.png | Bin 0 -> 1051 bytes .../drawable-xxhdpi/ic_ongoing_phone_24px_09.png | Bin 0 -> 1051 bytes .../drawable-xxhdpi/ic_person_add_grey600_24dp.png | Bin 0 -> 485 bytes .../drawable-xxhdpi/ic_phone_paused_white_24dp.png | Bin 0 -> 842 bytes .../res/drawable-xxhdpi/ic_question_mark.png | Bin 0 -> 1686 bytes .../res/drawable-xxhdpi/ic_schedule_white_24dp.png | Bin 0 -> 1107 bytes .../incallui/res/drawable-xxhdpi/img_business.png | Bin 0 -> 6499 bytes .../res/drawable-xxhdpi/img_conference.png | Bin 0 -> 16306 bytes .../incallui/res/drawable-xxhdpi/img_no_image.png | Bin 0 -> 9850 bytes .../incallui/res/drawable-xxhdpi/img_phone.png | Bin 0 -> 10848 bytes .../res/drawable-xxxhdpi/ic_block_grey600_24dp.png | Bin 0 -> 1353 bytes .../drawable-xxxhdpi/ic_call_end_white_24dp.png | Bin 0 -> 929 bytes .../drawable-xxxhdpi/ic_call_split_white_24dp.png | Bin 0 -> 646 bytes .../res/drawable-xxxhdpi/ic_close_grey600_24dp.png | Bin 0 -> 444 bytes .../drawable-xxxhdpi/ic_location_on_white_24dp.png | Bin 0 -> 869 bytes .../ic_person_add_grey600_24dp.png | Bin 0 -> 638 bytes .../res/drawable-xxxhdpi/ic_question_mark.png | Bin 0 -> 2304 bytes .../drawable-xxxhdpi/ic_schedule_white_24dp.png | Bin 0 -> 1478 bytes .../incallui/res/drawable-xxxhdpi/img_business.png | Bin 0 -> 10730 bytes .../res/drawable-xxxhdpi/img_conference.png | Bin 0 -> 19584 bytes .../incallui/res/drawable-xxxhdpi/img_no_image.png | Bin 0 -> 16251 bytes .../incallui/res/drawable-xxxhdpi/img_phone.png | Bin 0 -> 18635 bytes .../res/drawable/img_conference_automirrored.xml | 21 + .../res/drawable/img_no_image_automirrored.xml | 21 + .../res/drawable/incall_background_gradient.xml | 8 + .../res/drawable/spam_notification_icon.xml | 34 + .../res/drawable/unknown_notification_icon.xml | 34 + .../res/layout/activity_manage_conference.xml | 6 + .../incallui/res/layout/caller_in_conference.xml | 119 ++ .../res/layout/conference_manager_fragment.xml | 33 + .../res/layout/incall_dialpad_fragment.xml | 24 + .../android/incallui/res/layout/incall_screen.xml | 33 + .../res/layout/video_call_lte_to_wifi_failed.xml | 28 + .../android/incallui/res/values-sw360dp/dimens.xml | 32 + .../incallui/res/values-w500dp-land/colors.xml | 21 + .../incallui/res/values-w500dp-land/dimens.xml | 23 + .../incallui/res/values/animation_constants.xml | 19 + java/com/android/incallui/res/values/colors.xml | 92 + java/com/android/incallui/res/values/config.xml | 23 + java/com/android/incallui/res/values/dimens.xml | 66 + java/com/android/incallui/res/values/strings.xml | 367 ++++ java/com/android/incallui/res/values/styles.xml | 80 + .../incallui/ringtone/DialerRingtoneManager.java | 134 ++ .../incallui/ringtone/InCallTonePlayer.java | 168 ++ .../incallui/ringtone/ToneGeneratorFactory.java | 34 + .../incallui/sessiondata/AndroidManifest.xml | 18 + .../incallui/sessiondata/AvatarPresenter.java | 31 + .../incallui/sessiondata/MultimediaFragment.java | 231 +++ .../res/drawable/answer_data_background.xml | 22 + .../res/layout/fragment_composer_frag.xml | 42 + .../res/layout/fragment_composer_image.xml | 50 + .../res/layout/fragment_composer_image_frag.xml | 59 + .../res/layout/fragment_composer_text.xml | 43 + .../res/layout/fragment_composer_text_frag.xml | 61 + .../res/layout/fragment_composer_text_image.xml | 62 + .../layout/fragment_composer_text_image_frag.xml | 78 + .../incallui/sessiondata/res/values/dimens.xml | 21 + .../incallui/sessiondata/res/values/ids.xml | 23 + .../incallui/sessiondata/res/values/styles.xml | 24 + .../incallui/spam/NumberInCallHistoryTask.java | 107 ++ .../incallui/spam/SpamCallListListener.java | 364 ++++ .../incallui/spam/SpamNotificationActivity.java | 483 +++++ .../incallui/spam/SpamNotificationService.java | 132 ++ .../android/incallui/util/AccessibilityUtil.java | 35 + .../com/android/incallui/util/TelecomCallUtil.java | 51 + .../incallui/video/bindings/VideoBindings.java | 28 + .../incallui/video/impl/AndroidManifest.xml | 3 + .../video/impl/CameraPermissionDialogFragment.java | 62 + .../incallui/video/impl/CheckableImageButton.java | 222 +++ .../video/impl/SpeakerButtonController.java | 118 ++ .../video/impl/SwitchOnHoldCallController.java | 91 + .../incallui/video/impl/VideoCallFragment.java | 1215 ++++++++++++ .../impl/res/color/videocall_button_icon_tint.xml | 5 + .../impl/res/drawable-hdpi/ic_switch_camera.png | Bin 0 -> 1930 bytes .../res/drawable-hdpi/video_button_bg_checked.png | Bin 0 -> 3103 bytes .../video_button_bg_checked_disabled.png | Bin 0 -> 3304 bytes .../video_button_bg_checked_pressed.png | Bin 0 -> 4836 bytes .../res/drawable-hdpi/video_button_bg_default.png | Bin 0 -> 4209 bytes .../res/drawable-hdpi/video_button_bg_disabled.png | Bin 0 -> 4022 bytes .../res/drawable-hdpi/video_button_bg_pressed.png | Bin 0 -> 5695 bytes .../impl/res/drawable-mdpi/ic_switch_camera.png | Bin 0 -> 1293 bytes .../res/drawable-mdpi/video_button_bg_checked.png | Bin 0 -> 1426 bytes .../video_button_bg_checked_disabled.png | Bin 0 -> 1715 bytes .../video_button_bg_checked_pressed.png | Bin 0 -> 2724 bytes .../res/drawable-mdpi/video_button_bg_default.png | Bin 0 -> 2155 bytes .../res/drawable-mdpi/video_button_bg_disabled.png | Bin 0 -> 1990 bytes .../res/drawable-mdpi/video_button_bg_pressed.png | Bin 0 -> 3188 bytes .../impl/res/drawable-xhdpi/ic_switch_camera.png | Bin 0 -> 2518 bytes .../res/drawable-xhdpi/video_button_bg_checked.png | Bin 0 -> 4603 bytes .../video_button_bg_checked_disabled.png | Bin 0 -> 4957 bytes .../video_button_bg_checked_pressed.png | Bin 0 -> 7213 bytes .../res/drawable-xhdpi/video_button_bg_default.png | Bin 0 -> 6352 bytes .../drawable-xhdpi/video_button_bg_disabled.png | Bin 0 -> 6054 bytes .../res/drawable-xhdpi/video_button_bg_pressed.png | Bin 0 -> 8418 bytes .../impl/res/drawable-xxhdpi/ic_switch_camera.png | Bin 0 -> 4001 bytes .../drawable-xxhdpi/video_button_bg_checked.png | Bin 0 -> 9032 bytes .../video_button_bg_checked_disabled.png | Bin 0 -> 8611 bytes .../video_button_bg_checked_pressed.png | Bin 0 -> 13529 bytes .../drawable-xxhdpi/video_button_bg_default.png | Bin 0 -> 11101 bytes .../drawable-xxhdpi/video_button_bg_disabled.png | Bin 0 -> 10736 bytes .../drawable-xxhdpi/video_button_bg_pressed.png | Bin 0 -> 15167 bytes .../impl/res/drawable-xxxhdpi/ic_switch_camera.png | Bin 0 -> 2424 bytes .../drawable/videocall_background_circle_white.xml | 10 + .../drawable/videocall_video_button_background.xml | 27 + .../impl/res/layout-v21/switch_camera_button.xml | 6 + .../video/impl/res/layout/frag_videocall.xml | 114 ++ .../video/impl/res/layout/frag_videocall_land.xml | 111 ++ .../video/impl/res/layout/switch_camera_button.xml | 6 + .../video/impl/res/layout/video_contact_grid.xml | 33 + .../video/impl/res/layout/videocall_controls.xml | 113 ++ .../impl/res/layout/videocall_controls_land.xml | 115 ++ .../video/impl/res/values-h580dp/dimens.xml | 7 + .../video/impl/res/values-w460dp/dimens.xml | 7 + .../incallui/video/impl/res/values/attrs.xml | 8 + .../incallui/video/impl/res/values/dimens.xml | 10 + .../incallui/video/impl/res/values/strings.xml | 28 + .../incallui/video/impl/res/values/styles.xml | 11 + .../incallui/video/protocol/VideoCallScreen.java | 36 + .../video/protocol/VideoCallScreenDelegate.java | 48 + .../protocol/VideoCallScreenDelegateFactory.java | 23 + .../bindings/VideoSurfaceBindings.java | 44 + .../incallui/videosurface/impl/VideoScale.java | 147 ++ .../videosurface/impl/VideoSurfaceTextureImpl.java | 249 +++ .../protocol/VideoSurfaceDelegate.java | 29 + .../videosurface/protocol/VideoSurfaceTexture.java | 57 + java/com/android/incallui/wifi/AndroidManifest.xml | 3 + .../incallui/wifi/EnableWifiCallingPrompt.java | 82 + .../android/incallui/wifi/res/values/strings.xml | 9 + java/com/android/voicemailomtp/ActivationTask.java | 305 +++ java/com/android/voicemailomtp/AndroidManifest.xml | 105 ++ java/com/android/voicemailomtp/Assert.java | 62 + .../voicemailomtp/DefaultOmtpEventHandler.java | 202 ++ .../android/voicemailomtp/NeededForTesting.java | 25 + java/com/android/voicemailomtp/OmtpConstants.java | 248 +++ java/com/android/voicemailomtp/OmtpEvents.java | 156 ++ java/com/android/voicemailomtp/OmtpService.java | 65 + .../voicemailomtp/OmtpVvmCarrierConfigHelper.java | 423 +++++ .../voicemailomtp/SubscriptionInfoHelper.java | 75 + .../voicemailomtp/TelephonyManagerStub.java | 80 + .../voicemailomtp/TelephonyVvmConfigManager.java | 154 ++ .../voicemailomtp/VisualVoicemailPreferences.java | 143 ++ java/com/android/voicemailomtp/Voicemail.java | 330 ++++ .../com/android/voicemailomtp/VoicemailStatus.java | 158 ++ java/com/android/voicemailomtp/VvmLog.java | 179 ++ .../voicemailomtp/VvmPackageInstallReceiver.java | 70 + .../voicemailomtp/VvmPhoneStateListener.java | 103 ++ .../fetch/FetchVoicemailReceiver.java | 219 +++ .../fetch/VoicemailFetchedCallback.java | 101 + .../com/android/voicemailomtp/imap/ImapHelper.java | 711 +++++++ .../voicemailomtp/imap/VoicemailPayload.java | 38 + java/com/android/voicemailomtp/mail/Address.java | 541 ++++++ .../mail/AuthenticationFailedException.java | 33 + .../com/android/voicemailomtp/mail/Base64Body.java | 62 + java/com/android/voicemailomtp/mail/Body.java | 25 + java/com/android/voicemailomtp/mail/BodyPart.java | 24 + .../mail/CertificateValidationException.java | 29 + .../android/voicemailomtp/mail/FetchProfile.java | 84 + java/com/android/voicemailomtp/mail/Fetchable.java | 23 + .../voicemailomtp/mail/FixedLengthInputStream.java | 79 + java/com/android/voicemailomtp/mail/Flag.java | 29 + .../android/voicemailomtp/mail/MailTransport.java | 344 ++++ .../android/voicemailomtp/mail/MeetingInfo.java | 29 + java/com/android/voicemailomtp/mail/Message.java | 144 ++ .../voicemailomtp/mail/MessageDateComparator.java | 34 + .../voicemailomtp/mail/MessagingException.java | 139 ++ java/com/android/voicemailomtp/mail/Multipart.java | 62 + .../android/voicemailomtp/mail/PackedString.java | 175 ++ java/com/android/voicemailomtp/mail/Part.java | 51 + .../voicemailomtp/mail/PeekableInputStream.java | 80 + .../android/voicemailomtp/mail/TempDirectory.java | 41 + .../mail/internet/BinaryTempFileBody.java | 91 + .../voicemailomtp/mail/internet/MimeBodyPart.java | 207 +++ .../voicemailomtp/mail/internet/MimeHeader.java | 161 ++ .../voicemailomtp/mail/internet/MimeMessage.java | 675 +++++++ .../voicemailomtp/mail/internet/MimeMultipart.java | 112 ++ .../voicemailomtp/mail/internet/MimeUtility.java | 416 +++++ .../voicemailomtp/mail/internet/TextBody.java | 63 + .../voicemailomtp/mail/store/ImapConnection.java | 413 +++++ .../voicemailomtp/mail/store/ImapFolder.java | 784 ++++++++ .../voicemailomtp/mail/store/ImapStore.java | 176 ++ .../mail/store/imap/DigestMd5Utils.java | 335 ++++ .../mail/store/imap/ImapConstants.java | 144 ++ .../voicemailomtp/mail/store/imap/ImapElement.java | 120 ++ .../voicemailomtp/mail/store/imap/ImapList.java | 235 +++ .../mail/store/imap/ImapMemoryLiteral.java | 76 + .../mail/store/imap/ImapResponse.java | 158 ++ .../mail/store/imap/ImapResponseParser.java | 432 +++++ .../mail/store/imap/ImapSimpleString.java | 62 + .../voicemailomtp/mail/store/imap/ImapString.java | 192 ++ .../mail/store/imap/ImapTempFileLiteral.java | 123 ++ .../voicemailomtp/mail/store/imap/ImapUtility.java | 125 ++ .../mail/utility/CountingOutputStream.java | 48 + .../mail/utility/EOLConvertingOutputStream.java | 48 + .../android/voicemailomtp/mail/utils/LogUtils.java | 413 +++++ .../android/voicemailomtp/mail/utils/Utility.java | 80 + java/com/android/voicemailomtp/permissions.xml | 21 + .../voicemailomtp/protocol/CvvmProtocol.java | 59 + .../voicemailomtp/protocol/OmtpProtocol.java | 37 + .../voicemailomtp/protocol/ProtocolHelper.java | 43 + .../protocol/VisualVoicemailProtocol.java | 100 + .../protocol/VisualVoicemailProtocolFactory.java | 47 + .../voicemailomtp/protocol/Vvm3EventHandler.java | 271 +++ .../voicemailomtp/protocol/Vvm3Protocol.java | 301 +++ .../voicemailomtp/protocol/Vvm3Subscriber.java | 326 ++++ .../res/layout/voicemail_change_pin.xml | 97 + .../android/voicemailomtp/res/values/arrays.xml | 19 + .../com/android/voicemailomtp/res/values/attrs.xml | 20 + .../android/voicemailomtp/res/values/colors.xml | 19 + .../android/voicemailomtp/res/values/config.xml | 19 + .../android/voicemailomtp/res/values/dimens.xml | 19 + java/com/android/voicemailomtp/res/values/ids.xml | 20 + .../android/voicemailomtp/res/values/strings.xml | 86 + .../android/voicemailomtp/res/values/styles.xml | 19 + .../voicemailomtp/res/xml/voicemail_settings.xml | 27 + .../android/voicemailomtp/res/xml/vvm_config.xml | 134 ++ .../android/voicemailomtp/scheduling/BaseTask.java | 206 +++ .../voicemailomtp/scheduling/BlockerTask.java | 55 + .../scheduling/MinimalIntervalPolicy.java | 69 + .../android/voicemailomtp/scheduling/Policy.java | 36 + .../voicemailomtp/scheduling/PostponePolicy.java | 69 + .../voicemailomtp/scheduling/RetryPolicy.java | 117 ++ .../com/android/voicemailomtp/scheduling/Task.java | 133 ++ .../scheduling/TaskSchedulerService.java | 392 ++++ .../settings/VisualVoicemailSettingsUtil.java | 77 + .../settings/VoicemailChangePinActivity.java | 634 +++++++ .../settings/VoicemailSettingsActivity.java | 222 +++ .../voicemailomtp/sms/LegacyModeSmsHandler.java | 67 + .../voicemailomtp/sms/OmtpCvvmMessageSender.java | 55 + .../voicemailomtp/sms/OmtpMessageReceiver.java | 162 ++ .../voicemailomtp/sms/OmtpMessageSender.java | 89 + .../sms/OmtpStandardMessageSender.java | 119 ++ .../android/voicemailomtp/sms/StatusMessage.java | 209 +++ .../voicemailomtp/sms/StatusSmsFetcher.java | 162 ++ .../com/android/voicemailomtp/sms/SyncMessage.java | 166 ++ .../voicemailomtp/sms/Vvm3MessageSender.java | 56 + .../src/org/apache/commons/io/IOUtils.java | 1202 ++++++++++++ .../org/apache/james/mime4j/BodyDescriptor.java | 392 ++++ .../james/mime4j/CloseShieldInputStream.java | 129 ++ .../org/apache/james/mime4j/ContentHandler.java | 177 ++ .../james/mime4j/EOLConvertingInputStream.java | 139 ++ .../src/org/apache/james/mime4j/Log.java | 114 ++ .../src/org/apache/james/mime4j/LogFactory.java | 29 + .../james/mime4j/MimeBoundaryInputStream.java | 184 ++ .../org/apache/james/mime4j/MimeStreamParser.java | 324 ++++ .../org/apache/james/mime4j/RootInputStream.java | 111 ++ .../org/apache/james/mime4j/codec/EncoderUtil.java | 630 +++++++ .../james/mime4j/decoder/Base64InputStream.java | 151 ++ .../org/apache/james/mime4j/decoder/ByteQueue.java | 62 + .../apache/james/mime4j/decoder/DecoderUtil.java | 284 +++ .../mime4j/decoder/QuotedPrintableInputStream.java | 229 +++ .../mime4j/decoder/UnboundedFifoByteBuffer.java | 272 +++ .../james/mime4j/field/AddressListField.java | 65 + .../mime4j/field/ContentTransferEncodingField.java | 88 + .../james/mime4j/field/ContentTypeField.java | 259 +++ .../apache/james/mime4j/field/DateTimeField.java | 73 + .../james/mime4j/field/DefaultFieldParser.java | 45 + .../james/mime4j/field/DelegatingFieldParser.java | 47 + .../src/org/apache/james/mime4j/field/Field.java | 192 ++ .../org/apache/james/mime4j/field/FieldParser.java | 21 + .../apache/james/mime4j/field/MailboxField.java | 70 + .../james/mime4j/field/MailboxListField.java | 67 + .../james/mime4j/field/UnstructuredField.java | 49 + .../apache/james/mime4j/field/address/Address.java | 52 + .../james/mime4j/field/address/AddressList.java | 138 ++ .../apache/james/mime4j/field/address/Builder.java | 243 +++ .../james/mime4j/field/address/DomainList.java | 76 + .../apache/james/mime4j/field/address/Group.java | 75 + .../apache/james/mime4j/field/address/Mailbox.java | 121 ++ .../james/mime4j/field/address/MailboxList.java | 71 + .../james/mime4j/field/address/NamedMailbox.java | 71 + .../mime4j/field/address/parser/ASTaddr_spec.java | 19 + .../mime4j/field/address/parser/ASTaddress.java | 19 + .../field/address/parser/ASTaddress_list.java | 19 + .../mime4j/field/address/parser/ASTangle_addr.java | 19 + .../mime4j/field/address/parser/ASTdomain.java | 19 + .../mime4j/field/address/parser/ASTgroup_body.java | 19 + .../mime4j/field/address/parser/ASTlocal_part.java | 19 + .../mime4j/field/address/parser/ASTmailbox.java | 19 + .../mime4j/field/address/parser/ASTname_addr.java | 19 + .../mime4j/field/address/parser/ASTphrase.java | 19 + .../mime4j/field/address/parser/ASTroute.java | 19 + .../field/address/parser/AddressListParser.java | 977 ++++++++++ .../field/address/parser/AddressListParser.jj | 595 ++++++ .../address/parser/AddressListParserConstants.java | 76 + .../parser/AddressListParserTokenManager.java | 1009 ++++++++++ .../parser/AddressListParserTreeConstants.java | 35 + .../address/parser/AddressListParserVisitor.java | 19 + .../mime4j/field/address/parser/BaseNode.java | 30 + .../address/parser/JJTAddressListParserState.java | 123 ++ .../james/mime4j/field/address/parser/Node.java | 37 + .../field/address/parser/ParseException.java | 207 +++ .../field/address/parser/SimpleCharStream.java | 454 +++++ .../mime4j/field/address/parser/SimpleNode.java | 87 + .../james/mime4j/field/address/parser/Token.java | 96 + .../mime4j/field/address/parser/TokenMgrError.java | 148 ++ .../contenttype/parser/ContentTypeParser.java | 268 +++ .../parser/ContentTypeParserConstants.java | 62 + .../parser/ContentTypeParserTokenManager.java | 877 +++++++++ .../field/contenttype/parser/ParseException.java | 207 +++ .../field/contenttype/parser/SimpleCharStream.java | 454 +++++ .../mime4j/field/contenttype/parser/Token.java | 96 + .../field/contenttype/parser/TokenMgrError.java | 148 ++ .../james/mime4j/field/datetime/DateTime.java | 127 ++ .../field/datetime/parser/DateTimeParser.java | 570 ++++++ .../datetime/parser/DateTimeParserConstants.java | 86 + .../parser/DateTimeParserTokenManager.java | 882 +++++++++ .../field/datetime/parser/ParseException.java | 207 +++ .../field/datetime/parser/SimpleCharStream.java | 454 +++++ .../james/mime4j/field/datetime/parser/Token.java | 96 + .../field/datetime/parser/TokenMgrError.java | 148 ++ .../org/apache/james/mime4j/util/CharsetUtil.java | 1249 +++++++++++++ .../voicemailomtp/sync/OmtpVvmSourceManager.java | 120 ++ .../voicemailomtp/sync/OmtpVvmSyncReceiver.java | 61 + .../voicemailomtp/sync/OmtpVvmSyncService.java | 278 +++ .../android/voicemailomtp/sync/SyncOneTask.java | 82 + java/com/android/voicemailomtp/sync/SyncTask.java | 79 + .../com/android/voicemailomtp/sync/UploadTask.java | 68 + .../sync/VoicemailProviderChangeReceiver.java | 41 + .../sync/VoicemailStatusQueryHelper.java | 113 ++ .../voicemailomtp/sync/VoicemailsQueryHelper.java | 244 +++ .../voicemailomtp/sync/VvmNetworkRequest.java | 118 ++ .../sync/VvmNetworkRequestCallback.java | 171 ++ .../voicemailomtp/utils/IndentingPrintWriter.java | 160 ++ .../voicemailomtp/utils/VoicemailDatabaseUtil.java | 90 + .../voicemailomtp/utils/VvmDumpHandler.java | 46 + java/com/android/voicemailomtp/utils/XmlUtils.java | 245 +++ proguard.flags | 79 +- res/color/settings_text_color_primary.xml | 23 - res/color/settings_text_color_secondary.xml | 23 - res/color/tab_text_color.xml | 21 - res/drawable-hdpi/empty_call_log.png | Bin 3538 -> 0 bytes res/drawable-hdpi/empty_contacts.png | Bin 2461 -> 0 bytes res/drawable-hdpi/empty_speed_dial.png | Bin 6041 -> 0 bytes res/drawable-hdpi/fab_blue.png | Bin 2805 -> 0 bytes res/drawable-hdpi/fab_ic_call.png | Bin 875 -> 0 bytes res/drawable-hdpi/fab_ic_dial.png | Bin 1028 -> 0 bytes res/drawable-hdpi/ic_archive_white_24dp.png | Bin 247 -> 0 bytes res/drawable-hdpi/ic_block_24dp.png | Bin 478 -> 0 bytes res/drawable-hdpi/ic_call_arrow.png | Bin 538 -> 0 bytes res/drawable-hdpi/ic_content_copy_24dp.png | Bin 203 -> 0 bytes res/drawable-hdpi/ic_delete_24dp.png | Bin 242 -> 0 bytes res/drawable-hdpi/ic_dialer_fork_add_call.png | Bin 1649 -> 0 bytes res/drawable-hdpi/ic_dialer_fork_current_call.png | Bin 2305 -> 0 bytes res/drawable-hdpi/ic_dialer_fork_tt_keypad.png | Bin 2419 -> 0 bytes res/drawable-hdpi/ic_grade_24dp.png | Bin 370 -> 0 bytes res/drawable-hdpi/ic_handle.png | Bin 543 -> 0 bytes res/drawable-hdpi/ic_menu_history_lt.png | Bin 1565 -> 0 bytes res/drawable-hdpi/ic_mic_grey600.png | Bin 377 -> 0 bytes res/drawable-hdpi/ic_more_vert_24dp.png | Bin 134 -> 0 bytes .../ic_not_interested_googblue_24dp.png | Bin 565 -> 0 bytes res/drawable-hdpi/ic_pause_24dp.png | Bin 105 -> 0 bytes res/drawable-hdpi/ic_people_24dp.png | Bin 299 -> 0 bytes res/drawable-hdpi/ic_phone_24dp.png | Bin 347 -> 0 bytes res/drawable-hdpi/ic_play_arrow_24dp.png | Bin 195 -> 0 bytes res/drawable-hdpi/ic_remove.png | Bin 884 -> 0 bytes res/drawable-hdpi/ic_report_24dp.png | Bin 240 -> 0 bytes res/drawable-hdpi/ic_results_phone.png | Bin 1084 -> 0 bytes res/drawable-hdpi/ic_schedule_24dp.png | Bin 575 -> 0 bytes res/drawable-hdpi/ic_share_white_24dp.png | Bin 397 -> 0 bytes res/drawable-hdpi/ic_star.png | Bin 732 -> 0 bytes res/drawable-hdpi/ic_unblock.png | Bin 1049 -> 0 bytes res/drawable-hdpi/ic_videocam_24dp.png | Bin 267 -> 0 bytes res/drawable-hdpi/ic_vm_sound_off_dis.png | Bin 1339 -> 0 bytes res/drawable-hdpi/ic_vm_sound_off_dk.png | Bin 1337 -> 0 bytes res/drawable-hdpi/ic_vm_sound_on_dis.png | Bin 1755 -> 0 bytes res/drawable-hdpi/ic_vm_sound_on_dk.png | Bin 1750 -> 0 bytes res/drawable-hdpi/ic_voicemail_24dp.png | Bin 478 -> 0 bytes res/drawable-hdpi/ic_volume_down_24dp.png | Bin 186 -> 0 bytes res/drawable-hdpi/ic_volume_up_24dp.png | Bin 365 -> 0 bytes res/drawable-hdpi/search_shadow.9.png | Bin 183 -> 0 bytes res/drawable-hdpi/shadow_contact_photo.png | Bin 960 -> 0 bytes res/drawable-mdpi/empty_call_log.png | Bin 2463 -> 0 bytes res/drawable-mdpi/empty_contacts.png | Bin 1778 -> 0 bytes res/drawable-mdpi/empty_speed_dial.png | Bin 4119 -> 0 bytes res/drawable-mdpi/fab_blue.png | Bin 1841 -> 0 bytes res/drawable-mdpi/fab_ic_call.png | Bin 698 -> 0 bytes res/drawable-mdpi/fab_ic_dial.png | Bin 905 -> 0 bytes res/drawable-mdpi/ic_archive_white_24dp.png | Bin 181 -> 0 bytes res/drawable-mdpi/ic_block_24dp.png | Bin 335 -> 0 bytes res/drawable-mdpi/ic_call_arrow.png | Bin 455 -> 0 bytes res/drawable-mdpi/ic_content_copy_24dp.png | Bin 134 -> 0 bytes res/drawable-mdpi/ic_delete_24dp.png | Bin 195 -> 0 bytes res/drawable-mdpi/ic_dialer_fork_add_call.png | Bin 1309 -> 0 bytes res/drawable-mdpi/ic_dialer_fork_current_call.png | Bin 1581 -> 0 bytes res/drawable-mdpi/ic_dialer_fork_tt_keypad.png | Bin 1586 -> 0 bytes res/drawable-mdpi/ic_grade_24dp.png | Bin 271 -> 0 bytes res/drawable-mdpi/ic_handle.png | Bin 454 -> 0 bytes res/drawable-mdpi/ic_menu_history_lt.png | Bin 1086 -> 0 bytes res/drawable-mdpi/ic_mic_grey600.png | Bin 252 -> 0 bytes res/drawable-mdpi/ic_more_vert_24dp.png | Bin 112 -> 0 bytes .../ic_not_interested_googblue_24dp.png | Bin 377 -> 0 bytes res/drawable-mdpi/ic_pause_24dp.png | Bin 83 -> 0 bytes res/drawable-mdpi/ic_people_24dp.png | Bin 210 -> 0 bytes res/drawable-mdpi/ic_phone_24dp.png | Bin 262 -> 0 bytes res/drawable-mdpi/ic_play_arrow_24dp.png | Bin 157 -> 0 bytes res/drawable-mdpi/ic_remove.png | Bin 728 -> 0 bytes res/drawable-mdpi/ic_report_24dp.png | Bin 174 -> 0 bytes res/drawable-mdpi/ic_results_phone.png | Bin 801 -> 0 bytes res/drawable-mdpi/ic_schedule_24dp.png | Bin 377 -> 0 bytes res/drawable-mdpi/ic_share_white_24dp.png | Bin 268 -> 0 bytes res/drawable-mdpi/ic_star.png | Bin 531 -> 0 bytes res/drawable-mdpi/ic_unblock.png | Bin 746 -> 0 bytes res/drawable-mdpi/ic_videocam_24dp.png | Bin 215 -> 0 bytes res/drawable-mdpi/ic_vm_sound_off_dis.png | Bin 948 -> 0 bytes res/drawable-mdpi/ic_vm_sound_off_dk.png | Bin 945 -> 0 bytes res/drawable-mdpi/ic_vm_sound_on_dis.png | Bin 1166 -> 0 bytes res/drawable-mdpi/ic_vm_sound_on_dk.png | Bin 1192 -> 0 bytes res/drawable-mdpi/ic_voicemail_24dp.png | Bin 221 -> 0 bytes res/drawable-mdpi/ic_volume_down_24dp.png | Bin 139 -> 0 bytes res/drawable-mdpi/ic_volume_up_24dp.png | Bin 251 -> 0 bytes res/drawable-mdpi/search_shadow.9.png | Bin 159 -> 0 bytes res/drawable-mdpi/shadow_contact_photo.png | Bin 948 -> 0 bytes res/drawable-xhdpi/empty_call_log.png | Bin 4860 -> 0 bytes res/drawable-xhdpi/empty_contacts.png | Bin 3352 -> 0 bytes res/drawable-xhdpi/empty_speed_dial.png | Bin 8689 -> 0 bytes res/drawable-xhdpi/fab_blue.png | Bin 4085 -> 0 bytes res/drawable-xhdpi/fab_ic_call.png | Bin 1266 -> 0 bytes res/drawable-xhdpi/fab_ic_dial.png | Bin 1699 -> 0 bytes res/drawable-xhdpi/ic_archive_white_24dp.png | Bin 267 -> 0 bytes res/drawable-xhdpi/ic_block_24dp.png | Bin 665 -> 0 bytes res/drawable-xhdpi/ic_call_arrow.png | Bin 627 -> 0 bytes res/drawable-xhdpi/ic_content_copy_24dp.png | Bin 188 -> 0 bytes res/drawable-xhdpi/ic_delete_24dp.png | Bin 271 -> 0 bytes res/drawable-xhdpi/ic_dialer_fork_add_call.png | Bin 2150 -> 0 bytes res/drawable-xhdpi/ic_dialer_fork_current_call.png | Bin 3154 -> 0 bytes res/drawable-xhdpi/ic_dialer_fork_tt_keypad.png | Bin 3298 -> 0 bytes res/drawable-xhdpi/ic_grade_24dp.png | Bin 479 -> 0 bytes res/drawable-xhdpi/ic_handle.png | Bin 681 -> 0 bytes res/drawable-xhdpi/ic_menu_history_lt.png | Bin 2237 -> 0 bytes res/drawable-xhdpi/ic_mic_grey600.png | Bin 454 -> 0 bytes res/drawable-xhdpi/ic_more_vert_24dp.png | Bin 158 -> 0 bytes .../ic_not_interested_googblue_24dp.png | Bin 755 -> 0 bytes res/drawable-xhdpi/ic_pause_24dp.png | Bin 90 -> 0 bytes res/drawable-xhdpi/ic_people_24dp.png | Bin 368 -> 0 bytes res/drawable-xhdpi/ic_phone_24dp.png | Bin 439 -> 0 bytes res/drawable-xhdpi/ic_play_arrow_24dp.png | Bin 220 -> 0 bytes res/drawable-xhdpi/ic_remove.png | Bin 1237 -> 0 bytes res/drawable-xhdpi/ic_report_24dp.png | Bin 272 -> 0 bytes res/drawable-xhdpi/ic_results_phone.png | Bin 1376 -> 0 bytes res/drawable-xhdpi/ic_schedule_24dp.png | Bin 737 -> 0 bytes res/drawable-xhdpi/ic_share_white_24dp.png | Bin 496 -> 0 bytes res/drawable-xhdpi/ic_star.png | Bin 889 -> 0 bytes res/drawable-xhdpi/ic_unblock.png | Bin 1356 -> 0 bytes res/drawable-xhdpi/ic_videocam_24dp.png | Bin 257 -> 0 bytes res/drawable-xhdpi/ic_vm_sound_off_dis.png | Bin 1794 -> 0 bytes res/drawable-xhdpi/ic_vm_sound_off_dk.png | Bin 1794 -> 0 bytes res/drawable-xhdpi/ic_vm_sound_on_dis.png | Bin 2354 -> 0 bytes res/drawable-xhdpi/ic_vm_sound_on_dk.png | Bin 2339 -> 0 bytes res/drawable-xhdpi/ic_voicemail_24dp.png | Bin 487 -> 0 bytes res/drawable-xhdpi/ic_volume_down_24dp.png | Bin 212 -> 0 bytes res/drawable-xhdpi/ic_volume_up_24dp.png | Bin 455 -> 0 bytes res/drawable-xhdpi/search_shadow.9.png | Bin 198 -> 0 bytes res/drawable-xhdpi/shadow_contact_photo.png | Bin 965 -> 0 bytes res/drawable-xxhdpi/empty_call_log.png | Bin 6226 -> 0 bytes res/drawable-xxhdpi/empty_contacts.png | Bin 3686 -> 0 bytes res/drawable-xxhdpi/empty_speed_dial.png | Bin 11039 -> 0 bytes res/drawable-xxhdpi/fab_blue.png | Bin 7009 -> 0 bytes res/drawable-xxhdpi/fab_ic_call.png | Bin 2321 -> 0 bytes res/drawable-xxhdpi/fab_ic_dial.png | Bin 3042 -> 0 bytes res/drawable-xxhdpi/ic_archive_white_24dp.png | Bin 390 -> 0 bytes res/drawable-xxhdpi/ic_block_24dp.png | Bin 973 -> 0 bytes res/drawable-xxhdpi/ic_call_arrow.png | Bin 1203 -> 0 bytes res/drawable-xxhdpi/ic_content_copy_24dp.png | Bin 266 -> 0 bytes res/drawable-xxhdpi/ic_delete_24dp.png | Bin 323 -> 0 bytes res/drawable-xxhdpi/ic_dialer_fork_add_call.png | Bin 2583 -> 0 bytes .../ic_dialer_fork_current_call.png | Bin 3622 -> 0 bytes res/drawable-xxhdpi/ic_dialer_fork_tt_keypad.png | Bin 3229 -> 0 bytes res/drawable-xxhdpi/ic_grade_24dp.png | Bin 676 -> 0 bytes res/drawable-xxhdpi/ic_handle.png | Bin 1431 -> 0 bytes res/drawable-xxhdpi/ic_menu_history_lt.png | Bin 2945 -> 0 bytes res/drawable-xxhdpi/ic_mic_grey600.png | Bin 631 -> 0 bytes res/drawable-xxhdpi/ic_more_vert_24dp.png | Bin 216 -> 0 bytes .../ic_not_interested_googblue_24dp.png | Bin 1112 -> 0 bytes res/drawable-xxhdpi/ic_pause_24dp.png | Bin 92 -> 0 bytes res/drawable-xxhdpi/ic_people_24dp.png | Bin 488 -> 0 bytes res/drawable-xxhdpi/ic_phone_24dp.png | Bin 619 -> 0 bytes res/drawable-xxhdpi/ic_play_arrow_24dp.png | Bin 283 -> 0 bytes res/drawable-xxhdpi/ic_remove.png | Bin 1942 -> 0 bytes res/drawable-xxhdpi/ic_report_24dp.png | Bin 340 -> 0 bytes res/drawable-xxhdpi/ic_results_phone.png | Bin 2090 -> 0 bytes res/drawable-xxhdpi/ic_schedule_24dp.png | Bin 1107 -> 0 bytes res/drawable-xxhdpi/ic_share_white_24dp.png | Bin 698 -> 0 bytes res/drawable-xxhdpi/ic_star.png | Bin 1539 -> 0 bytes res/drawable-xxhdpi/ic_unblock.png | Bin 1990 -> 0 bytes res/drawable-xxhdpi/ic_videocam_24dp.png | Bin 340 -> 0 bytes res/drawable-xxhdpi/ic_vm_sound_off_dis.png | Bin 2316 -> 0 bytes res/drawable-xxhdpi/ic_vm_sound_off_dk.png | Bin 2319 -> 0 bytes res/drawable-xxhdpi/ic_vm_sound_on_dis.png | Bin 2878 -> 0 bytes res/drawable-xxhdpi/ic_vm_sound_on_dk.png | Bin 2879 -> 0 bytes res/drawable-xxhdpi/ic_voicemail_24dp.png | Bin 625 -> 0 bytes res/drawable-xxhdpi/ic_volume_down_24dp.png | Bin 291 -> 0 bytes res/drawable-xxhdpi/ic_volume_up_24dp.png | Bin 654 -> 0 bytes res/drawable-xxhdpi/search_shadow.9.png | Bin 1148 -> 0 bytes res/drawable-xxhdpi/shadow_contact_photo.png | Bin 970 -> 0 bytes res/drawable-xxxhdpi/empty_call_log.png | Bin 8761 -> 0 bytes res/drawable-xxxhdpi/empty_contacts.png | Bin 5204 -> 0 bytes res/drawable-xxxhdpi/fab_blue.png | Bin 9807 -> 0 bytes res/drawable-xxxhdpi/fab_ic_call.png | Bin 2921 -> 0 bytes res/drawable-xxxhdpi/fab_ic_dial.png | Bin 3800 -> 0 bytes res/drawable-xxxhdpi/ic_archive_white_24dp.png | Bin 489 -> 0 bytes res/drawable-xxxhdpi/ic_block_24dp.png | Bin 1295 -> 0 bytes res/drawable-xxxhdpi/ic_call_arrow.png | Bin 1344 -> 0 bytes res/drawable-xxxhdpi/ic_content_copy_24dp.png | Bin 329 -> 0 bytes res/drawable-xxxhdpi/ic_delete_24dp.png | Bin 1394 -> 0 bytes res/drawable-xxxhdpi/ic_grade_24dp.png | Bin 887 -> 0 bytes res/drawable-xxxhdpi/ic_handle.png | Bin 1687 -> 0 bytes res/drawable-xxxhdpi/ic_mic_grey600.png | Bin 853 -> 0 bytes res/drawable-xxxhdpi/ic_more_vert_24dp.png | Bin 305 -> 0 bytes .../ic_not_interested_googblue_24dp.png | Bin 1458 -> 0 bytes res/drawable-xxxhdpi/ic_pause_24dp.png | Bin 94 -> 0 bytes res/drawable-xxxhdpi/ic_people_24dp.png | Bin 636 -> 0 bytes res/drawable-xxxhdpi/ic_phone_24dp.png | Bin 837 -> 0 bytes res/drawable-xxxhdpi/ic_play_arrow_24dp.png | Bin 343 -> 0 bytes res/drawable-xxxhdpi/ic_report_24dp.png | Bin 450 -> 0 bytes res/drawable-xxxhdpi/ic_results_phone.png | Bin 2281 -> 0 bytes res/drawable-xxxhdpi/ic_schedule_24dp.png | Bin 1478 -> 0 bytes res/drawable-xxxhdpi/ic_share_white_24dp.png | Bin 938 -> 0 bytes res/drawable-xxxhdpi/ic_unblock.png | Bin 1389 -> 0 bytes res/drawable-xxxhdpi/ic_videocam_24dp.png | Bin 290 -> 0 bytes res/drawable-xxxhdpi/ic_voicemail_24dp.png | Bin 971 -> 0 bytes res/drawable-xxxhdpi/ic_volume_down_24dp.png | Bin 356 -> 0 bytes res/drawable-xxxhdpi/ic_volume_up_24dp.png | Bin 878 -> 0 bytes res/drawable/background_dial_holo_dark.xml | 22 - res/drawable/blocked_contact.xml | 33 - res/drawable/floating_action_button.xml | 25 - res/drawable/ic_call_detail_block.xml | 20 - res/drawable/ic_call_detail_content_copy.xml | 20 - res/drawable/ic_call_detail_edit.xml | 20 - res/drawable/ic_call_detail_report.xml | 20 - res/drawable/ic_call_detail_unblock.xml | 20 - res/drawable/ic_pause.xml | 31 - res/drawable/ic_play_arrow.xml | 32 - res/drawable/ic_search_phone.xml | 20 - res/drawable/ic_speakerphone_off.xml | 20 - res/drawable/ic_speakerphone_on.xml | 20 - res/drawable/ic_voicemail_seek_handle.xml | 20 - res/drawable/ic_voicemail_seek_handle_disabled.xml | 20 - res/drawable/oval_ripple.xml | 26 - res/drawable/overflow_menu.xml | 20 - res/drawable/rounded_corner.xml | 22 - res/drawable/seekbar_drawable.xml | 63 - res/drawable/selectable_primary_flat_button.xml | 27 - res/drawable/shadow_fade_left.xml | 24 - res/drawable/shadow_fade_up.xml | 24 - res/layout-land/dialpad_fragment.xml | 87 - .../account_filter_header_for_phone_favorite.xml | 47 - res/layout/all_contacts_activity.xml | 25 - res/layout/all_contacts_fragment.xml | 54 - res/layout/block_report_spam_dialog.xml | 35 - res/layout/blocked_number_footer.xml | 37 - res/layout/blocked_number_fragment.xml | 29 - res/layout/blocked_number_header.xml | 217 --- res/layout/blocked_number_item.xml | 72 - res/layout/blocked_numbers_activity.xml | 22 - res/layout/call_detail.xml | 32 - res/layout/call_detail_footer.xml | 56 - res/layout/call_detail_header.xml | 89 - res/layout/call_detail_history_item.xml | 56 - res/layout/call_log_activity.xml | 40 - res/layout/call_log_fragment.xml | 39 - res/layout/call_log_list_item.xml | 174 -- res/layout/call_log_list_item_actions.xml | 202 -- res/layout/dialpad_chooser_list_item.xml | 36 - res/layout/dialpad_fragment.xml | 76 - res/layout/dialtacts_activity.xml | 73 - res/layout/empty_content_view.xml | 54 - res/layout/keyguard_preview.xml | 30 - res/layout/lists_fragment.xml | 98 - res/layout/phone_disambig_item.xml | 43 - res/layout/phone_favorite_tile_view.xml | 128 -- res/layout/search_edittext.xml | 71 - res/layout/set_primary_checkbox.xml | 32 - res/layout/speed_dial_fragment.xml | 51 - res/layout/view_numbers_to_import_fragment.xml | 56 - res/layout/voicemail_playback_layout.xml | 138 -- res/layout/voicemail_promo_card.xml | 99 - res/menu/call_log_options.xml | 22 - res/menu/dialpad_options.xml | 30 - res/menu/dialtacts_options.xml | 39 - res/mipmap-hdpi/ic_launcher_phone.png | Bin 2780 -> 0 bytes res/mipmap-mdpi/ic_launcher_phone.png | Bin 1778 -> 0 bytes res/mipmap-xhdpi/ic_launcher_phone.png | Bin 3939 -> 0 bytes res/mipmap-xxhdpi/ic_launcher_phone.png | Bin 6251 -> 0 bytes res/mipmap-xxxhdpi/ic_launcher_phone.png | Bin 8793 -> 0 bytes res/values-af/strings.xml | 288 --- res/values-am/strings.xml | 288 --- res/values-ar/strings.xml | 296 --- res/values-az/strings.xml | 288 --- res/values-b+sr+Latn/strings.xml | 290 --- res/values-be/strings.xml | 292 --- res/values-bg/strings.xml | 288 --- res/values-bn/strings.xml | 288 --- res/values-bs/strings.xml | 290 --- res/values-ca/strings.xml | 288 --- res/values-cs/strings.xml | 292 --- res/values-da/strings.xml | 288 --- res/values-de/strings.xml | 288 --- res/values-el/strings.xml | 288 --- res/values-en-rAU/strings.xml | 288 --- res/values-en-rGB/strings.xml | 288 --- res/values-en-rIN/strings.xml | 288 --- res/values-es-rUS/strings.xml | 288 --- res/values-es/strings.xml | 288 --- res/values-et/strings.xml | 288 --- res/values-eu/strings.xml | 288 --- res/values-fa/strings.xml | 288 --- res/values-fi/strings.xml | 288 --- res/values-fr-rCA/strings.xml | 288 --- res/values-fr/strings.xml | 288 --- res/values-gl/strings.xml | 288 --- res/values-gu/strings.xml | 288 --- res/values-hi/strings.xml | 288 --- res/values-hr/strings.xml | 290 --- res/values-hu/strings.xml | 288 --- res/values-hy/strings.xml | 288 --- res/values-in/strings.xml | 288 --- res/values-is/strings.xml | 288 --- res/values-it/strings.xml | 288 --- res/values-iw/strings.xml | 292 --- res/values-ja/strings.xml | 288 --- res/values-ka/strings.xml | 288 --- res/values-kk/strings.xml | 288 --- res/values-km/strings.xml | 288 --- res/values-kn/strings.xml | 289 --- res/values-ko/strings.xml | 288 --- res/values-ky/strings.xml | 288 --- res/values-lo/strings.xml | 288 --- res/values-lt/strings.xml | 292 --- res/values-lv/strings.xml | 290 --- res/values-mk/strings.xml | 288 --- res/values-ml/strings.xml | 288 --- res/values-mn/strings.xml | 288 --- res/values-mr/strings.xml | 288 --- res/values-ms/strings.xml | 288 --- res/values-my/strings.xml | 288 --- res/values-nb/strings.xml | 288 --- res/values-ne/strings.xml | 288 --- res/values-nl/strings.xml | 288 --- res/values-pa/strings.xml | 288 --- res/values-pl/strings.xml | 292 --- res/values-pt-rBR/strings.xml | 288 --- res/values-pt-rPT/strings.xml | 288 --- res/values-pt/strings.xml | 288 --- res/values-ro/strings.xml | 290 --- res/values-ru/strings.xml | 292 --- res/values-si/strings.xml | 288 --- res/values-sk/strings.xml | 292 --- res/values-sl/strings.xml | 292 --- res/values-sq/strings.xml | 288 --- res/values-sr/strings.xml | 290 --- res/values-sv/strings.xml | 288 --- res/values-sw/strings.xml | 288 --- res/values-ta/strings.xml | 288 --- res/values-te/strings.xml | 288 --- res/values-th/strings.xml | 288 --- res/values-tl/strings.xml | 288 --- res/values-tr/strings.xml | 288 --- res/values-uk/strings.xml | 292 --- res/values-ur/strings.xml | 288 --- res/values-uz/strings.xml | 288 --- res/values-vi/strings.xml | 288 --- res/values-zh-rCN/strings.xml | 288 --- res/values-zh-rHK/strings.xml | 288 --- res/values-zh-rTW/strings.xml | 288 --- res/values-zu/strings.xml | 288 --- res/values/animation_constants.xml | 30 - res/values/attrs.xml | 36 - res/values/colors.xml | 142 -- res/values/dimens.xml | 176 -- res/values/donottranslate_config.xml | 39 - res/values/ids.xml | 25 - res/values/strings.xml | 1116 ----------- res/values/styles.xml | 346 ---- res/xml/display_options_settings.xml | 31 - res/xml/file_paths.xml | 22 - res/xml/searchable.xml | 22 - res/xml/sound_settings.xml | 46 - src-N/com/android/dialer/SdkSelectionUtils.java | 35 - .../dialer/compat/BlockedNumbersSdkCompat.java | 37 - .../com/android/dialer/compat/CallsSdkCompat.java | 25 - .../dialer/compat/UserManagerSdkCompat.java | 39 - .../com/android/dialer/SdkSelectionUtils.java | 35 - .../dialer/compat/BlockedNumbersSdkCompat.java | 35 - .../com/android/dialer/compat/CallsSdkCompat.java | 25 - .../dialer/compat/UserManagerSdkCompat.java | 34 - src/com/android/dialer/CallDetailActivity.java | 507 ----- src/com/android/dialer/DialerApplication.java | 58 - src/com/android/dialer/DialerBackupAgent.java | 38 - src/com/android/dialer/DialtactsActivity.java | 1413 -------------- .../dialer/FloatingActionButtonBehavior.java | 47 - src/com/android/dialer/NeededForReflection.java | 30 - src/com/android/dialer/PhoneCallDetails.java | 187 -- src/com/android/dialer/SpecialCharSequenceMgr.java | 495 ----- .../android/dialer/TransactionSafeActivity.java | 65 - .../dialer/calllog/BlockReportSpamListener.java | 124 -- .../dialer/calllog/CallDetailHistoryAdapter.java | 166 -- .../android/dialer/calllog/CallLogActivity.java | 235 --- src/com/android/dialer/calllog/CallLogAdapter.java | 959 ---------- .../dialer/calllog/CallLogAsyncTaskUtil.java | 505 ----- .../android/dialer/calllog/CallLogFragment.java | 530 ------ .../dialer/calllog/CallLogGroupBuilder.java | 300 --- .../dialer/calllog/CallLogListItemHelper.java | 268 --- .../dialer/calllog/CallLogListItemViewHolder.java | 776 -------- .../dialer/calllog/CallLogNotificationsHelper.java | 353 ---- .../calllog/CallLogNotificationsService.java | 194 -- src/com/android/dialer/calllog/CallLogQuery.java | 115 -- .../dialer/calllog/CallLogQueryHandler.java | 354 ---- .../android/dialer/calllog/CallLogReceiver.java | 44 - src/com/android/dialer/calllog/CallTypeHelper.java | 134 -- .../android/dialer/calllog/CallTypeIconsView.java | 227 --- .../android/dialer/calllog/ClearCallLogDialog.java | 98 - src/com/android/dialer/calllog/ContactInfo.java | 108 -- .../android/dialer/calllog/ContactInfoHelper.java | 499 ----- .../dialer/calllog/DefaultVoicemailNotifier.java | 269 --- .../dialer/calllog/GroupingListAdapter.java | 171 -- src/com/android/dialer/calllog/IntentProvider.java | 206 --- .../calllog/MissedCallNotificationReceiver.java | 53 - .../android/dialer/calllog/MissedCallNotifier.java | 292 --- .../android/dialer/calllog/PhoneAccountUtils.java | 117 -- .../dialer/calllog/PhoneCallDetailsHelper.java | 359 ---- .../dialer/calllog/PhoneCallDetailsViews.java | 73 - .../dialer/calllog/PhoneNumberDisplayUtil.java | 83 - src/com/android/dialer/calllog/PhoneQuery.java | 96 - .../dialer/calllog/PromoCardViewHolder.java | 83 - .../calllog/VisualVoicemailCallLogFragment.java | 87 - .../dialer/calllog/VoicemailQueryHandler.java | 70 - .../dialer/calllog/calllogcache/CallLogCache.java | 96 - .../calllog/calllogcache/CallLogCacheLollipop.java | 73 - .../calllogcache/CallLogCacheLollipopMr1.java | 110 -- .../android/dialer/compat/DialerCompatUtils.java | 31 - .../dialer/compat/FilteredNumberCompat.java | 393 ---- src/com/android/dialer/compat/SettingsCompat.java | 47 - .../android/dialer/compat/UserManagerCompat.java | 71 - .../dialer/contact/ContactUpdateService.java | 51 - .../dialer/contactinfo/ContactInfoCache.java | 333 ---- .../dialer/contactinfo/ContactInfoRequest.java | 65 - .../dialer/contactinfo/ContactPhotoLoader.java | 120 -- .../dialer/contactinfo/NumberWithCountryIso.java | 53 - .../dialer/database/DialerDatabaseHelper.java | 1169 ------------ .../database/FilteredNumberAsyncQueryHandler.java | 273 --- .../dialer/database/FilteredNumberContract.java | 163 -- .../dialer/database/FilteredNumberProvider.java | 211 --- .../dialer/database/VoicemailArchiveContract.java | 203 -- .../dialer/database/VoicemailArchiveProvider.java | 218 --- .../android/dialer/dialpad/DialpadFragment.java | 1695 ----------------- .../android/dialer/dialpad/LatinSmartDialMap.java | 413 ----- .../dialer/dialpad/PseudoEmergencyAnimator.java | 160 -- .../dialer/dialpad/SmartDialCursorLoader.java | 193 -- src/com/android/dialer/dialpad/SmartDialMap.java | 43 - .../dialer/dialpad/SmartDialMatchPosition.java | 70 - .../dialer/dialpad/SmartDialNameMatcher.java | 439 ----- .../android/dialer/dialpad/SmartDialPrefix.java | 608 ------ .../dialer/dialpad/UnicodeDialerKeyListener.java | 54 - .../filterednumber/BlockNumberDialogFragment.java | 320 ---- .../filterednumber/BlockedNumbersAdapter.java | 96 - .../filterednumber/BlockedNumbersAutoMigrator.java | 101 - .../filterednumber/BlockedNumbersFragment.java | 264 --- .../filterednumber/BlockedNumbersMigrator.java | 135 -- .../BlockedNumbersSettingsActivity.java | 162 -- .../dialer/filterednumber/FilteredNumbersUtil.java | 369 ---- .../MigrateBlockedNumbersDialogFragment.java | 110 -- .../dialer/filterednumber/NumbersAdapter.java | 137 -- .../filterednumber/ViewNumbersToImportAdapter.java | 57 - .../ViewNumbersToImportFragment.java | 133 -- .../interactions/PhoneNumberInteraction.java | 516 ------ .../interactions/UndemoteOutgoingCallReceiver.java | 109 -- .../android/dialer/list/AllContactsFragment.java | 198 -- .../dialer/list/BlockedListSearchAdapter.java | 90 - .../dialer/list/BlockedListSearchFragment.java | 244 --- .../android/dialer/list/ContentChangedFilter.java | 40 - .../dialer/list/DialerPhoneNumberListAdapter.java | 220 --- .../android/dialer/list/DragDropController.java | 95 - src/com/android/dialer/list/ListsFragment.java | 487 ----- .../android/dialer/list/OnDragDropListener.java | 41 - .../list/OnListFragmentScrolledListener.java | 26 - .../android/dialer/list/PhoneFavoriteListView.java | 326 ---- .../dialer/list/PhoneFavoriteSquareTileView.java | 112 -- .../android/dialer/list/PhoneFavoriteTileView.java | 155 -- .../dialer/list/PhoneFavoritesTileAdapter.java | 696 ------- .../android/dialer/list/RegularSearchFragment.java | 151 -- .../dialer/list/RegularSearchListAdapter.java | 130 -- src/com/android/dialer/list/RemoveView.java | 94 - src/com/android/dialer/list/SearchFragment.java | 399 ---- .../dialer/list/SmartDialNumberListAdapter.java | 130 -- .../dialer/list/SmartDialSearchFragment.java | 134 -- src/com/android/dialer/list/SpeedDialFragment.java | 504 ----- .../android/dialer/logging/InteractionEvent.java | 76 - src/com/android/dialer/logging/Logger.java | 85 - src/com/android/dialer/logging/ScreenEvent.java | 172 -- .../dialer/service/CachedNumberLookupService.java | 61 - .../dialer/service/ExtendedCallInfoService.java | 79 - .../settings/AppCompatPreferenceActivity.java | 155 -- .../dialer/settings/DefaultRingtonePreference.java | 65 - .../dialer/settings/DialerSettingsActivity.java | 190 -- .../settings/DisplayOptionsSettingsFragment.java | 31 - .../dialer/settings/SoundSettingsFragment.java | 245 --- .../android/dialer/util/AppCompatConstants.java | 30 - src/com/android/dialer/util/Assert.java | 36 - src/com/android/dialer/util/AsyncTaskExecutor.java | 48 - .../android/dialer/util/AsyncTaskExecutors.java | 100 - .../dialer/util/BlockReportSpamDialogs.java | 293 --- src/com/android/dialer/util/DialerUtils.java | 195 -- src/com/android/dialer/util/EmptyLoader.java | 60 - src/com/android/dialer/util/ExpirableCache.java | 266 --- src/com/android/dialer/util/IntentUtil.java | 162 -- src/com/android/dialer/util/MoreStrings.java | 43 - src/com/android/dialer/util/OrientationUtil.java | 34 - src/com/android/dialer/util/PhoneLookupUtil.java | 40 - src/com/android/dialer/util/PhoneNumberUtil.java | 138 -- src/com/android/dialer/util/TelecomUtil.java | 229 --- .../voicemail/VisualVoicemailEnabledChecker.java | 103 -- .../dialer/voicemail/VoicemailArchiveActivity.java | 160 -- .../VoicemailArchivePlaybackPresenter.java | 90 - .../dialer/voicemail/VoicemailAsyncTaskUtil.java | 346 ---- .../dialer/voicemail/VoicemailAudioManager.java | 200 -- .../dialer/voicemail/VoicemailPlaybackLayout.java | 638 ------- .../voicemail/VoicemailPlaybackPresenter.java | 1010 ---------- .../dialer/voicemail/VoicemailStatusHelper.java | 91 - .../voicemail/VoicemailStatusHelperImpl.java | 272 --- .../dialer/voicemail/WiredHeadsetManager.java | 88 - .../android/dialer/widget/ActionBarController.java | 243 --- .../android/dialer/widget/EmptyContentView.java | 118 -- .../dialer/widget/SearchEditTextLayout.java | 321 ---- .../android/dialerbind/DatabaseHelperManager.java | 28 - src/com/android/dialerbind/ObjectFactory.java | 96 - tests/Android.mk | 28 - tests/AndroidManifest.xml | 69 - tests/assets/README.txt | 3 - tests/assets/quick_test_recording.mp3 | Bin 30591 -> 0 bytes tests/proguard.flags | 20 - tests/res/drawable/phone_icon.png | Bin 3621 -> 0 bytes tests/res/layout/fill_call_log_test.xml | 267 --- tests/res/values/donottranslate_strings.xml | 60 - tests/res/xml/iconset.xml | 23 - .../com/android/dialer/CallDetailActivityTest.java | 183 -- .../android/dialer/DialerLaunchPerformance.java | 50 - .../calllog/BlockReportSpamListenerTest.java | 72 - .../android/dialer/calllog/CallLogAdapterTest.java | 918 ---------- .../dialer/calllog/CallLogGroupBuilderTest.java | 470 ----- .../dialer/calllog/CallLogListItemHelperTest.java | 304 --- .../calllog/CallLogNotificationsHelperTest.java | 137 -- .../dialer/calllog/CallLogQueryTestUtils.java | 46 - .../dialer/calllog/ContactInfoHelperTest.java | 160 -- .../dialer/calllog/GroupingListAdapterTests.java | 173 -- .../dialer/calllog/PhoneAccountUtilsTest.java | 104 -- .../dialer/calllog/PhoneCallDetailsHelperTest.java | 581 ------ .../dialer/calllog/PhoneCallDetailsTest.java | 63 - .../calllogcache/TestTelecomCallLogCache.java | 65 - .../FilteredNumberCompatInstrumentationTest.java | 92 - .../dialer/compat/FilteredNumberCompatTest.java | 292 --- .../dialer/compat/UserManagerCompatTest.java | 44 - .../dialer/contactinfo/ContactPhotoLoaderTest.java | 106 -- .../android/dialer/database/DatabaseTestUtils.java | 82 - .../dialer/database/DialerDatabaseHelperTest.java | 154 -- .../FilteredNumberAsyncQueryHandlerTest.java | 457 ----- .../database/FilteredNumberProviderTest.java | 232 --- .../dialer/database/SmartDialPrefixTest.java | 523 ------ .../database/VoicemailArchiveProviderTest.java | 306 ---- .../DialpadFragmentInstrumentationTest.java | 121 -- .../dialer/dialpad/DialpadFragmentTest.java | 111 -- .../dialer/dialpad/SmartDialNameMatcherTest.java | 275 --- .../dialpad/UnicodeDialerKeyListenerTest.java | 74 - .../BlockedNumbersAutoMigratorTest.java | 201 -- .../BlockedNumbersFragmentInstrumentationTest.java | 93 - .../filterednumber/BlockedNumbersMigratorTest.java | 160 -- .../filterednumber/FilteredNumbersUtilTest.java | 132 -- ...edNumbersDialogFragmentInstrumentationTest.java | 93 - .../MigrateBlockedNumbersDialogFragmentTest.java | 61 - .../interactions/PhoneNumberInteractionTest.java | 262 --- .../dialer/list/PhoneFavoritesTileAdapterTest.java | 301 --- .../tests/calllog/FillCallLogTestActivity.java | 658 ------- .../com/android/dialer/util/DialerUtilsTest.java | 78 - .../android/dialer/util/ExpirableCacheTest.java | 125 -- .../android/dialer/util/FakeAsyncTaskExecutor.java | 231 --- .../com/android/dialer/util/LocaleTestUtils.java | 119 -- .../src/com/android/dialer/util/TestConstants.java | 5 - .../VoicemailActivityInstrumentationTestCase2.java | 227 --- .../dialer/voicemail/VoicemailArchiveTest.java | 116 -- .../voicemail/VoicemailAsyncTaskUtilTest.java | 388 ---- .../dialer/voicemail/VoicemailPlaybackTest.java | 145 -- .../voicemail/VoicemailStatusHelperImplTest.java | 274 --- .../dialer/widget/ActionBarControllerTest.java | 181 -- tools/gradle/android.properties | 2 - tools/gradle/gradlew | 204 --- tools/gradle/repositories.properties | 1 - tools/gradle/settings.gradle | 63 - 3031 files changed, 163126 insertions(+), 118613 deletions(-) create mode 100644 CONTRIBUTING delete mode 100644 InCallUI/AndroidManifest.xml delete mode 100644 InCallUI/build.gradle delete mode 100644 InCallUI/proguard.flags delete mode 100644 InCallUI/res/anim/activity_open_enter.xml delete mode 100644 InCallUI/res/anim/activity_open_exit.xml delete mode 100644 InCallUI/res/anim/call_status_pulse.xml delete mode 100644 InCallUI/res/anim/decelerate_cubic.xml delete mode 100644 InCallUI/res/anim/decelerate_quint.xml delete mode 100644 InCallUI/res/color/ota_title_color.xml delete mode 100644 InCallUI/res/color/selectable_icon_tint.xml delete mode 100644 InCallUI/res/drawable-hdpi/fab_blue.png delete mode 100644 InCallUI/res/drawable-hdpi/fab_ic_call.png delete mode 100644 InCallUI/res/drawable-hdpi/fab_ic_end_call.png delete mode 100644 InCallUI/res/drawable-hdpi/fab_ic_message.png delete mode 100644 InCallUI/res/drawable-hdpi/fab_red.png delete mode 100644 InCallUI/res/drawable-hdpi/ic_block_grey600_24dp.png delete mode 100644 InCallUI/res/drawable-hdpi/ic_business_white_24dp.png delete mode 100644 InCallUI/res/drawable-hdpi/ic_call_end_white_24dp.png delete mode 100644 InCallUI/res/drawable-hdpi/ic_call_split_white_24dp.png delete mode 100644 InCallUI/res/drawable-hdpi/ic_call_white_24dp.png delete mode 100644 InCallUI/res/drawable-hdpi/ic_close_grey600_24dp.png delete mode 100644 InCallUI/res/drawable-hdpi/ic_forward_white_24dp.png delete mode 100644 InCallUI/res/drawable-hdpi/ic_hd_24dp.png delete mode 100644 InCallUI/res/drawable-hdpi/ic_location_on_white_24dp.png delete mode 100644 InCallUI/res/drawable-hdpi/ic_lockscreen_glowdot.png delete mode 100644 InCallUI/res/drawable-hdpi/ic_person_add_grey600_24dp.png delete mode 100644 InCallUI/res/drawable-hdpi/ic_phone_paused_white_24dp.png delete mode 100644 InCallUI/res/drawable-hdpi/ic_question_mark.png delete mode 100644 InCallUI/res/drawable-hdpi/ic_report_white_36dp.png delete mode 100644 InCallUI/res/drawable-hdpi/ic_schedule_white_24dp.png delete mode 100644 InCallUI/res/drawable-hdpi/ic_toolbar_add_call.png delete mode 100644 InCallUI/res/drawable-hdpi/ic_toolbar_arrow_whitespace.png delete mode 100644 InCallUI/res/drawable-hdpi/ic_toolbar_audio_bluetooth.png delete mode 100644 InCallUI/res/drawable-hdpi/ic_toolbar_audio_headphones.png delete mode 100644 InCallUI/res/drawable-hdpi/ic_toolbar_audio_phone.png delete mode 100644 InCallUI/res/drawable-hdpi/ic_toolbar_dialpad.png delete mode 100644 InCallUI/res/drawable-hdpi/ic_toolbar_hold.png delete mode 100644 InCallUI/res/drawable-hdpi/ic_toolbar_merge.png delete mode 100644 InCallUI/res/drawable-hdpi/ic_toolbar_mic_off.png delete mode 100644 InCallUI/res/drawable-hdpi/ic_toolbar_speaker_on.png delete mode 100644 InCallUI/res/drawable-hdpi/ic_toolbar_swap.png delete mode 100644 InCallUI/res/drawable-hdpi/ic_toolbar_video.png delete mode 100644 InCallUI/res/drawable-hdpi/ic_toolbar_video_off.png delete mode 100644 InCallUI/res/drawable-hdpi/ic_toolbar_video_switch.png delete mode 100644 InCallUI/res/drawable-hdpi/img_business.png delete mode 100644 InCallUI/res/drawable-hdpi/img_conference.png delete mode 100644 InCallUI/res/drawable-hdpi/img_no_image.png delete mode 100644 InCallUI/res/drawable-hdpi/img_phone.png delete mode 100644 InCallUI/res/drawable-land/rounded_call_card_background.xml delete mode 100644 InCallUI/res/drawable-mdpi/fab_blue.png delete mode 100644 InCallUI/res/drawable-mdpi/fab_ic_call.png delete mode 100644 InCallUI/res/drawable-mdpi/fab_ic_end_call.png delete mode 100644 InCallUI/res/drawable-mdpi/fab_ic_message.png delete mode 100644 InCallUI/res/drawable-mdpi/fab_red.png delete mode 100644 InCallUI/res/drawable-mdpi/ic_block_grey600_24dp.png delete mode 100644 InCallUI/res/drawable-mdpi/ic_business_white_24dp.png delete mode 100644 InCallUI/res/drawable-mdpi/ic_call_end_white_24dp.png delete mode 100644 InCallUI/res/drawable-mdpi/ic_call_split_white_24dp.png delete mode 100644 InCallUI/res/drawable-mdpi/ic_call_white_24dp.png delete mode 100644 InCallUI/res/drawable-mdpi/ic_close_grey600_24dp.png delete mode 100644 InCallUI/res/drawable-mdpi/ic_forward_white_24dp.png delete mode 100644 InCallUI/res/drawable-mdpi/ic_hd_24dp.png delete mode 100644 InCallUI/res/drawable-mdpi/ic_location_on_white_24dp.png delete mode 100644 InCallUI/res/drawable-mdpi/ic_lockscreen_glowdot.png delete mode 100644 InCallUI/res/drawable-mdpi/ic_person_add_grey600_24dp.png delete mode 100644 InCallUI/res/drawable-mdpi/ic_phone_paused_white_24dp.png delete mode 100644 InCallUI/res/drawable-mdpi/ic_question_mark.png delete mode 100644 InCallUI/res/drawable-mdpi/ic_report_white_36dp.png delete mode 100644 InCallUI/res/drawable-mdpi/ic_schedule_white_24dp.png delete mode 100644 InCallUI/res/drawable-mdpi/ic_toolbar_add_call.png delete mode 100644 InCallUI/res/drawable-mdpi/ic_toolbar_arrow_whitespace.png delete mode 100644 InCallUI/res/drawable-mdpi/ic_toolbar_audio_bluetooth.png delete mode 100644 InCallUI/res/drawable-mdpi/ic_toolbar_audio_headphones.png delete mode 100644 InCallUI/res/drawable-mdpi/ic_toolbar_audio_phone.png delete mode 100644 InCallUI/res/drawable-mdpi/ic_toolbar_dialpad.png delete mode 100644 InCallUI/res/drawable-mdpi/ic_toolbar_hold.png delete mode 100644 InCallUI/res/drawable-mdpi/ic_toolbar_merge.png delete mode 100644 InCallUI/res/drawable-mdpi/ic_toolbar_mic_off.png delete mode 100644 InCallUI/res/drawable-mdpi/ic_toolbar_speaker_on.png delete mode 100644 InCallUI/res/drawable-mdpi/ic_toolbar_swap.png delete mode 100644 InCallUI/res/drawable-mdpi/ic_toolbar_video.png delete mode 100644 InCallUI/res/drawable-mdpi/ic_toolbar_video_off.png delete mode 100644 InCallUI/res/drawable-mdpi/ic_toolbar_video_switch.png delete mode 100644 InCallUI/res/drawable-mdpi/img_business.png delete mode 100644 InCallUI/res/drawable-mdpi/img_conference.png delete mode 100644 InCallUI/res/drawable-mdpi/img_no_image.png delete mode 100644 InCallUI/res/drawable-mdpi/img_phone.png delete mode 100644 InCallUI/res/drawable-xhdpi/fab_blue.png delete mode 100644 InCallUI/res/drawable-xhdpi/fab_ic_call.png delete mode 100644 InCallUI/res/drawable-xhdpi/fab_ic_end_call.png delete mode 100644 InCallUI/res/drawable-xhdpi/fab_ic_message.png delete mode 100644 InCallUI/res/drawable-xhdpi/fab_red.png delete mode 100644 InCallUI/res/drawable-xhdpi/ic_block_grey600_24dp.png delete mode 100644 InCallUI/res/drawable-xhdpi/ic_business_white_24dp.png delete mode 100644 InCallUI/res/drawable-xhdpi/ic_call_end_white_24dp.png delete mode 100644 InCallUI/res/drawable-xhdpi/ic_call_split_white_24dp.png delete mode 100644 InCallUI/res/drawable-xhdpi/ic_call_white_24dp.png delete mode 100644 InCallUI/res/drawable-xhdpi/ic_close_grey600_24dp.png delete mode 100644 InCallUI/res/drawable-xhdpi/ic_forward_white_24dp.png delete mode 100644 InCallUI/res/drawable-xhdpi/ic_hd_24dp.png delete mode 100644 InCallUI/res/drawable-xhdpi/ic_location_on_white_24dp.png delete mode 100644 InCallUI/res/drawable-xhdpi/ic_lockscreen_glowdot.png delete mode 100644 InCallUI/res/drawable-xhdpi/ic_person_add_grey600_24dp.png delete mode 100644 InCallUI/res/drawable-xhdpi/ic_phone_paused_white_24dp.png delete mode 100644 InCallUI/res/drawable-xhdpi/ic_question_mark.png delete mode 100644 InCallUI/res/drawable-xhdpi/ic_report_white_36dp.png delete mode 100644 InCallUI/res/drawable-xhdpi/ic_schedule_white_24dp.png delete mode 100644 InCallUI/res/drawable-xhdpi/ic_toolbar_add_call.png delete mode 100644 InCallUI/res/drawable-xhdpi/ic_toolbar_arrow_whitespace.png delete mode 100644 InCallUI/res/drawable-xhdpi/ic_toolbar_audio_bluetooth.png delete mode 100644 InCallUI/res/drawable-xhdpi/ic_toolbar_audio_headphones.png delete mode 100644 InCallUI/res/drawable-xhdpi/ic_toolbar_audio_phone.png delete mode 100644 InCallUI/res/drawable-xhdpi/ic_toolbar_dialpad.png delete mode 100644 InCallUI/res/drawable-xhdpi/ic_toolbar_hold.png delete mode 100644 InCallUI/res/drawable-xhdpi/ic_toolbar_merge.png delete mode 100644 InCallUI/res/drawable-xhdpi/ic_toolbar_mic_off.png delete mode 100644 InCallUI/res/drawable-xhdpi/ic_toolbar_speaker_on.png delete mode 100644 InCallUI/res/drawable-xhdpi/ic_toolbar_swap.png delete mode 100644 InCallUI/res/drawable-xhdpi/ic_toolbar_video.png delete mode 100644 InCallUI/res/drawable-xhdpi/ic_toolbar_video_off.png delete mode 100644 InCallUI/res/drawable-xhdpi/ic_toolbar_video_switch.png delete mode 100644 InCallUI/res/drawable-xhdpi/img_business.png delete mode 100644 InCallUI/res/drawable-xhdpi/img_conference.png delete mode 100644 InCallUI/res/drawable-xhdpi/img_no_image.png delete mode 100644 InCallUI/res/drawable-xhdpi/img_phone.png delete mode 100644 InCallUI/res/drawable-xxhdpi/fab_blue.png delete mode 100644 InCallUI/res/drawable-xxhdpi/fab_ic_call.png delete mode 100644 InCallUI/res/drawable-xxhdpi/fab_ic_end_call.png delete mode 100644 InCallUI/res/drawable-xxhdpi/fab_ic_message.png delete mode 100644 InCallUI/res/drawable-xxhdpi/fab_red.png delete mode 100644 InCallUI/res/drawable-xxhdpi/ic_block_grey600_24dp.png delete mode 100644 InCallUI/res/drawable-xxhdpi/ic_business_white_24dp.png delete mode 100644 InCallUI/res/drawable-xxhdpi/ic_call_end_white_24dp.png delete mode 100644 InCallUI/res/drawable-xxhdpi/ic_call_split_white_24dp.png delete mode 100644 InCallUI/res/drawable-xxhdpi/ic_call_white_24dp.png delete mode 100644 InCallUI/res/drawable-xxhdpi/ic_close_grey600_24dp.png delete mode 100644 InCallUI/res/drawable-xxhdpi/ic_forward_white_24dp.png delete mode 100644 InCallUI/res/drawable-xxhdpi/ic_hd_24dp.png delete mode 100644 InCallUI/res/drawable-xxhdpi/ic_location_on_white_24dp.png delete mode 100644 InCallUI/res/drawable-xxhdpi/ic_lockscreen_glowdot.png delete mode 100644 InCallUI/res/drawable-xxhdpi/ic_person_add_grey600_24dp.png delete mode 100644 InCallUI/res/drawable-xxhdpi/ic_phone_paused_white_24dp.png delete mode 100644 InCallUI/res/drawable-xxhdpi/ic_question_mark.png delete mode 100644 InCallUI/res/drawable-xxhdpi/ic_report_white_36dp.png delete mode 100644 InCallUI/res/drawable-xxhdpi/ic_schedule_white_24dp.png delete mode 100644 InCallUI/res/drawable-xxhdpi/ic_toolbar_add_call.png delete mode 100644 InCallUI/res/drawable-xxhdpi/ic_toolbar_arrow_whitespace.png delete mode 100644 InCallUI/res/drawable-xxhdpi/ic_toolbar_audio_bluetooth.png delete mode 100644 InCallUI/res/drawable-xxhdpi/ic_toolbar_audio_headphones.png delete mode 100644 InCallUI/res/drawable-xxhdpi/ic_toolbar_audio_phone.png delete mode 100644 InCallUI/res/drawable-xxhdpi/ic_toolbar_dialpad.png delete mode 100644 InCallUI/res/drawable-xxhdpi/ic_toolbar_hold.png delete mode 100644 InCallUI/res/drawable-xxhdpi/ic_toolbar_merge.png delete mode 100644 InCallUI/res/drawable-xxhdpi/ic_toolbar_mic_off.png delete mode 100644 InCallUI/res/drawable-xxhdpi/ic_toolbar_speaker_on.png delete mode 100644 InCallUI/res/drawable-xxhdpi/ic_toolbar_swap.png delete mode 100644 InCallUI/res/drawable-xxhdpi/ic_toolbar_video.png delete mode 100644 InCallUI/res/drawable-xxhdpi/ic_toolbar_video_off.png delete mode 100644 InCallUI/res/drawable-xxhdpi/ic_toolbar_video_switch.png delete mode 100644 InCallUI/res/drawable-xxhdpi/img_business.png delete mode 100644 InCallUI/res/drawable-xxhdpi/img_conference.png delete mode 100644 InCallUI/res/drawable-xxhdpi/img_no_image.png delete mode 100644 InCallUI/res/drawable-xxhdpi/img_phone.png delete mode 100644 InCallUI/res/drawable-xxxhdpi/fab_blue.png delete mode 100644 InCallUI/res/drawable-xxxhdpi/fab_ic_call.png delete mode 100644 InCallUI/res/drawable-xxxhdpi/fab_ic_end_call.png delete mode 100644 InCallUI/res/drawable-xxxhdpi/fab_ic_message.png delete mode 100644 InCallUI/res/drawable-xxxhdpi/fab_red.png delete mode 100644 InCallUI/res/drawable-xxxhdpi/ic_block_grey600_24dp.png delete mode 100644 InCallUI/res/drawable-xxxhdpi/ic_business_white_24dp.png delete mode 100644 InCallUI/res/drawable-xxxhdpi/ic_call_end_white_24dp.png delete mode 100644 InCallUI/res/drawable-xxxhdpi/ic_call_split_white_24dp.png delete mode 100644 InCallUI/res/drawable-xxxhdpi/ic_close_grey600_24dp.png delete mode 100644 InCallUI/res/drawable-xxxhdpi/ic_forward_white_24dp.png delete mode 100644 InCallUI/res/drawable-xxxhdpi/ic_hd_24dp.png delete mode 100644 InCallUI/res/drawable-xxxhdpi/ic_location_on_white_24dp.png delete mode 100644 InCallUI/res/drawable-xxxhdpi/ic_person_add_grey600_24dp.png delete mode 100644 InCallUI/res/drawable-xxxhdpi/ic_question_mark.png delete mode 100644 InCallUI/res/drawable-xxxhdpi/ic_report_white_36dp.png delete mode 100644 InCallUI/res/drawable-xxxhdpi/ic_schedule_white_24dp.png delete mode 100644 InCallUI/res/drawable-xxxhdpi/ic_toolbar_add_call.png delete mode 100644 InCallUI/res/drawable-xxxhdpi/ic_toolbar_arrow_whitespace.png delete mode 100644 InCallUI/res/drawable-xxxhdpi/ic_toolbar_audio_bluetooth.png delete mode 100644 InCallUI/res/drawable-xxxhdpi/ic_toolbar_audio_headphones.png delete mode 100644 InCallUI/res/drawable-xxxhdpi/ic_toolbar_audio_phone.png delete mode 100644 InCallUI/res/drawable-xxxhdpi/ic_toolbar_dialpad.png delete mode 100644 InCallUI/res/drawable-xxxhdpi/ic_toolbar_hold.png delete mode 100644 InCallUI/res/drawable-xxxhdpi/ic_toolbar_merge.png delete mode 100644 InCallUI/res/drawable-xxxhdpi/ic_toolbar_mic_off.png delete mode 100644 InCallUI/res/drawable-xxxhdpi/ic_toolbar_speaker_on.png delete mode 100644 InCallUI/res/drawable-xxxhdpi/ic_toolbar_swap.png delete mode 100644 InCallUI/res/drawable-xxxhdpi/ic_toolbar_video.png delete mode 100644 InCallUI/res/drawable-xxxhdpi/ic_toolbar_video_off.png delete mode 100644 InCallUI/res/drawable-xxxhdpi/ic_toolbar_video_switch.png delete mode 100644 InCallUI/res/drawable-xxxhdpi/img_business.png delete mode 100644 InCallUI/res/drawable-xxxhdpi/img_conference.png delete mode 100644 InCallUI/res/drawable-xxxhdpi/img_no_image.png delete mode 100644 InCallUI/res/drawable-xxxhdpi/img_phone.png delete mode 100644 InCallUI/res/drawable/btn_add.xml delete mode 100644 InCallUI/res/drawable/btn_background.xml delete mode 100644 InCallUI/res/drawable/btn_change_to_video.xml delete mode 100644 InCallUI/res/drawable/btn_change_to_voice.xml delete mode 100644 InCallUI/res/drawable/btn_compound_audio.xml delete mode 100644 InCallUI/res/drawable/btn_compound_background.xml delete mode 100644 InCallUI/res/drawable/btn_compound_dialpad.xml delete mode 100644 InCallUI/res/drawable/btn_compound_hold.xml delete mode 100644 InCallUI/res/drawable/btn_compound_mute.xml delete mode 100644 InCallUI/res/drawable/btn_compound_video_off.xml delete mode 100644 InCallUI/res/drawable/btn_compound_video_switch.xml delete mode 100644 InCallUI/res/drawable/btn_merge.xml delete mode 100644 InCallUI/res/drawable/btn_overflow.xml delete mode 100644 InCallUI/res/drawable/btn_selected.xml delete mode 100644 InCallUI/res/drawable/btn_selected_focused.xml delete mode 100644 InCallUI/res/drawable/btn_swap.xml delete mode 100644 InCallUI/res/drawable/btn_unselected.xml delete mode 100644 InCallUI/res/drawable/btn_unselected_focused.xml delete mode 100644 InCallUI/res/drawable/conference_ripple.xml delete mode 100644 InCallUI/res/drawable/end_call_background.xml delete mode 100644 InCallUI/res/drawable/ic_incall_audio_handle.xml delete mode 100644 InCallUI/res/drawable/ic_incall_video_handle.xml delete mode 100644 InCallUI/res/drawable/ic_lockscreen_answer.xml delete mode 100644 InCallUI/res/drawable/ic_lockscreen_answer_activated_layer.xml delete mode 100644 InCallUI/res/drawable/ic_lockscreen_answer_normal_layer.xml delete mode 100644 InCallUI/res/drawable/ic_lockscreen_answer_video.xml delete mode 100644 InCallUI/res/drawable/ic_lockscreen_answer_video_activated_layer.xml delete mode 100644 InCallUI/res/drawable/ic_lockscreen_answer_video_normal_layer.xml delete mode 100644 InCallUI/res/drawable/ic_lockscreen_decline.xml delete mode 100644 InCallUI/res/drawable/ic_lockscreen_decline_activated_layer.xml delete mode 100644 InCallUI/res/drawable/ic_lockscreen_decline_normal_layer.xml delete mode 100644 InCallUI/res/drawable/ic_lockscreen_decline_video.xml delete mode 100644 InCallUI/res/drawable/ic_lockscreen_decline_video_activated_layer.xml delete mode 100644 InCallUI/res/drawable/ic_lockscreen_decline_video_normal_layer.xml delete mode 100644 InCallUI/res/drawable/ic_lockscreen_outerring.xml delete mode 100644 InCallUI/res/drawable/ic_lockscreen_text.xml delete mode 100644 InCallUI/res/drawable/ic_lockscreen_text_activated_layer.xml delete mode 100644 InCallUI/res/drawable/ic_lockscreen_text_normal_layer.xml delete mode 100644 InCallUI/res/drawable/img_conference_automirrored.xml delete mode 100644 InCallUI/res/drawable/img_no_image_automirrored.xml delete mode 100644 InCallUI/res/drawable/incoming_sms_background.xml delete mode 100644 InCallUI/res/drawable/outgoing_sms_background.xml delete mode 100644 InCallUI/res/drawable/spam_notification_icon.xml delete mode 100644 InCallUI/res/drawable/subject_bubble.xml delete mode 100644 InCallUI/res/drawable/unknown_notification_icon.xml delete mode 100644 InCallUI/res/layout-h400dp/call_card_fragment.xml delete mode 100644 InCallUI/res/layout-h600dp/manage_conference_call_button.xml delete mode 100644 InCallUI/res/layout-w500dp-land/call_card_fragment.xml delete mode 100644 InCallUI/res/layout-w600dp-land/manage_conference_call_button.xml delete mode 100644 InCallUI/res/layout/accessible_answer_fragment.xml delete mode 100644 InCallUI/res/layout/answer_fragment.xml delete mode 100644 InCallUI/res/layout/business_contact_context_list_header.xml delete mode 100644 InCallUI/res/layout/business_context_info_list_item.xml delete mode 100644 InCallUI/res/layout/call_button_fragment.xml delete mode 100644 InCallUI/res/layout/call_card_fragment.xml delete mode 100644 InCallUI/res/layout/caller_in_conference.xml delete mode 100644 InCallUI/res/layout/conference_manager_fragment.xml delete mode 100644 InCallUI/res/layout/incall_dialpad_fragment.xml delete mode 100644 InCallUI/res/layout/incall_screen.xml delete mode 100644 InCallUI/res/layout/manage_conference_call_button.xml delete mode 100644 InCallUI/res/layout/outgoing_call_animation.xml delete mode 100644 InCallUI/res/layout/person_context_info_list_item.xml delete mode 100644 InCallUI/res/layout/primary_call_info.xml delete mode 100644 InCallUI/res/layout/secondary_call_info.xml delete mode 100644 InCallUI/res/layout/video_call_fragment.xml delete mode 100644 InCallUI/res/layout/video_call_views.xml delete mode 100644 InCallUI/res/menu/incall_audio_mode_menu.xml delete mode 100644 InCallUI/res/values-af/strings.xml delete mode 100644 InCallUI/res/values-am/strings.xml delete mode 100644 InCallUI/res/values-ar/strings.xml delete mode 100644 InCallUI/res/values-az/strings.xml delete mode 100644 InCallUI/res/values-b+sr+Latn/strings.xml delete mode 100644 InCallUI/res/values-be/strings.xml delete mode 100644 InCallUI/res/values-bg/strings.xml delete mode 100644 InCallUI/res/values-bn/strings.xml delete mode 100644 InCallUI/res/values-bs/strings.xml delete mode 100644 InCallUI/res/values-ca/strings.xml delete mode 100644 InCallUI/res/values-cs/strings.xml delete mode 100644 InCallUI/res/values-da/strings.xml delete mode 100644 InCallUI/res/values-de/strings.xml delete mode 100644 InCallUI/res/values-el/strings.xml delete mode 100644 InCallUI/res/values-en-rAU/strings.xml delete mode 100644 InCallUI/res/values-en-rGB/strings.xml delete mode 100644 InCallUI/res/values-en-rIN/strings.xml delete mode 100644 InCallUI/res/values-es-rUS/strings.xml delete mode 100644 InCallUI/res/values-es/strings.xml delete mode 100644 InCallUI/res/values-et/strings.xml delete mode 100644 InCallUI/res/values-eu/strings.xml delete mode 100644 InCallUI/res/values-fa/strings.xml delete mode 100644 InCallUI/res/values-fi/strings.xml delete mode 100644 InCallUI/res/values-fr-rCA/strings.xml delete mode 100644 InCallUI/res/values-fr/strings.xml delete mode 100644 InCallUI/res/values-gl/strings.xml delete mode 100644 InCallUI/res/values-gu/strings.xml delete mode 100644 InCallUI/res/values-h400dp/dimens.xml delete mode 100644 InCallUI/res/values-hi/strings.xml delete mode 100644 InCallUI/res/values-hr/strings.xml delete mode 100644 InCallUI/res/values-hu/strings.xml delete mode 100644 InCallUI/res/values-hy/strings.xml delete mode 100644 InCallUI/res/values-in/strings.xml delete mode 100644 InCallUI/res/values-is/strings.xml delete mode 100644 InCallUI/res/values-it/strings.xml delete mode 100644 InCallUI/res/values-iw/strings.xml delete mode 100644 InCallUI/res/values-ja/strings.xml delete mode 100644 InCallUI/res/values-ka/strings.xml delete mode 100644 InCallUI/res/values-kk/strings.xml delete mode 100644 InCallUI/res/values-km/strings.xml delete mode 100644 InCallUI/res/values-kn/strings.xml delete mode 100644 InCallUI/res/values-ko/strings.xml delete mode 100644 InCallUI/res/values-ky/strings.xml delete mode 100644 InCallUI/res/values-lo/strings.xml delete mode 100644 InCallUI/res/values-lt/strings.xml delete mode 100644 InCallUI/res/values-lv/strings.xml delete mode 100644 InCallUI/res/values-mk/strings.xml delete mode 100644 InCallUI/res/values-ml/strings.xml delete mode 100644 InCallUI/res/values-mn/strings.xml delete mode 100644 InCallUI/res/values-mr/strings.xml delete mode 100644 InCallUI/res/values-ms/strings.xml delete mode 100644 InCallUI/res/values-my/strings.xml delete mode 100644 InCallUI/res/values-nb/strings.xml delete mode 100644 InCallUI/res/values-ne/strings.xml delete mode 100644 InCallUI/res/values-nl/strings.xml delete mode 100644 InCallUI/res/values-pa/strings.xml delete mode 100644 InCallUI/res/values-pl/strings.xml delete mode 100644 InCallUI/res/values-pt-rBR/strings.xml delete mode 100644 InCallUI/res/values-pt-rPT/strings.xml delete mode 100644 InCallUI/res/values-pt/strings.xml delete mode 100644 InCallUI/res/values-ro/strings.xml delete mode 100644 InCallUI/res/values-ru/strings.xml delete mode 100644 InCallUI/res/values-si/strings.xml delete mode 100644 InCallUI/res/values-sk/strings.xml delete mode 100644 InCallUI/res/values-sl/strings.xml delete mode 100644 InCallUI/res/values-sq/strings.xml delete mode 100644 InCallUI/res/values-sr/strings.xml delete mode 100644 InCallUI/res/values-sv/strings.xml delete mode 100644 InCallUI/res/values-sw/strings.xml delete mode 100644 InCallUI/res/values-sw360dp/dimens.xml delete mode 100644 InCallUI/res/values-sw410dp/config.xml delete mode 100644 InCallUI/res/values-ta/strings.xml delete mode 100644 InCallUI/res/values-te/strings.xml delete mode 100644 InCallUI/res/values-th/strings.xml delete mode 100644 InCallUI/res/values-tl/strings.xml delete mode 100644 InCallUI/res/values-tr/strings.xml delete mode 100644 InCallUI/res/values-uk/strings.xml delete mode 100644 InCallUI/res/values-ur/strings.xml delete mode 100644 InCallUI/res/values-uz/strings.xml delete mode 100644 InCallUI/res/values-vi/strings.xml delete mode 100644 InCallUI/res/values-w500dp-land/colors.xml delete mode 100644 InCallUI/res/values-w500dp-land/dimens.xml delete mode 100644 InCallUI/res/values-zh-rCN/strings.xml delete mode 100644 InCallUI/res/values-zh-rHK/strings.xml delete mode 100644 InCallUI/res/values-zh-rTW/strings.xml delete mode 100644 InCallUI/res/values-zu/strings.xml delete mode 100644 InCallUI/res/values/animation_constants.xml delete mode 100644 InCallUI/res/values/array.xml delete mode 100644 InCallUI/res/values/attrs.xml delete mode 100644 InCallUI/res/values/colors.xml delete mode 100644 InCallUI/res/values/config.xml delete mode 100644 InCallUI/res/values/dimens.xml delete mode 100644 InCallUI/res/values/ids.xml delete mode 100644 InCallUI/res/values/strings.xml delete mode 100644 InCallUI/res/values/styles.xml delete mode 100644 InCallUI/src/com/android/incallui/AccelerometerListener.java delete mode 100644 InCallUI/src/com/android/incallui/AccessibleAnswerFragment.java delete mode 100644 InCallUI/src/com/android/incallui/AnswerFragment.java delete mode 100644 InCallUI/src/com/android/incallui/AnswerPresenter.java delete mode 100644 InCallUI/src/com/android/incallui/AudioModeProvider.java delete mode 100644 InCallUI/src/com/android/incallui/BaseFragment.java delete mode 100644 InCallUI/src/com/android/incallui/Call.java delete mode 100644 InCallUI/src/com/android/incallui/CallButtonFragment.java delete mode 100644 InCallUI/src/com/android/incallui/CallButtonPresenter.java delete mode 100644 InCallUI/src/com/android/incallui/CallCardFragment.java delete mode 100644 InCallUI/src/com/android/incallui/CallCardPresenter.java delete mode 100644 InCallUI/src/com/android/incallui/CallList.java delete mode 100644 InCallUI/src/com/android/incallui/CallTimer.java delete mode 100644 InCallUI/src/com/android/incallui/CallerInfo.java delete mode 100644 InCallUI/src/com/android/incallui/CallerInfoAsyncQuery.java delete mode 100644 InCallUI/src/com/android/incallui/CallerInfoUtils.java delete mode 100644 InCallUI/src/com/android/incallui/CircularRevealFragment.java delete mode 100644 InCallUI/src/com/android/incallui/ConferenceManagerFragment.java delete mode 100644 InCallUI/src/com/android/incallui/ConferenceManagerPresenter.java delete mode 100644 InCallUI/src/com/android/incallui/ConferenceParticipantListAdapter.java delete mode 100644 InCallUI/src/com/android/incallui/ContactInfoCache.java delete mode 100644 InCallUI/src/com/android/incallui/ContactUtils.java delete mode 100644 InCallUI/src/com/android/incallui/ContactsAsyncHelper.java delete mode 100644 InCallUI/src/com/android/incallui/ContactsPreferencesFactory.java delete mode 100644 InCallUI/src/com/android/incallui/DialpadFragment.java delete mode 100644 InCallUI/src/com/android/incallui/DialpadPresenter.java delete mode 100644 InCallUI/src/com/android/incallui/DistanceHelper.java delete mode 100644 InCallUI/src/com/android/incallui/ExternalCallList.java delete mode 100644 InCallUI/src/com/android/incallui/ExternalCallNotifier.java delete mode 100644 InCallUI/src/com/android/incallui/FragmentDisplayManager.java delete mode 100644 InCallUI/src/com/android/incallui/GlowPadAnswerFragment.java delete mode 100644 InCallUI/src/com/android/incallui/GlowPadWrapper.java delete mode 100644 InCallUI/src/com/android/incallui/InCallActivity.java delete mode 100644 InCallUI/src/com/android/incallui/InCallAnimationUtils.java delete mode 100644 InCallUI/src/com/android/incallui/InCallCameraManager.java delete mode 100644 InCallUI/src/com/android/incallui/InCallContactInteractions.java delete mode 100644 InCallUI/src/com/android/incallui/InCallDateUtils.java delete mode 100644 InCallUI/src/com/android/incallui/InCallOrientationEventListener.java delete mode 100644 InCallUI/src/com/android/incallui/InCallPresenter.java delete mode 100644 InCallUI/src/com/android/incallui/InCallServiceImpl.java delete mode 100644 InCallUI/src/com/android/incallui/InCallServiceListener.java delete mode 100644 InCallUI/src/com/android/incallui/InCallUIMaterialColorMapUtils.java delete mode 100644 InCallUI/src/com/android/incallui/InCallVideoCallCallback.java delete mode 100644 InCallUI/src/com/android/incallui/InCallVideoCallCallbackNotifier.java delete mode 100644 InCallUI/src/com/android/incallui/LatencyReport.java delete mode 100644 InCallUI/src/com/android/incallui/Log.java delete mode 100644 InCallUI/src/com/android/incallui/NeededForReflection.java delete mode 100644 InCallUI/src/com/android/incallui/NotificationBroadcastReceiver.java delete mode 100644 InCallUI/src/com/android/incallui/PostCharDialogFragment.java delete mode 100644 InCallUI/src/com/android/incallui/Presenter.java delete mode 100644 InCallUI/src/com/android/incallui/ProximitySensor.java delete mode 100644 InCallUI/src/com/android/incallui/StatusBarNotifier.java delete mode 100644 InCallUI/src/com/android/incallui/TelecomAdapter.java delete mode 100644 InCallUI/src/com/android/incallui/Ui.java delete mode 100644 InCallUI/src/com/android/incallui/VideoCallFragment.java delete mode 100644 InCallUI/src/com/android/incallui/VideoCallPresenter.java delete mode 100644 InCallUI/src/com/android/incallui/VideoPauseController.java delete mode 100644 InCallUI/src/com/android/incallui/VideoUtils.java delete mode 100644 InCallUI/src/com/android/incallui/async/PausableExecutor.java delete mode 100644 InCallUI/src/com/android/incallui/async/PausableExecutorImpl.java delete mode 100644 InCallUI/src/com/android/incallui/ringtone/DialerRingtoneManager.java delete mode 100644 InCallUI/src/com/android/incallui/ringtone/InCallTonePlayer.java delete mode 100644 InCallUI/src/com/android/incallui/ringtone/ToneGeneratorFactory.java delete mode 100644 InCallUI/src/com/android/incallui/service/PhoneNumberService.java delete mode 100644 InCallUI/src/com/android/incallui/spam/SpamCallListListener.java delete mode 100644 InCallUI/src/com/android/incallui/util/AccessibilityUtil.java delete mode 100644 InCallUI/src/com/android/incallui/util/TelecomCallUtil.java delete mode 100644 InCallUI/src/com/android/incallui/widget/multiwaveview/Ease.java delete mode 100644 InCallUI/src/com/android/incallui/widget/multiwaveview/GlowPadView.java delete mode 100644 InCallUI/src/com/android/incallui/widget/multiwaveview/PointCloud.java delete mode 100644 InCallUI/src/com/android/incallui/widget/multiwaveview/TargetDrawable.java delete mode 100644 InCallUI/src/com/android/incallui/widget/multiwaveview/Tweener.java delete mode 100644 InCallUI/src/com/android/incalluibind/ObjectFactory.java delete mode 100644 InCallUI/tests/src/com/android/incallui/CallCardPresenterTest.java delete mode 100644 InCallUI/tests/src/com/android/incallui/CallTest.java delete mode 100644 InCallUI/tests/src/com/android/incallui/CallerInfoUtilsTest.java delete mode 100644 InCallUI/tests/src/com/android/incallui/ContactsPreferencesFactoryTest.java delete mode 100644 InCallUI/tests/src/com/android/incallui/ExternalCallListTest.java delete mode 100644 InCallUI/tests/src/com/android/incallui/ExternalCallNotifierTest.java delete mode 100644 InCallUI/tests/src/com/android/incallui/InCallContactInteractionsTest.java delete mode 100644 InCallUI/tests/src/com/android/incallui/InCallPresenterTest.java delete mode 100644 InCallUI/tests/src/com/android/incallui/LatencyReportTest.java delete mode 100644 InCallUI/tests/src/com/android/incallui/MockCallListWrapper.java delete mode 100644 InCallUI/tests/src/com/android/incallui/ProximitySensorTest.java delete mode 100644 InCallUI/tests/src/com/android/incallui/StatusBarNotifierTest.java delete mode 100644 InCallUI/tests/src/com/android/incallui/TestTelecomCall.java delete mode 100644 InCallUI/tests/src/com/android/incallui/async/SingleProdThreadExecutor.java delete mode 100644 InCallUI/tests/src/com/android/incallui/ringtone/DialerRingtoneManagerTest.java delete mode 100644 InCallUI/tests/src/com/android/incallui/ringtone/InCallTonePlayerTest.java delete mode 100644 InCallUI/tests/src/com/android/incallui/spam/SpamCallListListenerTest.java create mode 100644 LICENSE create mode 100644 assets/product/res/drawable-hdpi/product_logo_avatar_anonymous_color_120.png create mode 100644 assets/product/res/drawable-hdpi/product_logo_avatar_anonymous_white_color_120.png create mode 100644 assets/product/res/drawable-mdpi/product_logo_avatar_anonymous_color_120.png create mode 100644 assets/product/res/drawable-mdpi/product_logo_avatar_anonymous_white_color_120.png create mode 100644 assets/product/res/drawable-xhdpi/product_logo_avatar_anonymous_color_120.png create mode 100644 assets/product/res/drawable-xhdpi/product_logo_avatar_anonymous_white_color_120.png create mode 100644 assets/product/res/drawable-xxhdpi/product_logo_avatar_anonymous_color_120.png create mode 100644 assets/product/res/drawable-xxhdpi/product_logo_avatar_anonymous_white_color_120.png create mode 100644 assets/product/res/drawable-xxxhdpi/product_logo_avatar_anonymous_color_120.png create mode 100644 assets/product/res/drawable-xxxhdpi/product_logo_avatar_anonymous_white_color_120.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_arrow_back_white_24.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_arrow_drop_down_white_18.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_bluetooth_audio_grey600_24.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_bluetooth_audio_white_36.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_call_end_white_24.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_call_end_white_36.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_call_merge_white_36.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_call_white_18.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_call_white_24.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_camera_alt_white_24.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_camera_alt_white_48.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_camera_front_white_36.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_camera_rear_white_36.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_check_black_24.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_check_black_36.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_check_white_48.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_close_white_24.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_close_white_36.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_dialpad_white_36.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_forward_white_24.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_fullscreen_exit_white_48.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_fullscreen_white_36.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_fullscreen_white_48.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_group_white_36.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_hd_white_24.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_headset_grey600_24.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_headset_white_36.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_message_white_24.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_mic_off_black_24.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_mic_off_white_36.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_network_wifi_white_24.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_pause_white_36.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_person_add_white_24.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_photo_library_white_24.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_photo_white_24.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_photo_white_48.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_report_white_18.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_send_black_24.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_send_white_24.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_swap_calls_white_36.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_switch_camera_white_36.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_switch_video_white_36.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_undo_white_48.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_videocam_off_white_24.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_videocam_off_white_36.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_videocam_white_18.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_videocam_white_24.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_videocam_white_36.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_voicemail_white_18.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_volume_up_grey600_24.png create mode 100644 assets/quantum/res/drawable-hdpi/quantum_ic_volume_up_white_36.png create mode 100644 assets/quantum/res/drawable-ldrtl-hdpi/quantum_ic_arrow_back_white_24.png create mode 100644 assets/quantum/res/drawable-ldrtl-hdpi/quantum_ic_send_black_24.png create mode 100644 assets/quantum/res/drawable-ldrtl-hdpi/quantum_ic_send_white_24.png create mode 100644 assets/quantum/res/drawable-ldrtl-hdpi/quantum_ic_undo_white_48.png create mode 100644 assets/quantum/res/drawable-ldrtl-mdpi/quantum_ic_arrow_back_white_24.png create mode 100644 assets/quantum/res/drawable-ldrtl-mdpi/quantum_ic_send_black_24.png create mode 100644 assets/quantum/res/drawable-ldrtl-mdpi/quantum_ic_send_white_24.png create mode 100644 assets/quantum/res/drawable-ldrtl-mdpi/quantum_ic_undo_white_48.png create mode 100644 assets/quantum/res/drawable-ldrtl-xhdpi/quantum_ic_arrow_back_white_24.png create mode 100644 assets/quantum/res/drawable-ldrtl-xhdpi/quantum_ic_send_black_24.png create mode 100644 assets/quantum/res/drawable-ldrtl-xhdpi/quantum_ic_send_white_24.png create mode 100644 assets/quantum/res/drawable-ldrtl-xhdpi/quantum_ic_undo_white_48.png create mode 100644 assets/quantum/res/drawable-ldrtl-xxhdpi/quantum_ic_arrow_back_white_24.png create mode 100644 assets/quantum/res/drawable-ldrtl-xxhdpi/quantum_ic_send_black_24.png create mode 100644 assets/quantum/res/drawable-ldrtl-xxhdpi/quantum_ic_send_white_24.png create mode 100644 assets/quantum/res/drawable-ldrtl-xxhdpi/quantum_ic_undo_white_48.png create mode 100644 assets/quantum/res/drawable-ldrtl-xxxhdpi/quantum_ic_arrow_back_white_24.png create mode 100644 assets/quantum/res/drawable-ldrtl-xxxhdpi/quantum_ic_send_black_24.png create mode 100644 assets/quantum/res/drawable-ldrtl-xxxhdpi/quantum_ic_send_white_24.png create mode 100644 assets/quantum/res/drawable-ldrtl-xxxhdpi/quantum_ic_undo_white_48.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_arrow_back_white_24.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_arrow_drop_down_white_18.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_bluetooth_audio_grey600_24.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_bluetooth_audio_white_36.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_call_end_white_24.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_call_end_white_36.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_call_merge_white_36.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_call_white_18.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_call_white_24.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_camera_alt_white_24.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_camera_alt_white_48.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_camera_front_white_36.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_camera_rear_white_36.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_check_black_24.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_check_black_36.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_check_white_48.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_close_white_24.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_close_white_36.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_dialpad_white_36.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_forward_white_24.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_fullscreen_exit_white_48.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_fullscreen_white_36.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_fullscreen_white_48.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_group_white_36.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_hd_white_24.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_headset_grey600_24.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_headset_white_36.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_message_white_24.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_mic_off_black_24.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_mic_off_white_36.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_network_wifi_white_24.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_pause_white_36.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_person_add_white_24.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_photo_library_white_24.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_photo_white_24.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_photo_white_48.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_report_white_18.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_send_black_24.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_send_white_24.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_swap_calls_white_36.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_switch_camera_white_36.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_switch_video_white_36.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_undo_white_48.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_videocam_off_white_24.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_videocam_off_white_36.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_videocam_white_18.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_videocam_white_24.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_videocam_white_36.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_voicemail_white_18.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_volume_up_grey600_24.png create mode 100644 assets/quantum/res/drawable-mdpi/quantum_ic_volume_up_white_36.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_arrow_back_white_24.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_arrow_drop_down_white_18.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_bluetooth_audio_grey600_24.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_bluetooth_audio_white_36.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_call_end_white_24.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_call_end_white_36.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_call_merge_white_36.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_call_white_18.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_call_white_24.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_camera_alt_white_24.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_camera_alt_white_48.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_camera_front_white_36.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_camera_rear_white_36.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_check_black_24.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_check_black_36.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_check_white_48.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_close_white_24.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_close_white_36.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_dialpad_white_36.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_forward_white_24.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_fullscreen_exit_white_48.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_fullscreen_white_36.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_fullscreen_white_48.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_group_white_36.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_hd_white_24.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_headset_grey600_24.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_headset_white_36.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_message_white_24.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_mic_off_black_24.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_mic_off_white_36.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_network_wifi_white_24.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_pause_white_36.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_person_add_white_24.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_photo_library_white_24.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_photo_white_24.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_photo_white_48.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_report_white_18.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_send_black_24.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_send_white_24.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_swap_calls_white_36.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_switch_camera_white_36.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_switch_video_white_36.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_undo_white_48.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_videocam_off_white_24.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_videocam_off_white_36.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_videocam_white_18.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_videocam_white_24.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_videocam_white_36.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_voicemail_white_18.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_volume_up_grey600_24.png create mode 100644 assets/quantum/res/drawable-xhdpi/quantum_ic_volume_up_white_36.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_arrow_back_white_24.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_arrow_drop_down_white_18.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_bluetooth_audio_grey600_24.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_bluetooth_audio_white_36.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_call_end_white_24.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_call_end_white_36.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_call_merge_white_36.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_call_white_18.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_call_white_24.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_camera_alt_white_24.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_camera_alt_white_48.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_camera_front_white_36.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_camera_rear_white_36.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_check_black_24.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_check_black_36.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_check_white_48.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_close_white_24.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_close_white_36.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_dialpad_white_36.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_forward_white_24.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_fullscreen_exit_white_48.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_fullscreen_white_36.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_fullscreen_white_48.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_group_white_36.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_hd_white_24.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_headset_grey600_24.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_headset_white_36.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_message_white_24.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_mic_off_black_24.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_mic_off_white_36.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_network_wifi_white_24.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_pause_white_36.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_person_add_white_24.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_photo_library_white_24.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_photo_white_24.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_photo_white_48.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_report_white_18.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_send_black_24.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_send_white_24.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_swap_calls_white_36.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_switch_camera_white_36.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_switch_video_white_36.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_undo_white_48.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_videocam_off_white_24.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_videocam_off_white_36.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_videocam_white_18.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_videocam_white_24.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_videocam_white_36.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_voicemail_white_18.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_volume_up_grey600_24.png create mode 100644 assets/quantum/res/drawable-xxhdpi/quantum_ic_volume_up_white_36.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_arrow_back_white_24.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_arrow_drop_down_white_18.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_bluetooth_audio_grey600_24.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_bluetooth_audio_white_36.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_call_end_white_24.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_call_end_white_36.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_call_merge_white_36.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_call_white_18.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_call_white_24.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_camera_alt_white_24.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_camera_alt_white_48.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_camera_front_white_36.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_camera_rear_white_36.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_check_black_24.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_check_black_36.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_check_white_48.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_close_white_24.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_close_white_36.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_dialpad_white_36.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_forward_white_24.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_fullscreen_exit_white_48.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_fullscreen_white_36.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_fullscreen_white_48.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_group_white_36.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_hd_white_24.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_headset_grey600_24.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_headset_white_36.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_message_white_24.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_mic_off_black_24.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_mic_off_white_36.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_network_wifi_white_24.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_pause_white_36.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_person_add_white_24.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_photo_library_white_24.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_photo_white_24.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_photo_white_48.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_report_white_18.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_send_black_24.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_send_white_24.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_swap_calls_white_36.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_switch_camera_white_36.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_switch_video_white_36.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_undo_white_48.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_videocam_off_white_24.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_videocam_off_white_36.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_videocam_white_18.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_videocam_white_24.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_videocam_white_36.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_voicemail_white_18.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_volume_up_grey600_24.png create mode 100644 assets/quantum/res/drawable-xxxhdpi/quantum_ic_volume_up_white_36.png delete mode 100644 build-app.gradle delete mode 100644 build-library.gradle create mode 100644 java/com/android/contacts/common/AndroidManifest.xml create mode 100644 java/com/android/contacts/common/Bindings.java create mode 100644 java/com/android/contacts/common/ClipboardUtils.java create mode 100644 java/com/android/contacts/common/Collapser.java create mode 100644 java/com/android/contacts/common/ContactPhotoManager.java create mode 100644 java/com/android/contacts/common/ContactPhotoManagerImpl.java create mode 100644 java/com/android/contacts/common/ContactPresenceIconUtil.java create mode 100644 java/com/android/contacts/common/ContactStatusUtil.java create mode 100644 java/com/android/contacts/common/ContactTileLoaderFactory.java create mode 100644 java/com/android/contacts/common/ContactsUtils.java create mode 100644 java/com/android/contacts/common/GeoUtil.java create mode 100644 java/com/android/contacts/common/GroupMetaData.java create mode 100644 java/com/android/contacts/common/MoreContactUtils.java create mode 100644 java/com/android/contacts/common/bindings/ContactsCommonBindings.java create mode 100644 java/com/android/contacts/common/bindings/ContactsCommonBindingsFactory.java create mode 100644 java/com/android/contacts/common/bindings/ContactsCommonBindingsStub.java create mode 100644 java/com/android/contacts/common/compat/CallCompat.java create mode 100644 java/com/android/contacts/common/compat/CallableCompat.java create mode 100644 java/com/android/contacts/common/compat/ContactsCompat.java create mode 100644 java/com/android/contacts/common/compat/DirectoryCompat.java create mode 100644 java/com/android/contacts/common/compat/PhoneAccountCompat.java create mode 100644 java/com/android/contacts/common/compat/PhoneCompat.java create mode 100644 java/com/android/contacts/common/compat/PhoneNumberUtilsCompat.java create mode 100644 java/com/android/contacts/common/compat/TelephonyManagerCompat.java create mode 100644 java/com/android/contacts/common/compat/telecom/TelecomManagerCompat.java create mode 100644 java/com/android/contacts/common/database/ContactUpdateUtils.java create mode 100644 java/com/android/contacts/common/database/EmptyCursor.java create mode 100644 java/com/android/contacts/common/database/NoNullCursorAsyncQueryHandler.java create mode 100644 java/com/android/contacts/common/dialog/CallSubjectDialog.java create mode 100644 java/com/android/contacts/common/dialog/ClearFrequentsDialog.java create mode 100644 java/com/android/contacts/common/extensions/PhoneDirectoryExtender.java create mode 100644 java/com/android/contacts/common/extensions/PhoneDirectoryExtenderAccessor.java create mode 100644 java/com/android/contacts/common/extensions/PhoneDirectoryExtenderFactory.java create mode 100644 java/com/android/contacts/common/extensions/PhoneDirectoryExtenderStub.java create mode 100644 java/com/android/contacts/common/format/FormatUtils.java create mode 100644 java/com/android/contacts/common/format/TextHighlighter.java create mode 100644 java/com/android/contacts/common/format/testing/SpannedTestUtils.java create mode 100644 java/com/android/contacts/common/lettertiles/LetterTileDrawable.java create mode 100644 java/com/android/contacts/common/list/AutoScrollListView.java create mode 100644 java/com/android/contacts/common/list/ContactEntry.java create mode 100644 java/com/android/contacts/common/list/ContactEntryListAdapter.java create mode 100644 java/com/android/contacts/common/list/ContactEntryListFragment.java create mode 100644 java/com/android/contacts/common/list/ContactListAdapter.java create mode 100644 java/com/android/contacts/common/list/ContactListFilter.java create mode 100644 java/com/android/contacts/common/list/ContactListFilterController.java create mode 100644 java/com/android/contacts/common/list/ContactListItemView.java create mode 100644 java/com/android/contacts/common/list/ContactListPinnedHeaderView.java create mode 100644 java/com/android/contacts/common/list/ContactTileView.java create mode 100644 java/com/android/contacts/common/list/ContactsSectionIndexer.java create mode 100644 java/com/android/contacts/common/list/DefaultContactListAdapter.java create mode 100644 java/com/android/contacts/common/list/DirectoryListLoader.java create mode 100644 java/com/android/contacts/common/list/DirectoryPartition.java create mode 100644 java/com/android/contacts/common/list/IndexerListAdapter.java create mode 100644 java/com/android/contacts/common/list/OnPhoneNumberPickerActionListener.java create mode 100644 java/com/android/contacts/common/list/PhoneNumberListAdapter.java create mode 100644 java/com/android/contacts/common/list/PhoneNumberPickerFragment.java create mode 100644 java/com/android/contacts/common/list/PinnedHeaderListAdapter.java create mode 100644 java/com/android/contacts/common/list/PinnedHeaderListView.java create mode 100644 java/com/android/contacts/common/list/ViewPagerTabStrip.java create mode 100644 java/com/android/contacts/common/list/ViewPagerTabs.java create mode 100644 java/com/android/contacts/common/location/CountryDetector.java create mode 100644 java/com/android/contacts/common/location/UpdateCountryService.java create mode 100644 java/com/android/contacts/common/model/AccountTypeManager.java create mode 100644 java/com/android/contacts/common/model/BuilderWrapper.java create mode 100644 java/com/android/contacts/common/model/CPOWrapper.java create mode 100644 java/com/android/contacts/common/model/Contact.java create mode 100644 java/com/android/contacts/common/model/ContactLoader.java create mode 100644 java/com/android/contacts/common/model/RawContact.java create mode 100644 java/com/android/contacts/common/model/account/AccountType.java create mode 100644 java/com/android/contacts/common/model/account/AccountTypeWithDataSet.java create mode 100644 java/com/android/contacts/common/model/account/AccountWithDataSet.java create mode 100644 java/com/android/contacts/common/model/account/BaseAccountType.java create mode 100644 java/com/android/contacts/common/model/account/ExchangeAccountType.java create mode 100644 java/com/android/contacts/common/model/account/ExternalAccountType.java create mode 100644 java/com/android/contacts/common/model/account/FallbackAccountType.java create mode 100644 java/com/android/contacts/common/model/account/GoogleAccountType.java create mode 100644 java/com/android/contacts/common/model/account/SamsungAccountType.java create mode 100644 java/com/android/contacts/common/model/dataitem/DataItem.java create mode 100644 java/com/android/contacts/common/model/dataitem/DataKind.java create mode 100644 java/com/android/contacts/common/model/dataitem/EmailDataItem.java create mode 100644 java/com/android/contacts/common/model/dataitem/EventDataItem.java create mode 100644 java/com/android/contacts/common/model/dataitem/GroupMembershipDataItem.java create mode 100644 java/com/android/contacts/common/model/dataitem/IdentityDataItem.java create mode 100644 java/com/android/contacts/common/model/dataitem/ImDataItem.java create mode 100644 java/com/android/contacts/common/model/dataitem/NicknameDataItem.java create mode 100644 java/com/android/contacts/common/model/dataitem/NoteDataItem.java create mode 100644 java/com/android/contacts/common/model/dataitem/OrganizationDataItem.java create mode 100644 java/com/android/contacts/common/model/dataitem/PhoneDataItem.java create mode 100644 java/com/android/contacts/common/model/dataitem/PhotoDataItem.java create mode 100644 java/com/android/contacts/common/model/dataitem/RelationDataItem.java create mode 100644 java/com/android/contacts/common/model/dataitem/SipAddressDataItem.java create mode 100644 java/com/android/contacts/common/model/dataitem/StructuredNameDataItem.java create mode 100644 java/com/android/contacts/common/model/dataitem/StructuredPostalDataItem.java create mode 100644 java/com/android/contacts/common/model/dataitem/WebsiteDataItem.java create mode 100644 java/com/android/contacts/common/preference/ContactsPreferences.java create mode 100644 java/com/android/contacts/common/preference/DisplayOrderPreference.java create mode 100644 java/com/android/contacts/common/preference/SortOrderPreference.java create mode 100644 java/com/android/contacts/common/res/color/popup_menu_color.xml create mode 100644 java/com/android/contacts/common/res/color/tab_text_color.xml create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_ab_search.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_arrow_back_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_business_white_120dp.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_call_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_call_note_white_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_close_dk.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_create_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_group_white_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_history_white_drawable_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_info_outline_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_menu_back.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_menu_group_dk.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_menu_group_lt.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_menu_overflow_lt.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_menu_person_dk.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_menu_person_lt.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_menu_remove_field_holo_light.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_dk.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_holo_light.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_lt.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_message_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_person_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_person_add_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_phone_attach.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_rx_videocam.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_scroll_handle.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_tx_videocam.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_videocam.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_voicemail_avatar.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/list_activated_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/list_background_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/list_focused_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/list_longpressed_holo_light.9.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/list_pressed_holo_light.9.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/list_section_divider_holo_custom.9.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/list_title_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_background_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_focused_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_section_divider_holo_custom.9.png create mode 100644 java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_title_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_background_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_focused_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_section_divider_holo_custom.9.png create mode 100644 java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_title_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-hdpi/list_activated_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-mdpi/list_activated_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-xhdpi/list_activated_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_background_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_focused_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_section_divider_holo_custom.9.png create mode 100644 java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_title_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_ab_search.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_arrow_back_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_business_white_120dp.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_call_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_call_note_white_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_close_dk.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_create_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_group_white_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_history_white_drawable_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_info_outline_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_menu_back.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_menu_group_dk.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_menu_group_lt.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_menu_overflow_lt.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_menu_person_dk.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_menu_person_lt.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_menu_remove_field_holo_light.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_menu_star_dk.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_menu_star_lt.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_message_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_person_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_person_add_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_phone_attach.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_rx_videocam.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_scroll_handle.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_tx_videocam.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_videocam.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_voicemail_avatar.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/list_activated_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/list_background_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/list_focused_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/list_longpressed_holo_light.9.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/list_pressed_holo_light.9.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/list_section_divider_holo_custom.9.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/list_title_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-sw600dp-hdpi/list_activated_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-sw600dp-mdpi/list_activated_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-sw600dp-xhdpi/list_activated_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_ab_search.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_arrow_back_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_business_white_120dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_call_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_call_note_white_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_close_dk.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_create_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_group_white_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_history_white_drawable_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_info_outline_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_back.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_group_dk.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_group_lt.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_overflow_lt.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_person_dk.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_person_lt.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_remove_field_holo_light.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_dk.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_holo_light.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_lt.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_message_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_person_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_person_add_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_phone_attach.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_rx_videocam.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_scroll_handle.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_tx_videocam.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_videocam.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_voicemail_avatar.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/list_activated_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/list_background_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/list_focused_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/list_longpressed_holo_light.9.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/list_pressed_holo_light.9.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/list_section_divider_holo_custom.9.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/list_title_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_ab_search.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_arrow_back_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_business_white_120dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_call_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_call_note_white_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_close_dk.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_create_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_group_white_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_history_white_drawable_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_info_outline_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_back.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_group_dk.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_group_lt.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_overflow_lt.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_person_dk.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_person_lt.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_remove_field_holo_light.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_dk.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_holo_light.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_lt.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_message_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_person_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_person_add_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_phone_attach.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_rx_videocam.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_scroll_handle.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_tx_videocam.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_videocam.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_voicemail_avatar.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/list_activated_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/list_focused_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/list_longpressed_holo_light.9.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/list_pressed_holo_light.9.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/list_title_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-xxxhdpi/ic_ab_search.png create mode 100644 java/com/android/contacts/common/res/drawable-xxxhdpi/ic_arrow_back_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xxxhdpi/ic_business_white_120dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xxxhdpi/ic_call_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xxxhdpi/ic_call_note_white_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xxxhdpi/ic_close_dk.png create mode 100644 java/com/android/contacts/common/res/drawable-xxxhdpi/ic_create_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xxxhdpi/ic_history_white_drawable_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xxxhdpi/ic_info_outline_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xxxhdpi/ic_message_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xxxhdpi/ic_person_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xxxhdpi/ic_person_add_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xxxhdpi/ic_phone_attach.png create mode 100644 java/com/android/contacts/common/res/drawable-xxxhdpi/ic_scroll_handle.png create mode 100644 java/com/android/contacts/common/res/drawable-xxxhdpi/ic_videocam.png create mode 100644 java/com/android/contacts/common/res/drawable-xxxhdpi/ic_voicemail_avatar.png create mode 100644 java/com/android/contacts/common/res/drawable/dialog_background_material.xml create mode 100644 java/com/android/contacts/common/res/drawable/fastscroll_thumb.xml create mode 100644 java/com/android/contacts/common/res/drawable/ic_back_arrow.xml create mode 100644 java/com/android/contacts/common/res/drawable/ic_call.xml create mode 100644 java/com/android/contacts/common/res/drawable/ic_message_24dp.xml create mode 100644 java/com/android/contacts/common/res/drawable/ic_more_vert.xml create mode 100644 java/com/android/contacts/common/res/drawable/ic_person_add_tinted_24dp.xml create mode 100644 java/com/android/contacts/common/res/drawable/ic_scroll_handle_default.xml create mode 100644 java/com/android/contacts/common/res/drawable/ic_scroll_handle_pressed.xml create mode 100644 java/com/android/contacts/common/res/drawable/ic_search_add_contact.xml create mode 100644 java/com/android/contacts/common/res/drawable/ic_search_video_call.xml create mode 100644 java/com/android/contacts/common/res/drawable/ic_tab_all.xml create mode 100644 java/com/android/contacts/common/res/drawable/ic_tab_groups.xml create mode 100644 java/com/android/contacts/common/res/drawable/ic_tab_starred.xml create mode 100644 java/com/android/contacts/common/res/drawable/ic_work_profile.xml create mode 100644 java/com/android/contacts/common/res/drawable/item_background_material_borderless_dark.xml create mode 100644 java/com/android/contacts/common/res/drawable/item_background_material_dark.xml create mode 100644 java/com/android/contacts/common/res/drawable/item_background_material_light.xml create mode 100644 java/com/android/contacts/common/res/drawable/list_item_activated_background.xml create mode 100644 java/com/android/contacts/common/res/drawable/list_selector_background_transition_holo_light.xml create mode 100644 java/com/android/contacts/common/res/drawable/searchedittext_custom_cursor.xml create mode 100644 java/com/android/contacts/common/res/drawable/unread_count_background.xml create mode 100644 java/com/android/contacts/common/res/drawable/view_pager_tab_background.xml create mode 100644 java/com/android/contacts/common/res/layout-ldrtl/unread_count_tab.xml create mode 100644 java/com/android/contacts/common/res/layout/account_filter_header.xml create mode 100644 java/com/android/contacts/common/res/layout/account_selector_list_item.xml create mode 100644 java/com/android/contacts/common/res/layout/account_selector_list_item_condensed.xml create mode 100644 java/com/android/contacts/common/res/layout/call_subject_history.xml create mode 100644 java/com/android/contacts/common/res/layout/call_subject_history_list_item.xml create mode 100644 java/com/android/contacts/common/res/layout/contact_detail_list_padding.xml create mode 100644 java/com/android/contacts/common/res/layout/contact_list_card.xml create mode 100644 java/com/android/contacts/common/res/layout/contact_list_content.xml create mode 100644 java/com/android/contacts/common/res/layout/default_account_checkbox.xml create mode 100644 java/com/android/contacts/common/res/layout/dialog_call_subject.xml create mode 100644 java/com/android/contacts/common/res/layout/directory_header.xml create mode 100644 java/com/android/contacts/common/res/layout/list_separator.xml create mode 100644 java/com/android/contacts/common/res/layout/search_bar_expanded.xml create mode 100644 java/com/android/contacts/common/res/layout/select_account_list_item.xml create mode 100644 java/com/android/contacts/common/res/layout/unread_count_tab.xml create mode 100644 java/com/android/contacts/common/res/mipmap-hdpi/ic_contacts_launcher.png create mode 100644 java/com/android/contacts/common/res/mipmap-mdpi/ic_contacts_launcher.png create mode 100644 java/com/android/contacts/common/res/mipmap-xhdpi/ic_contacts_launcher.png create mode 100644 java/com/android/contacts/common/res/mipmap-xxhdpi/ic_contacts_launcher.png create mode 100644 java/com/android/contacts/common/res/mipmap-xxxhdpi/ic_contacts_launcher.png create mode 100644 java/com/android/contacts/common/res/values-ja/donottranslate_config.xml create mode 100644 java/com/android/contacts/common/res/values-ko/donottranslate_config.xml create mode 100644 java/com/android/contacts/common/res/values-land/integers.xml create mode 100644 java/com/android/contacts/common/res/values-sw600dp-land/integers.xml create mode 100644 java/com/android/contacts/common/res/values-sw600dp/dimens.xml create mode 100644 java/com/android/contacts/common/res/values-sw600dp/integers.xml create mode 100644 java/com/android/contacts/common/res/values-sw720dp-land/integers.xml create mode 100644 java/com/android/contacts/common/res/values-sw720dp/integers.xml create mode 100644 java/com/android/contacts/common/res/values-zh-rCN/donottranslate_config.xml create mode 100644 java/com/android/contacts/common/res/values-zh-rTW/donottranslate_config.xml create mode 100644 java/com/android/contacts/common/res/values/animation_constants.xml create mode 100644 java/com/android/contacts/common/res/values/attrs.xml create mode 100644 java/com/android/contacts/common/res/values/colors.xml create mode 100644 java/com/android/contacts/common/res/values/dimens.xml create mode 100644 java/com/android/contacts/common/res/values/donottranslate_config.xml create mode 100644 java/com/android/contacts/common/res/values/ids.xml create mode 100644 java/com/android/contacts/common/res/values/integers.xml create mode 100644 java/com/android/contacts/common/res/values/strings.xml create mode 100644 java/com/android/contacts/common/res/values/styles.xml create mode 100644 java/com/android/contacts/common/testing/InjectedServices.java create mode 100644 java/com/android/contacts/common/util/AccountFilterUtil.java create mode 100644 java/com/android/contacts/common/util/BitmapUtil.java create mode 100644 java/com/android/contacts/common/util/CommonDateUtils.java create mode 100644 java/com/android/contacts/common/util/Constants.java create mode 100644 java/com/android/contacts/common/util/ContactDisplayUtils.java create mode 100644 java/com/android/contacts/common/util/ContactListViewUtils.java create mode 100644 java/com/android/contacts/common/util/ContactLoaderUtils.java create mode 100644 java/com/android/contacts/common/util/DateUtils.java create mode 100644 java/com/android/contacts/common/util/FabUtil.java create mode 100644 java/com/android/contacts/common/util/MaterialColorMapUtils.java create mode 100644 java/com/android/contacts/common/util/NameConverter.java create mode 100644 java/com/android/contacts/common/util/SearchUtil.java create mode 100644 java/com/android/contacts/common/util/StopWatch.java create mode 100644 java/com/android/contacts/common/util/TelephonyManagerUtils.java create mode 100644 java/com/android/contacts/common/util/TrafficStatsTags.java create mode 100644 java/com/android/contacts/common/util/UriUtils.java create mode 100644 java/com/android/contacts/common/widget/ActivityTouchLinearLayout.java create mode 100644 java/com/android/contacts/common/widget/FloatingActionButtonController.java create mode 100644 java/com/android/contacts/common/widget/LayoutSuppressingImageView.java create mode 100644 java/com/android/contacts/common/widget/SelectPhoneAccountDialogFragment.java create mode 100644 java/com/android/dialer/animation/AnimUtils.java create mode 100644 java/com/android/dialer/animation/AnimationListenerAdapter.java create mode 100644 java/com/android/dialer/app/AndroidManifest.xml create mode 100644 java/com/android/dialer/app/Bindings.java create mode 100644 java/com/android/dialer/app/CallDetailActivity.java create mode 100644 java/com/android/dialer/app/DialerApplication.java create mode 100644 java/com/android/dialer/app/DialtactsActivity.java create mode 100644 java/com/android/dialer/app/FloatingActionButtonBehavior.java create mode 100644 java/com/android/dialer/app/PhoneCallDetails.java create mode 100644 java/com/android/dialer/app/SpecialCharSequenceMgr.java create mode 100644 java/com/android/dialer/app/alert/AlertManager.java create mode 100644 java/com/android/dialer/app/bindings/DialerBindings.java create mode 100644 java/com/android/dialer/app/bindings/DialerBindingsFactory.java create mode 100644 java/com/android/dialer/app/bindings/DialerBindingsStub.java create mode 100644 java/com/android/dialer/app/calllog/BlockReportSpamListener.java create mode 100644 java/com/android/dialer/app/calllog/CallDetailHistoryAdapter.java create mode 100644 java/com/android/dialer/app/calllog/CallLogAdapter.java create mode 100644 java/com/android/dialer/app/calllog/CallLogAlertManager.java create mode 100644 java/com/android/dialer/app/calllog/CallLogAsync.java create mode 100644 java/com/android/dialer/app/calllog/CallLogAsyncTaskUtil.java create mode 100644 java/com/android/dialer/app/calllog/CallLogFragment.java create mode 100644 java/com/android/dialer/app/calllog/CallLogGroupBuilder.java create mode 100644 java/com/android/dialer/app/calllog/CallLogListItemHelper.java create mode 100644 java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java create mode 100644 java/com/android/dialer/app/calllog/CallLogModalAlertManager.java create mode 100644 java/com/android/dialer/app/calllog/CallLogNotificationsHelper.java create mode 100644 java/com/android/dialer/app/calllog/CallLogNotificationsService.java create mode 100644 java/com/android/dialer/app/calllog/CallLogReceiver.java create mode 100644 java/com/android/dialer/app/calllog/CallTypeHelper.java create mode 100644 java/com/android/dialer/app/calllog/CallTypeIconsView.java create mode 100644 java/com/android/dialer/app/calllog/ClearCallLogDialog.java create mode 100644 java/com/android/dialer/app/calllog/DefaultVoicemailNotifier.java create mode 100644 java/com/android/dialer/app/calllog/GroupingListAdapter.java create mode 100644 java/com/android/dialer/app/calllog/IntentProvider.java create mode 100644 java/com/android/dialer/app/calllog/MissedCallNotificationReceiver.java create mode 100644 java/com/android/dialer/app/calllog/MissedCallNotifier.java create mode 100644 java/com/android/dialer/app/calllog/PhoneAccountUtils.java create mode 100644 java/com/android/dialer/app/calllog/PhoneCallDetailsHelper.java create mode 100644 java/com/android/dialer/app/calllog/PhoneCallDetailsViews.java create mode 100644 java/com/android/dialer/app/calllog/PhoneNumberDisplayUtil.java create mode 100644 java/com/android/dialer/app/calllog/VisualVoicemailCallLogFragment.java create mode 100644 java/com/android/dialer/app/calllog/VoicemailQueryHandler.java create mode 100644 java/com/android/dialer/app/calllog/calllogcache/CallLogCache.java create mode 100644 java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipop.java create mode 100644 java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipopMr1.java create mode 100644 java/com/android/dialer/app/contactinfo/ContactInfoCache.java create mode 100644 java/com/android/dialer/app/contactinfo/ContactInfoRequest.java create mode 100644 java/com/android/dialer/app/contactinfo/ContactPhotoLoader.java create mode 100644 java/com/android/dialer/app/contactinfo/ExpirableCacheHeadlessFragment.java create mode 100644 java/com/android/dialer/app/contactinfo/NumberWithCountryIso.java create mode 100644 java/com/android/dialer/app/dialpad/DialpadFragment.java create mode 100644 java/com/android/dialer/app/dialpad/PseudoEmergencyAnimator.java create mode 100644 java/com/android/dialer/app/dialpad/SmartDialCursorLoader.java create mode 100644 java/com/android/dialer/app/dialpad/UnicodeDialerKeyListener.java create mode 100644 java/com/android/dialer/app/filterednumber/BlockedNumbersAdapter.java create mode 100644 java/com/android/dialer/app/filterednumber/BlockedNumbersFragment.java create mode 100644 java/com/android/dialer/app/filterednumber/BlockedNumbersSettingsActivity.java create mode 100644 java/com/android/dialer/app/filterednumber/NumbersAdapter.java create mode 100644 java/com/android/dialer/app/filterednumber/ViewNumbersToImportAdapter.java create mode 100644 java/com/android/dialer/app/filterednumber/ViewNumbersToImportFragment.java create mode 100644 java/com/android/dialer/app/legacybindings/DialerLegacyBindings.java create mode 100644 java/com/android/dialer/app/legacybindings/DialerLegacyBindingsFactory.java create mode 100644 java/com/android/dialer/app/legacybindings/DialerLegacyBindingsStub.java create mode 100644 java/com/android/dialer/app/list/AllContactsFragment.java create mode 100644 java/com/android/dialer/app/list/BlockedListSearchAdapter.java create mode 100644 java/com/android/dialer/app/list/BlockedListSearchFragment.java create mode 100644 java/com/android/dialer/app/list/ContentChangedFilter.java create mode 100644 java/com/android/dialer/app/list/DialerPhoneNumberListAdapter.java create mode 100644 java/com/android/dialer/app/list/DragDropController.java create mode 100644 java/com/android/dialer/app/list/ListsFragment.java create mode 100644 java/com/android/dialer/app/list/OnDragDropListener.java create mode 100644 java/com/android/dialer/app/list/OnListFragmentScrolledListener.java create mode 100644 java/com/android/dialer/app/list/PhoneFavoriteListView.java create mode 100644 java/com/android/dialer/app/list/PhoneFavoriteSquareTileView.java create mode 100644 java/com/android/dialer/app/list/PhoneFavoriteTileView.java create mode 100644 java/com/android/dialer/app/list/PhoneFavoritesTileAdapter.java create mode 100644 java/com/android/dialer/app/list/RegularSearchFragment.java create mode 100644 java/com/android/dialer/app/list/RegularSearchListAdapter.java create mode 100644 java/com/android/dialer/app/list/RemoveView.java create mode 100644 java/com/android/dialer/app/list/SearchFragment.java create mode 100644 java/com/android/dialer/app/list/SmartDialNumberListAdapter.java create mode 100644 java/com/android/dialer/app/list/SmartDialSearchFragment.java create mode 100644 java/com/android/dialer/app/list/SpeedDialFragment.java create mode 100644 java/com/android/dialer/app/manifests/activities/AndroidManifest.xml create mode 100644 java/com/android/dialer/app/res/color/settings_text_color_primary.xml create mode 100644 java/com/android/dialer/app/res/color/settings_text_color_secondary.xml create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/empty_call_log.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/empty_contacts.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/empty_speed_dial.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/fab_ic_dial.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_archive_white_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_call_arrow.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_content_copy_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_delete_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_dialer_fork_add_call.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_dialer_fork_current_call.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_dialer_fork_tt_keypad.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_grade_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_handle.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_menu_history_lt.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_mic_grey600.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_more_vert_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_not_interested_googblue_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_not_spam.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_pause_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_people_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_phone_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_play_arrow_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_remove.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_results_phone.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_schedule_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_share_white_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_star.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_unblock.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_off_dis.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_off_dk.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_on_dis.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_on_dk.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_voicemail_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_volume_down_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_volume_up_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/search_shadow.9.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/shadow_contact_photo.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/empty_call_log.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/empty_contacts.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/empty_speed_dial.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/fab_ic_dial.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_archive_white_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_call_arrow.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_content_copy_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_delete_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_dialer_fork_add_call.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_dialer_fork_current_call.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_dialer_fork_tt_keypad.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_grade_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_handle.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_menu_history_lt.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_mic_grey600.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_more_vert_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_not_interested_googblue_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_not_spam.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_pause_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_people_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_phone_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_play_arrow_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_remove.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_results_phone.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_schedule_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_share_white_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_star.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_unblock.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_off_dis.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_off_dk.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_on_dis.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_on_dk.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_voicemail_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_volume_down_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_volume_up_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/search_shadow.9.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/shadow_contact_photo.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/empty_call_log.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/empty_contacts.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/empty_speed_dial.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/fab_ic_dial.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_archive_white_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_call_arrow.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_content_copy_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_delete_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_dialer_fork_add_call.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_dialer_fork_current_call.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_dialer_fork_tt_keypad.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_grade_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_handle.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_menu_history_lt.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_mic_grey600.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_more_vert_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_not_interested_googblue_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_not_spam.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_pause_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_people_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_phone_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_play_arrow_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_remove.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_results_phone.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_schedule_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_share_white_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_star.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_unblock.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_off_dis.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_off_dk.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_on_dis.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_on_dk.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_voicemail_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_volume_down_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_volume_up_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/search_shadow.9.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/shadow_contact_photo.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/empty_call_log.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/empty_contacts.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/empty_speed_dial.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/fab_ic_dial.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_archive_white_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_call_arrow.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_content_copy_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_delete_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_dialer_fork_add_call.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_dialer_fork_current_call.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_dialer_fork_tt_keypad.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_grade_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_handle.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_menu_history_lt.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_mic_grey600.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_more_vert_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_not_interested_googblue_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_not_spam.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_pause_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_people_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_phone_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_play_arrow_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_remove.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_results_phone.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_schedule_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_share_white_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_star.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_unblock.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_off_dis.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_off_dk.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_on_dis.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_on_dk.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_voicemail_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_volume_down_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_volume_up_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/search_shadow.9.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/shadow_contact_photo.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/empty_call_log.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/empty_contacts.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/fab_ic_dial.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_archive_white_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_call_arrow.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_content_copy_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_delete_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_grade_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_handle.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_mic_grey600.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_more_vert_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_not_interested_googblue_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_not_spam.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_pause_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_people_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_phone_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_play_arrow_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_results_phone.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_schedule_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_share_white_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_unblock.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_voicemail_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_volume_down_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_volume_up_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable/background_dial_holo_dark.xml create mode 100644 java/com/android/dialer/app/res/drawable/floating_action_button.xml create mode 100644 java/com/android/dialer/app/res/drawable/ic_call_detail_content_copy.xml create mode 100644 java/com/android/dialer/app/res/drawable/ic_call_detail_edit.xml create mode 100644 java/com/android/dialer/app/res/drawable/ic_call_detail_report.xml create mode 100644 java/com/android/dialer/app/res/drawable/ic_call_detail_unblock.xml create mode 100644 java/com/android/dialer/app/res/drawable/ic_pause.xml create mode 100644 java/com/android/dialer/app/res/drawable/ic_play_arrow.xml create mode 100644 java/com/android/dialer/app/res/drawable/ic_search_phone.xml create mode 100644 java/com/android/dialer/app/res/drawable/ic_speakerphone_off.xml create mode 100644 java/com/android/dialer/app/res/drawable/ic_speakerphone_on.xml create mode 100644 java/com/android/dialer/app/res/drawable/ic_voicemail_seek_handle.xml create mode 100644 java/com/android/dialer/app/res/drawable/ic_voicemail_seek_handle_disabled.xml create mode 100644 java/com/android/dialer/app/res/drawable/oval_ripple.xml create mode 100644 java/com/android/dialer/app/res/drawable/overflow_menu.xml create mode 100644 java/com/android/dialer/app/res/drawable/rounded_corner.xml create mode 100644 java/com/android/dialer/app/res/drawable/seekbar_drawable.xml create mode 100644 java/com/android/dialer/app/res/drawable/selectable_primary_flat_button.xml create mode 100644 java/com/android/dialer/app/res/drawable/shadow_fade_left.xml create mode 100644 java/com/android/dialer/app/res/drawable/shadow_fade_up.xml create mode 100644 java/com/android/dialer/app/res/layout-land/dialpad_fragment.xml create mode 100644 java/com/android/dialer/app/res/layout-land/empty_content_view_dialpad_search.xml create mode 100644 java/com/android/dialer/app/res/layout/account_filter_header_for_phone_favorite.xml create mode 100644 java/com/android/dialer/app/res/layout/all_contacts_activity.xml create mode 100644 java/com/android/dialer/app/res/layout/all_contacts_fragment.xml create mode 100644 java/com/android/dialer/app/res/layout/blocked_number_footer.xml create mode 100644 java/com/android/dialer/app/res/layout/blocked_number_fragment.xml create mode 100644 java/com/android/dialer/app/res/layout/blocked_number_header.xml create mode 100644 java/com/android/dialer/app/res/layout/blocked_number_item.xml create mode 100644 java/com/android/dialer/app/res/layout/blocked_numbers_activity.xml create mode 100644 java/com/android/dialer/app/res/layout/call_detail.xml create mode 100644 java/com/android/dialer/app/res/layout/call_detail_footer.xml create mode 100644 java/com/android/dialer/app/res/layout/call_detail_header.xml create mode 100644 java/com/android/dialer/app/res/layout/call_detail_history_item.xml create mode 100644 java/com/android/dialer/app/res/layout/call_log_alert_item.xml create mode 100644 java/com/android/dialer/app/res/layout/call_log_fragment.xml create mode 100644 java/com/android/dialer/app/res/layout/call_log_list_item.xml create mode 100644 java/com/android/dialer/app/res/layout/call_log_list_item_actions.xml create mode 100644 java/com/android/dialer/app/res/layout/dialpad_chooser_list_item.xml create mode 100644 java/com/android/dialer/app/res/layout/dialpad_fragment.xml create mode 100644 java/com/android/dialer/app/res/layout/dialtacts_activity.xml create mode 100644 java/com/android/dialer/app/res/layout/empty_content_view.xml create mode 100644 java/com/android/dialer/app/res/layout/empty_content_view_dialpad_search.xml create mode 100644 java/com/android/dialer/app/res/layout/keyguard_preview.xml create mode 100644 java/com/android/dialer/app/res/layout/lists_fragment.xml create mode 100644 java/com/android/dialer/app/res/layout/phone_favorite_tile_view.xml create mode 100644 java/com/android/dialer/app/res/layout/search_edittext.xml create mode 100644 java/com/android/dialer/app/res/layout/speed_dial_fragment.xml create mode 100644 java/com/android/dialer/app/res/layout/view_numbers_to_import_fragment.xml create mode 100644 java/com/android/dialer/app/res/layout/voicemail_playback_layout.xml create mode 100644 java/com/android/dialer/app/res/menu/dialpad_options.xml create mode 100644 java/com/android/dialer/app/res/menu/dialtacts_options.xml create mode 100644 java/com/android/dialer/app/res/mipmap-hdpi/ic_launcher_phone.png create mode 100644 java/com/android/dialer/app/res/mipmap-mdpi/ic_launcher_phone.png create mode 100644 java/com/android/dialer/app/res/mipmap-xhdpi/ic_launcher_phone.png create mode 100644 java/com/android/dialer/app/res/mipmap-xxhdpi/ic_launcher_phone.png create mode 100644 java/com/android/dialer/app/res/mipmap-xxxhdpi/ic_launcher_phone.png create mode 100644 java/com/android/dialer/app/res/values/animation_constants.xml create mode 100644 java/com/android/dialer/app/res/values/attrs.xml create mode 100644 java/com/android/dialer/app/res/values/colors.xml create mode 100644 java/com/android/dialer/app/res/values/dimens.xml create mode 100644 java/com/android/dialer/app/res/values/donottranslate_config.xml create mode 100644 java/com/android/dialer/app/res/values/ids.xml create mode 100644 java/com/android/dialer/app/res/values/strings.xml create mode 100644 java/com/android/dialer/app/res/values/styles.xml create mode 100644 java/com/android/dialer/app/res/xml/display_options_settings.xml create mode 100644 java/com/android/dialer/app/res/xml/file_paths.xml create mode 100644 java/com/android/dialer/app/res/xml/searchable.xml create mode 100644 java/com/android/dialer/app/res/xml/sound_settings.xml create mode 100644 java/com/android/dialer/app/settings/AppCompatPreferenceActivity.java create mode 100644 java/com/android/dialer/app/settings/DefaultRingtonePreference.java create mode 100644 java/com/android/dialer/app/settings/DialerSettingsActivity.java create mode 100644 java/com/android/dialer/app/settings/DisplayOptionsSettingsFragment.java create mode 100644 java/com/android/dialer/app/settings/SoundSettingsFragment.java create mode 100644 java/com/android/dialer/app/voicemail/VoicemailAudioManager.java create mode 100644 java/com/android/dialer/app/voicemail/VoicemailErrorManager.java create mode 100644 java/com/android/dialer/app/voicemail/VoicemailPlaybackLayout.java create mode 100644 java/com/android/dialer/app/voicemail/VoicemailPlaybackPresenter.java create mode 100644 java/com/android/dialer/app/voicemail/WiredHeadsetManager.java create mode 100644 java/com/android/dialer/app/voicemail/error/AndroidManifest.xml create mode 100644 java/com/android/dialer/app/voicemail/error/OmtpVoicemailMessageCreator.java create mode 100644 java/com/android/dialer/app/voicemail/error/VoicemailErrorAlert.java create mode 100644 java/com/android/dialer/app/voicemail/error/VoicemailErrorMessage.java create mode 100644 java/com/android/dialer/app/voicemail/error/VoicemailErrorMessageCreator.java create mode 100644 java/com/android/dialer/app/voicemail/error/VoicemailStatus.java create mode 100644 java/com/android/dialer/app/voicemail/error/VoicemailStatusCorruptionHandler.java create mode 100644 java/com/android/dialer/app/voicemail/error/VoicemailStatusReader.java create mode 100644 java/com/android/dialer/app/voicemail/error/VoicemailTosMessage.java create mode 100644 java/com/android/dialer/app/voicemail/error/Vvm3VoicemailMessageCreator.java create mode 100644 java/com/android/dialer/app/voicemail/error/res/layout/voicemai_error_message_fragment.xml create mode 100644 java/com/android/dialer/app/voicemail/error/res/layout/voicemail_tos_fragment.xml create mode 100644 java/com/android/dialer/app/voicemail/error/res/values/dimens.xml create mode 100644 java/com/android/dialer/app/voicemail/error/res/values/strings.xml create mode 100644 java/com/android/dialer/app/voicemail/error/res/values/styles.xml create mode 100644 java/com/android/dialer/app/widget/ActionBarController.java create mode 100644 java/com/android/dialer/app/widget/DialpadSearchEmptyContentView.java create mode 100644 java/com/android/dialer/app/widget/EmptyContentView.java create mode 100644 java/com/android/dialer/app/widget/SearchEditTextLayout.java create mode 100644 java/com/android/dialer/backup/AndroidManifest.xml create mode 100644 java/com/android/dialer/backup/DialerBackupAgent.java create mode 100644 java/com/android/dialer/backup/DialerBackupUtils.java create mode 100644 java/com/android/dialer/backup/proto/VoicemailInfo.java create mode 100644 java/com/android/dialer/blocking/AndroidManifest.xml create mode 100644 java/com/android/dialer/blocking/BlockNumberDialogFragment.java create mode 100644 java/com/android/dialer/blocking/BlockReportSpamDialogs.java create mode 100644 java/com/android/dialer/blocking/BlockedNumbersAutoMigrator.java create mode 100644 java/com/android/dialer/blocking/BlockedNumbersMigrator.java create mode 100644 java/com/android/dialer/blocking/FilteredNumberAsyncQueryHandler.java create mode 100644 java/com/android/dialer/blocking/FilteredNumberCompat.java create mode 100644 java/com/android/dialer/blocking/FilteredNumberProvider.java create mode 100644 java/com/android/dialer/blocking/FilteredNumbersUtil.java create mode 100644 java/com/android/dialer/blocking/MigrateBlockedNumbersDialogFragment.java create mode 100644 java/com/android/dialer/blocking/res/drawable-hdpi/ic_block_24dp.png create mode 100644 java/com/android/dialer/blocking/res/drawable-hdpi/ic_report_24dp.png create mode 100644 java/com/android/dialer/blocking/res/drawable-hdpi/ic_report_white_36dp.png create mode 100644 java/com/android/dialer/blocking/res/drawable-mdpi/ic_block_24dp.png create mode 100644 java/com/android/dialer/blocking/res/drawable-mdpi/ic_report_24dp.png create mode 100644 java/com/android/dialer/blocking/res/drawable-mdpi/ic_report_white_36dp.png create mode 100644 java/com/android/dialer/blocking/res/drawable-xhdpi/ic_block_24dp.png create mode 100644 java/com/android/dialer/blocking/res/drawable-xhdpi/ic_report_24dp.png create mode 100644 java/com/android/dialer/blocking/res/drawable-xhdpi/ic_report_white_36dp.png create mode 100644 java/com/android/dialer/blocking/res/drawable-xxhdpi/ic_block_24dp.png create mode 100644 java/com/android/dialer/blocking/res/drawable-xxhdpi/ic_report_24dp.png create mode 100644 java/com/android/dialer/blocking/res/drawable-xxhdpi/ic_report_white_36dp.png create mode 100644 java/com/android/dialer/blocking/res/drawable-xxxhdpi/ic_block_24dp.png create mode 100644 java/com/android/dialer/blocking/res/drawable-xxxhdpi/ic_report_24dp.png create mode 100644 java/com/android/dialer/blocking/res/drawable-xxxhdpi/ic_report_white_36dp.png create mode 100644 java/com/android/dialer/blocking/res/drawable/blocked_contact.xml create mode 100644 java/com/android/dialer/blocking/res/layout/block_report_spam_dialog.xml create mode 100644 java/com/android/dialer/blocking/res/values/colors.xml create mode 100644 java/com/android/dialer/blocking/res/values/dimens.xml create mode 100644 java/com/android/dialer/blocking/res/values/strings.xml create mode 100644 java/com/android/dialer/buildtype/BuildType.java create mode 100644 java/com/android/dialer/buildtype/BuildTypeAccessor.java create mode 100644 java/com/android/dialer/buildtype/dogfood/BuildTypeAccessorImpl.java create mode 100644 java/com/android/dialer/callcomposer/AndroidManifest.xml create mode 100644 java/com/android/dialer/callcomposer/CallComposerActivity.java create mode 100644 java/com/android/dialer/callcomposer/CallComposerFragment.java create mode 100644 java/com/android/dialer/callcomposer/CallComposerPagerAdapter.java create mode 100644 java/com/android/dialer/callcomposer/CameraComposerFragment.java create mode 100644 java/com/android/dialer/callcomposer/GalleryComposerFragment.java create mode 100644 java/com/android/dialer/callcomposer/GalleryCursorLoader.java create mode 100644 java/com/android/dialer/callcomposer/GalleryGridAdapter.java create mode 100644 java/com/android/dialer/callcomposer/GalleryGridItemData.java create mode 100644 java/com/android/dialer/callcomposer/GalleryGridItemView.java create mode 100644 java/com/android/dialer/callcomposer/MessageComposerFragment.java create mode 100644 java/com/android/dialer/callcomposer/camera/AndroidManifest.xml create mode 100644 java/com/android/dialer/callcomposer/camera/CameraManager.java create mode 100644 java/com/android/dialer/callcomposer/camera/CameraPreview.java create mode 100644 java/com/android/dialer/callcomposer/camera/HardwareCameraPreview.java create mode 100644 java/com/android/dialer/callcomposer/camera/ImagePersistTask.java create mode 100644 java/com/android/dialer/callcomposer/camera/SoftwareCameraPreview.java create mode 100644 java/com/android/dialer/callcomposer/camera/camerafocus/AndroidManifest.xml create mode 100644 java/com/android/dialer/callcomposer/camera/camerafocus/FocusIndicator.java create mode 100644 java/com/android/dialer/callcomposer/camera/camerafocus/FocusOverlayManager.java create mode 100644 java/com/android/dialer/callcomposer/camera/camerafocus/OverlayRenderer.java create mode 100644 java/com/android/dialer/callcomposer/camera/camerafocus/PieItem.java create mode 100644 java/com/android/dialer/callcomposer/camera/camerafocus/PieRenderer.java create mode 100644 java/com/android/dialer/callcomposer/camera/camerafocus/RenderOverlay.java create mode 100644 java/com/android/dialer/callcomposer/camera/camerafocus/res/values/dimens.xml create mode 100644 java/com/android/dialer/callcomposer/camera/exif/CountedDataInputStream.java create mode 100644 java/com/android/dialer/callcomposer/camera/exif/ExifData.java create mode 100644 java/com/android/dialer/callcomposer/camera/exif/ExifInterface.java create mode 100644 java/com/android/dialer/callcomposer/camera/exif/ExifInvalidFormatException.java create mode 100644 java/com/android/dialer/callcomposer/camera/exif/ExifParser.java create mode 100644 java/com/android/dialer/callcomposer/camera/exif/ExifReader.java create mode 100644 java/com/android/dialer/callcomposer/camera/exif/ExifTag.java create mode 100644 java/com/android/dialer/callcomposer/camera/exif/IfdData.java create mode 100644 java/com/android/dialer/callcomposer/camera/exif/IfdId.java create mode 100644 java/com/android/dialer/callcomposer/camera/exif/JpegHeader.java create mode 100644 java/com/android/dialer/callcomposer/camera/exif/Rational.java create mode 100644 java/com/android/dialer/callcomposer/cameraui/AndroidManifest.xml create mode 100644 java/com/android/dialer/callcomposer/cameraui/CameraMediaChooserView.java create mode 100644 java/com/android/dialer/callcomposer/cameraui/res/drawable-hdpi/ic_capture.png create mode 100644 java/com/android/dialer/callcomposer/cameraui/res/drawable-mdpi/ic_capture.png create mode 100644 java/com/android/dialer/callcomposer/cameraui/res/drawable-xhdpi/ic_capture.png create mode 100644 java/com/android/dialer/callcomposer/cameraui/res/drawable-xxhdpi/ic_capture.png create mode 100644 java/com/android/dialer/callcomposer/cameraui/res/drawable-xxxhdpi/ic_capture.png create mode 100644 java/com/android/dialer/callcomposer/cameraui/res/drawable/transparent_button_background.xml create mode 100644 java/com/android/dialer/callcomposer/cameraui/res/layout/camera_view.xml create mode 100644 java/com/android/dialer/callcomposer/cameraui/res/values/colors.xml create mode 100644 java/com/android/dialer/callcomposer/cameraui/res/values/dimens.xml create mode 100644 java/com/android/dialer/callcomposer/cameraui/res/values/strings.xml create mode 100644 java/com/android/dialer/callcomposer/nano/CallComposerContact.java create mode 100644 java/com/android/dialer/callcomposer/res/drawable/call_composer_contact_border.xml create mode 100644 java/com/android/dialer/callcomposer/res/drawable/gallery_background.xml create mode 100644 java/com/android/dialer/callcomposer/res/drawable/gallery_grid_checkbox_background.xml create mode 100644 java/com/android/dialer/callcomposer/res/drawable/gallery_grid_item_view_background.xml create mode 100644 java/com/android/dialer/callcomposer/res/drawable/gallery_item_selected_drawable.xml create mode 100644 java/com/android/dialer/callcomposer/res/layout/call_composer_activity.xml create mode 100644 java/com/android/dialer/callcomposer/res/layout/fragment_camera_composer.xml create mode 100644 java/com/android/dialer/callcomposer/res/layout/fragment_gallery_composer.xml create mode 100644 java/com/android/dialer/callcomposer/res/layout/fragment_message_composer.xml create mode 100644 java/com/android/dialer/callcomposer/res/layout/gallery_grid_item_view.xml create mode 100644 java/com/android/dialer/callcomposer/res/layout/permission_view.xml create mode 100644 java/com/android/dialer/callcomposer/res/values/colors.xml create mode 100644 java/com/android/dialer/callcomposer/res/values/dimens.xml create mode 100644 java/com/android/dialer/callcomposer/res/values/strings.xml create mode 100644 java/com/android/dialer/callcomposer/res/values/styles.xml create mode 100644 java/com/android/dialer/callcomposer/util/CopyAndResizeImageTask.java create mode 100644 java/com/android/dialer/callintent/CallIntentBuilder.java create mode 100644 java/com/android/dialer/callintent/CallIntentParser.java create mode 100644 java/com/android/dialer/callintent/Constants.java create mode 100644 java/com/android/dialer/callintent/nano/CallInitiationType.java create mode 100644 java/com/android/dialer/callintent/nano/CallSpecificAppData.java create mode 100644 java/com/android/dialer/common/AndroidManifest.xml create mode 100644 java/com/android/dialer/common/Assert.java create mode 100644 java/com/android/dialer/common/AsyncTaskExecutor.java create mode 100644 java/com/android/dialer/common/AsyncTaskExecutors.java create mode 100644 java/com/android/dialer/common/AutoValue_FallibleAsyncTask_FallibleTaskResult.java create mode 100644 java/com/android/dialer/common/ConfigProvider.java create mode 100644 java/com/android/dialer/common/ConfigProviderBindings.java create mode 100644 java/com/android/dialer/common/ConfigProviderFactory.java create mode 100644 java/com/android/dialer/common/DpUtil.java create mode 100644 java/com/android/dialer/common/FallibleAsyncTask.java create mode 100644 java/com/android/dialer/common/FragmentUtils.java create mode 100644 java/com/android/dialer/common/LogUtil.java create mode 100644 java/com/android/dialer/common/MathUtil.java create mode 100644 java/com/android/dialer/common/NetworkUtil.java create mode 100644 java/com/android/dialer/common/UiUtil.java create mode 100644 java/com/android/dialer/common/res/values/strings.xml create mode 100644 java/com/android/dialer/compat/ActivityCompat.java create mode 100644 java/com/android/dialer/compat/AppCompatConstants.java create mode 100644 java/com/android/dialer/compat/CompatUtils.java create mode 100644 java/com/android/dialer/compat/PathInterpolatorCompat.java create mode 100644 java/com/android/dialer/compat/SdkVersionOverride.java create mode 100644 java/com/android/dialer/constants/Constants.java create mode 100644 java/com/android/dialer/constants/ScheduledJobIds.java create mode 100644 java/com/android/dialer/constants/aospdialer/ConstantsImpl.java create mode 100644 java/com/android/dialer/database/CallLogQueryHandler.java create mode 100644 java/com/android/dialer/database/Database.java create mode 100644 java/com/android/dialer/database/DatabaseBindings.java create mode 100644 java/com/android/dialer/database/DatabaseBindingsFactory.java create mode 100644 java/com/android/dialer/database/DatabaseBindingsStub.java create mode 100644 java/com/android/dialer/database/DialerDatabaseHelper.java create mode 100644 java/com/android/dialer/database/FilteredNumberContract.java create mode 100644 java/com/android/dialer/database/VoicemailStatusQuery.java create mode 100644 java/com/android/dialer/debug/AndroidManifest.xml create mode 100644 java/com/android/dialer/debug/bindings/impl/DebugBindings.java create mode 100644 java/com/android/dialer/debug/impl/AndroidManifest.xml create mode 100644 java/com/android/dialer/debug/impl/DebugConnection.java create mode 100644 java/com/android/dialer/debug/impl/DebugConnectionService.java create mode 100644 java/com/android/dialer/dialpadview/AndroidManifest.xml create mode 100644 java/com/android/dialer/dialpadview/DialpadKeyButton.java create mode 100644 java/com/android/dialer/dialpadview/DialpadTextView.java create mode 100644 java/com/android/dialer/dialpadview/DialpadView.java create mode 100644 java/com/android/dialer/dialpadview/DigitsEditText.java create mode 100644 java/com/android/dialer/dialpadview/res/anim/dialpad_slide_in_bottom.xml create mode 100644 java/com/android/dialer/dialpadview/res/anim/dialpad_slide_in_left.xml create mode 100644 java/com/android/dialer/dialpadview/res/anim/dialpad_slide_in_right.xml create mode 100644 java/com/android/dialer/dialpadview/res/anim/dialpad_slide_out_bottom.xml create mode 100644 java/com/android/dialer/dialpadview/res/anim/dialpad_slide_out_left.xml create mode 100644 java/com/android/dialer/dialpadview/res/anim/dialpad_slide_out_right.xml create mode 100644 java/com/android/dialer/dialpadview/res/drawable-hdpi/dialer_fab.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-hdpi/fab_green.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-hdpi/fab_ic_call.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-hdpi/ic_close_black_24dp.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-hdpi/ic_dialpad_delete.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-hdpi/ic_dialpad_voicemail.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-hdpi/ic_overflow_menu.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-mdpi/dialer_fab.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-mdpi/fab_green.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-mdpi/fab_ic_call.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-mdpi/ic_close_black_24dp.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-mdpi/ic_dialpad_delete.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-mdpi/ic_dialpad_voicemail.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-mdpi/ic_overflow_menu.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xhdpi/dialer_fab.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xhdpi/fab_green.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xhdpi/fab_ic_call.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xhdpi/ic_close_black_24dp.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xhdpi/ic_dialpad_delete.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xhdpi/ic_dialpad_voicemail.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xhdpi/ic_overflow_menu.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xxhdpi/dialer_fab.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xxhdpi/fab_green.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xxhdpi/fab_ic_call.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xxhdpi/ic_close_black_24dp.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xxhdpi/ic_dialpad_delete.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xxhdpi/ic_dialpad_voicemail.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xxhdpi/ic_overflow_menu.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/dialer_fab.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/fab_green.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/fab_ic_call.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/ic_close_black_24dp.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/ic_dialpad_delete.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/ic_dialpad_voicemail.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/ic_overflow_menu.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable/btn_dialpad_key.xml create mode 100644 java/com/android/dialer/dialpadview/res/drawable/dialpad_scrim.xml create mode 100644 java/com/android/dialer/dialpadview/res/layout-land/dialpad_key.xml create mode 100644 java/com/android/dialer/dialpadview/res/layout-land/dialpad_key_one.xml create mode 100644 java/com/android/dialer/dialpadview/res/layout-land/dialpad_key_pound.xml create mode 100644 java/com/android/dialer/dialpadview/res/layout-land/dialpad_key_star.xml create mode 100644 java/com/android/dialer/dialpadview/res/layout-land/dialpad_key_zero.xml create mode 100644 java/com/android/dialer/dialpadview/res/layout/dialpad.xml create mode 100644 java/com/android/dialer/dialpadview/res/layout/dialpad_key.xml create mode 100644 java/com/android/dialer/dialpadview/res/layout/dialpad_key_one.xml create mode 100644 java/com/android/dialer/dialpadview/res/layout/dialpad_key_pound.xml create mode 100644 java/com/android/dialer/dialpadview/res/layout/dialpad_key_star.xml create mode 100644 java/com/android/dialer/dialpadview/res/layout/dialpad_key_zero.xml create mode 100644 java/com/android/dialer/dialpadview/res/layout/dialpad_view.xml create mode 100644 java/com/android/dialer/dialpadview/res/layout/dialpad_view_unthemed.xml create mode 100644 java/com/android/dialer/dialpadview/res/values-land/dimens.xml create mode 100644 java/com/android/dialer/dialpadview/res/values-land/styles.xml create mode 100644 java/com/android/dialer/dialpadview/res/values/animation_constants.xml create mode 100644 java/com/android/dialer/dialpadview/res/values/attrs.xml create mode 100644 java/com/android/dialer/dialpadview/res/values/colors.xml create mode 100644 java/com/android/dialer/dialpadview/res/values/dimens.xml create mode 100644 java/com/android/dialer/dialpadview/res/values/strings.xml create mode 100644 java/com/android/dialer/dialpadview/res/values/styles.xml create mode 100644 java/com/android/dialer/disabled_lint_checks.txt create mode 100644 java/com/android/dialer/enrichedcall/AutoValue_EnrichedCallCapabilities.java create mode 100644 java/com/android/dialer/enrichedcall/AutoValue_OutgoingCallComposerData.java create mode 100644 java/com/android/dialer/enrichedcall/EnrichedCallCapabilities.java create mode 100644 java/com/android/dialer/enrichedcall/EnrichedCallManager.java create mode 100644 java/com/android/dialer/enrichedcall/EnrichedCallManagerStub.java create mode 100644 java/com/android/dialer/enrichedcall/OutgoingCallComposerData.java create mode 100644 java/com/android/dialer/enrichedcall/Session.java create mode 100644 java/com/android/dialer/enrichedcall/StubEnrichedCallModule.java create mode 100644 java/com/android/dialer/enrichedcall/extensions/StateExtension.java create mode 100644 java/com/android/dialer/inject/ApplicationModule.java create mode 100644 java/com/android/dialer/inject/DialerAppComponent.java create mode 100644 java/com/android/dialer/interactions/AndroidManifest.xml create mode 100644 java/com/android/dialer/interactions/ContactUpdateService.java create mode 100644 java/com/android/dialer/interactions/PhoneNumberInteraction.java create mode 100644 java/com/android/dialer/interactions/UndemoteOutgoingCallReceiver.java create mode 100644 java/com/android/dialer/interactions/res/layout/phone_disambig_item.xml create mode 100644 java/com/android/dialer/interactions/res/layout/set_primary_checkbox.xml create mode 100644 java/com/android/dialer/interactions/res/values/strings.xml create mode 100644 java/com/android/dialer/logging/Logger.java create mode 100644 java/com/android/dialer/logging/LoggingBindings.java create mode 100644 java/com/android/dialer/logging/LoggingBindingsFactory.java create mode 100644 java/com/android/dialer/logging/LoggingBindingsStub.java create mode 100644 java/com/android/dialer/logging/nano/ContactLookupResult.java create mode 100644 java/com/android/dialer/logging/nano/ContactSource.java create mode 100644 java/com/android/dialer/logging/nano/DialerImpression.java create mode 100644 java/com/android/dialer/logging/nano/InteractionEvent.java create mode 100644 java/com/android/dialer/logging/nano/ReportingLocation.java create mode 100644 java/com/android/dialer/logging/nano/ScreenEvent.java create mode 100644 java/com/android/dialer/multimedia/AutoValue_MultimediaData.java create mode 100644 java/com/android/dialer/multimedia/MultimediaData.java create mode 100644 java/com/android/dialer/p13n/inference/P13nRanking.java create mode 100644 java/com/android/dialer/p13n/inference/protocol/P13nRanker.java create mode 100644 java/com/android/dialer/p13n/inference/protocol/P13nRankerFactory.java create mode 100644 java/com/android/dialer/p13n/logging/P13nLogger.java create mode 100644 java/com/android/dialer/p13n/logging/P13nLoggerFactory.java create mode 100644 java/com/android/dialer/p13n/logging/P13nLogging.java create mode 100644 java/com/android/dialer/phonenumbercache/CachedNumberLookupService.java create mode 100644 java/com/android/dialer/phonenumbercache/CallLogQuery.java create mode 100644 java/com/android/dialer/phonenumbercache/ContactInfo.java create mode 100644 java/com/android/dialer/phonenumbercache/ContactInfoHelper.java create mode 100644 java/com/android/dialer/phonenumbercache/PhoneLookupUtil.java create mode 100644 java/com/android/dialer/phonenumbercache/PhoneNumberCache.java create mode 100644 java/com/android/dialer/phonenumbercache/PhoneNumberCacheBindings.java create mode 100644 java/com/android/dialer/phonenumbercache/PhoneNumberCacheBindingsFactory.java create mode 100644 java/com/android/dialer/phonenumbercache/PhoneNumberCacheBindingsStub.java create mode 100644 java/com/android/dialer/phonenumbercache/PhoneQuery.java create mode 100644 java/com/android/dialer/phonenumberutil/AndroidManifest.xml create mode 100644 java/com/android/dialer/phonenumberutil/PhoneNumberHelper.java create mode 100644 java/com/android/dialer/phonenumberutil/res/values/strings.xml create mode 100644 java/com/android/dialer/proguard/UsedByReflection.java create mode 100644 java/com/android/dialer/protos/ProtoParsers.java create mode 100644 java/com/android/dialer/shortcuts/AndroidManifest.xml create mode 100644 java/com/android/dialer/shortcuts/AutoValue_DialerShortcut.java create mode 100644 java/com/android/dialer/shortcuts/CallContactActivity.java create mode 100644 java/com/android/dialer/shortcuts/DialerShortcut.java create mode 100644 java/com/android/dialer/shortcuts/DynamicShortcuts.java create mode 100644 java/com/android/dialer/shortcuts/IconFactory.java create mode 100644 java/com/android/dialer/shortcuts/PeriodicJobService.java create mode 100644 java/com/android/dialer/shortcuts/PinnedShortcuts.java create mode 100644 java/com/android/dialer/shortcuts/RefreshShortcutsTask.java create mode 100644 java/com/android/dialer/shortcuts/ShortcutInfoFactory.java create mode 100644 java/com/android/dialer/shortcuts/ShortcutRefresher.java create mode 100644 java/com/android/dialer/shortcuts/ShortcutUsageReporter.java create mode 100644 java/com/android/dialer/shortcuts/Shortcuts.java create mode 100644 java/com/android/dialer/shortcuts/ShortcutsJobScheduler.java create mode 100644 java/com/android/dialer/shortcuts/res/drawable/ic_shortcut_add_contact.xml create mode 100644 java/com/android/dialer/shortcuts/res/values/colors.xml create mode 100644 java/com/android/dialer/shortcuts/res/values/dimens.xml create mode 100644 java/com/android/dialer/shortcuts/res/values/strings.xml create mode 100644 java/com/android/dialer/shortcuts/res/values/themes.xml create mode 100644 java/com/android/dialer/shortcuts/res/xml/shortcuts.xml create mode 100644 java/com/android/dialer/simulator/Simulator.java create mode 100644 java/com/android/dialer/simulator/impl/AndroidManifest.xml create mode 100644 java/com/android/dialer/simulator/impl/AutoValue_SimulatorCallLog_CallEntry.java create mode 100644 java/com/android/dialer/simulator/impl/AutoValue_SimulatorContacts_Contact.java create mode 100644 java/com/android/dialer/simulator/impl/AutoValue_SimulatorVoicemail_Voicemail.java create mode 100644 java/com/android/dialer/simulator/impl/SimulatorActionProvider.java create mode 100644 java/com/android/dialer/simulator/impl/SimulatorCallLog.java create mode 100644 java/com/android/dialer/simulator/impl/SimulatorConnection.java create mode 100644 java/com/android/dialer/simulator/impl/SimulatorConnectionService.java create mode 100644 java/com/android/dialer/simulator/impl/SimulatorContacts.java create mode 100644 java/com/android/dialer/simulator/impl/SimulatorModule.java create mode 100644 java/com/android/dialer/simulator/impl/SimulatorVoiceCall.java create mode 100644 java/com/android/dialer/simulator/impl/SimulatorVoicemail.java create mode 100644 java/com/android/dialer/smartdial/LatinSmartDialMap.java create mode 100644 java/com/android/dialer/smartdial/SmartDialMap.java create mode 100644 java/com/android/dialer/smartdial/SmartDialMatchPosition.java create mode 100644 java/com/android/dialer/smartdial/SmartDialNameMatcher.java create mode 100644 java/com/android/dialer/smartdial/SmartDialPrefix.java create mode 100644 java/com/android/dialer/spam/Spam.java create mode 100644 java/com/android/dialer/spam/SpamBindings.java create mode 100644 java/com/android/dialer/spam/SpamBindingsFactory.java create mode 100644 java/com/android/dialer/spam/SpamBindingsStub.java create mode 100644 java/com/android/dialer/telecom/TelecomUtil.java create mode 100644 java/com/android/dialer/theme/AndroidManifest.xml create mode 100644 java/com/android/dialer/theme/res/anim/front_back_switch_button_animation.xml create mode 100644 java/com/android/dialer/theme/res/animator/activated_button_elevation.xml create mode 100644 java/com/android/dialer/theme/res/animator/button_elevation.xml create mode 100644 java/com/android/dialer/theme/res/drawable/front_back_switch_button.xml create mode 100644 java/com/android/dialer/theme/res/drawable/front_back_switch_button_animation.xml create mode 100644 java/com/android/dialer/theme/res/values/colors.xml create mode 100644 java/com/android/dialer/theme/res/values/dimens.xml create mode 100644 java/com/android/dialer/theme/res/values/strings.xml create mode 100644 java/com/android/dialer/theme/res/values/styles.xml create mode 100644 java/com/android/dialer/theme/res/values/themes.xml create mode 100644 java/com/android/dialer/util/AndroidManifest.xml create mode 100644 java/com/android/dialer/util/CallUtil.java create mode 100644 java/com/android/dialer/util/DialerUtils.java create mode 100644 java/com/android/dialer/util/DrawableConverter.java create mode 100644 java/com/android/dialer/util/ExpirableCache.java create mode 100644 java/com/android/dialer/util/IntentUtil.java create mode 100644 java/com/android/dialer/util/MoreStrings.java create mode 100644 java/com/android/dialer/util/OrientationUtil.java create mode 100644 java/com/android/dialer/util/PermissionsUtil.java create mode 100644 java/com/android/dialer/util/SettingsUtil.java create mode 100644 java/com/android/dialer/util/TouchPointManager.java create mode 100644 java/com/android/dialer/util/TransactionSafeActivity.java create mode 100644 java/com/android/dialer/util/ViewUtil.java create mode 100644 java/com/android/dialer/util/res/values/strings.xml create mode 100644 java/com/android/dialer/voicemailstatus/AndroidManifest.xml create mode 100644 java/com/android/dialer/voicemailstatus/VisualVoicemailEnabledChecker.java create mode 100644 java/com/android/dialer/voicemailstatus/VoicemailStatusHelper.java create mode 100644 java/com/android/dialer/voicemailstatus/VoicemailStatusHelperImpl.java create mode 100644 java/com/android/dialer/voicemailstatus/res/values/strings.xml create mode 100644 java/com/android/dialer/widget/AndroidManifest.xml create mode 100644 java/com/android/dialer/widget/ResizingTextEditText.java create mode 100644 java/com/android/dialer/widget/ResizingTextTextView.java create mode 100644 java/com/android/dialer/widget/res/values/attrs.xml create mode 100644 java/com/android/incallui/AccelerometerListener.java create mode 100644 java/com/android/incallui/AndroidManifest.xml create mode 100644 java/com/android/incallui/AnswerScreenPresenter.java create mode 100644 java/com/android/incallui/AnswerScreenPresenterStub.java create mode 100644 java/com/android/incallui/AudioModeProvider.java create mode 100644 java/com/android/incallui/Bindings.java create mode 100644 java/com/android/incallui/CallButtonPresenter.java create mode 100644 java/com/android/incallui/CallCardPresenter.java create mode 100644 java/com/android/incallui/CallerInfo.java create mode 100644 java/com/android/incallui/CallerInfoAsyncQuery.java create mode 100644 java/com/android/incallui/CallerInfoUtils.java create mode 100644 java/com/android/incallui/ConferenceManagerFragment.java create mode 100644 java/com/android/incallui/ConferenceManagerPresenter.java create mode 100644 java/com/android/incallui/ConferenceParticipantListAdapter.java create mode 100644 java/com/android/incallui/ContactInfoCache.java create mode 100644 java/com/android/incallui/ContactsAsyncHelper.java create mode 100644 java/com/android/incallui/ContactsPreferencesFactory.java create mode 100644 java/com/android/incallui/DialpadFragment.java create mode 100644 java/com/android/incallui/DialpadPresenter.java create mode 100644 java/com/android/incallui/ExternalCallNotifier.java create mode 100644 java/com/android/incallui/InCallActivity.java create mode 100644 java/com/android/incallui/InCallActivityCommon.java create mode 100644 java/com/android/incallui/InCallCameraManager.java create mode 100644 java/com/android/incallui/InCallOrientationEventListener.java create mode 100644 java/com/android/incallui/InCallPresenter.java create mode 100644 java/com/android/incallui/InCallServiceImpl.java create mode 100644 java/com/android/incallui/InCallUIMaterialColorMapUtils.java create mode 100644 java/com/android/incallui/Log.java create mode 100644 java/com/android/incallui/ManageConferenceActivity.java create mode 100644 java/com/android/incallui/NotificationBroadcastReceiver.java create mode 100644 java/com/android/incallui/PostCharDialogFragment.java create mode 100644 java/com/android/incallui/ProximitySensor.java create mode 100644 java/com/android/incallui/StatusBarNotifier.java create mode 100644 java/com/android/incallui/ThemeColorManager.java create mode 100644 java/com/android/incallui/TransactionSafeFragmentActivity.java create mode 100644 java/com/android/incallui/VideoCallPresenter.java create mode 100644 java/com/android/incallui/VideoPauseController.java create mode 100644 java/com/android/incallui/answer/bindings/AnswerBindings.java create mode 100644 java/com/android/incallui/answer/impl/AffordanceHolderLayout.java create mode 100644 java/com/android/incallui/answer/impl/AndroidManifest.xml create mode 100644 java/com/android/incallui/answer/impl/AnswerFragment.java create mode 100644 java/com/android/incallui/answer/impl/AnswerVideoCallScreen.java create mode 100644 java/com/android/incallui/answer/impl/CreateCustomSmsDialogFragment.java create mode 100644 java/com/android/incallui/answer/impl/PillDrawable.java create mode 100644 java/com/android/incallui/answer/impl/SmsBottomSheetFragment.java create mode 100644 java/com/android/incallui/answer/impl/affordance/AndroidManifest.xml create mode 100644 java/com/android/incallui/answer/impl/affordance/SwipeButtonHelper.java create mode 100644 java/com/android/incallui/answer/impl/affordance/SwipeButtonView.java create mode 100644 java/com/android/incallui/answer/impl/affordance/res/values/dimens.xml create mode 100644 java/com/android/incallui/answer/impl/answermethod/AndroidManifest.xml create mode 100644 java/com/android/incallui/answer/impl/answermethod/AnswerMethod.java create mode 100644 java/com/android/incallui/answer/impl/answermethod/AnswerMethodFactory.java create mode 100644 java/com/android/incallui/answer/impl/answermethod/AnswerMethodHolder.java create mode 100644 java/com/android/incallui/answer/impl/answermethod/FlingUpDownMethod.java create mode 100644 java/com/android/incallui/answer/impl/answermethod/FlingUpDownTouchHandler.java create mode 100644 java/com/android/incallui/answer/impl/answermethod/TwoButtonMethod.java create mode 100644 java/com/android/incallui/answer/impl/answermethod/res/drawable/call_answer.xml create mode 100644 java/com/android/incallui/answer/impl/answermethod/res/drawable/circular_background.xml create mode 100644 java/com/android/incallui/answer/impl/answermethod/res/layout/swipe_up_down_method.xml create mode 100644 java/com/android/incallui/answer/impl/answermethod/res/layout/two_button_method.xml create mode 100644 java/com/android/incallui/answer/impl/answermethod/res/values-h240dp/values.xml create mode 100644 java/com/android/incallui/answer/impl/answermethod/res/values-h280dp/dimens.xml create mode 100644 java/com/android/incallui/answer/impl/answermethod/res/values-h480dp/dimens.xml create mode 100644 java/com/android/incallui/answer/impl/answermethod/res/values/dimens.xml create mode 100644 java/com/android/incallui/answer/impl/answermethod/res/values/ids.xml create mode 100644 java/com/android/incallui/answer/impl/answermethod/res/values/strings.xml create mode 100644 java/com/android/incallui/answer/impl/answermethod/res/values/styles.xml create mode 100644 java/com/android/incallui/answer/impl/answermethod/res/values/values.xml create mode 100644 java/com/android/incallui/answer/impl/classifier/AccelerationClassifier.java create mode 100644 java/com/android/incallui/answer/impl/classifier/AnglesClassifier.java create mode 100644 java/com/android/incallui/answer/impl/classifier/AnglesPercentageEvaluator.java create mode 100644 java/com/android/incallui/answer/impl/classifier/AnglesVarianceEvaluator.java create mode 100644 java/com/android/incallui/answer/impl/classifier/Classifier.java create mode 100644 java/com/android/incallui/answer/impl/classifier/ClassifierData.java create mode 100644 java/com/android/incallui/answer/impl/classifier/DirectionClassifier.java create mode 100644 java/com/android/incallui/answer/impl/classifier/DirectionEvaluator.java create mode 100644 java/com/android/incallui/answer/impl/classifier/DurationCountClassifier.java create mode 100644 java/com/android/incallui/answer/impl/classifier/DurationCountEvaluator.java create mode 100644 java/com/android/incallui/answer/impl/classifier/EndPointLengthClassifier.java create mode 100644 java/com/android/incallui/answer/impl/classifier/EndPointLengthEvaluator.java create mode 100644 java/com/android/incallui/answer/impl/classifier/EndPointRatioClassifier.java create mode 100644 java/com/android/incallui/answer/impl/classifier/EndPointRatioEvaluator.java create mode 100644 java/com/android/incallui/answer/impl/classifier/FalsingManager.java create mode 100644 java/com/android/incallui/answer/impl/classifier/GestureClassifier.java create mode 100644 java/com/android/incallui/answer/impl/classifier/HistoryEvaluator.java create mode 100644 java/com/android/incallui/answer/impl/classifier/HumanInteractionClassifier.java create mode 100644 java/com/android/incallui/answer/impl/classifier/LengthCountClassifier.java create mode 100644 java/com/android/incallui/answer/impl/classifier/LengthCountEvaluator.java create mode 100644 java/com/android/incallui/answer/impl/classifier/Point.java create mode 100644 java/com/android/incallui/answer/impl/classifier/PointerCountClassifier.java create mode 100644 java/com/android/incallui/answer/impl/classifier/PointerCountEvaluator.java create mode 100644 java/com/android/incallui/answer/impl/classifier/ProximityClassifier.java create mode 100644 java/com/android/incallui/answer/impl/classifier/ProximityEvaluator.java create mode 100644 java/com/android/incallui/answer/impl/classifier/SpeedAnglesClassifier.java create mode 100644 java/com/android/incallui/answer/impl/classifier/SpeedAnglesPercentageEvaluator.java create mode 100644 java/com/android/incallui/answer/impl/classifier/SpeedClassifier.java create mode 100644 java/com/android/incallui/answer/impl/classifier/SpeedEvaluator.java create mode 100644 java/com/android/incallui/answer/impl/classifier/SpeedRatioEvaluator.java create mode 100644 java/com/android/incallui/answer/impl/classifier/SpeedVarianceEvaluator.java create mode 100644 java/com/android/incallui/answer/impl/classifier/Stroke.java create mode 100644 java/com/android/incallui/answer/impl/classifier/StrokeClassifier.java create mode 100644 java/com/android/incallui/answer/impl/hint/AndroidManifest.xml create mode 100644 java/com/android/incallui/answer/impl/hint/AnswerHint.java create mode 100644 java/com/android/incallui/answer/impl/hint/AnswerHintFactory.java create mode 100644 java/com/android/incallui/answer/impl/hint/DotAnswerHint.java create mode 100644 java/com/android/incallui/answer/impl/hint/EmptyAnswerHint.java create mode 100644 java/com/android/incallui/answer/impl/hint/EventAnswerHint.java create mode 100644 java/com/android/incallui/answer/impl/hint/EventPayloadLoader.java create mode 100644 java/com/android/incallui/answer/impl/hint/EventPayloadLoaderImpl.java create mode 100644 java/com/android/incallui/answer/impl/hint/EventSecretCodeListener.java create mode 100644 java/com/android/incallui/answer/impl/hint/res/drawable/answer_hint_large.xml create mode 100644 java/com/android/incallui/answer/impl/hint/res/drawable/answer_hint_mid.xml create mode 100644 java/com/android/incallui/answer/impl/hint/res/drawable/answer_hint_small.xml create mode 100644 java/com/android/incallui/answer/impl/hint/res/layout/dot_hint.xml create mode 100644 java/com/android/incallui/answer/impl/hint/res/layout/event_hint.xml create mode 100644 java/com/android/incallui/answer/impl/hint/res/values/dimens.xml create mode 100644 java/com/android/incallui/answer/impl/hint/res/values/strings.xml create mode 100644 java/com/android/incallui/answer/impl/res/anim/incoming_unlocked_icon_entry.xml create mode 100644 java/com/android/incallui/answer/impl/res/anim/incoming_unlocked_text_entry.xml create mode 100644 java/com/android/incallui/answer/impl/res/layout/fragment_avatar.xml create mode 100644 java/com/android/incallui/answer/impl/res/layout/fragment_custom_sms_dialog.xml create mode 100644 java/com/android/incallui/answer/impl/res/layout/fragment_incoming_call.xml create mode 100644 java/com/android/incallui/answer/impl/res/values-h240dp/dimens.xml create mode 100644 java/com/android/incallui/answer/impl/res/values-h300dp/dimens.xml create mode 100644 java/com/android/incallui/answer/impl/res/values-h480dp/dimens.xml create mode 100644 java/com/android/incallui/answer/impl/res/values-h540dp/dimens.xml create mode 100644 java/com/android/incallui/answer/impl/res/values/dimens.xml create mode 100644 java/com/android/incallui/answer/impl/res/values/strings.xml create mode 100644 java/com/android/incallui/answer/impl/utils/FlingAnimationUtils.java create mode 100644 java/com/android/incallui/answer/impl/utils/Interpolators.java create mode 100644 java/com/android/incallui/answer/protocol/AnswerScreen.java create mode 100644 java/com/android/incallui/answer/protocol/AnswerScreenDelegate.java create mode 100644 java/com/android/incallui/answer/protocol/AnswerScreenDelegateFactory.java create mode 100644 java/com/android/incallui/answerproximitysensor/AnswerProximitySensor.java create mode 100644 java/com/android/incallui/answerproximitysensor/AnswerProximityWakeLock.java create mode 100644 java/com/android/incallui/answerproximitysensor/PseudoProximityWakeLock.java create mode 100644 java/com/android/incallui/answerproximitysensor/PseudoScreenState.java create mode 100644 java/com/android/incallui/answerproximitysensor/SystemProximityWakeLock.java create mode 100644 java/com/android/incallui/async/PausableExecutor.java create mode 100644 java/com/android/incallui/async/PausableExecutorImpl.java create mode 100644 java/com/android/incallui/audioroute/AndroidManifest.xml create mode 100644 java/com/android/incallui/audioroute/AudioRouteSelectorDialogFragment.java create mode 100644 java/com/android/incallui/audioroute/res/drawable-hdpi/ic_phone_audio_grey600_24dp.png create mode 100644 java/com/android/incallui/audioroute/res/drawable-mdpi/ic_phone_audio_grey600_24dp.png create mode 100644 java/com/android/incallui/audioroute/res/drawable-xhdpi/ic_phone_audio_grey600_24dp.png create mode 100644 java/com/android/incallui/audioroute/res/drawable-xxhdpi/ic_phone_audio_grey600_24dp.png create mode 100644 java/com/android/incallui/audioroute/res/layout/audioroute_selector.xml create mode 100644 java/com/android/incallui/audioroute/res/values/strings.xml create mode 100644 java/com/android/incallui/audioroute/res/values/styles.xml create mode 100644 java/com/android/incallui/autoresizetext/AndroidManifest.xml create mode 100644 java/com/android/incallui/autoresizetext/AutoResizeTextView.java create mode 100644 java/com/android/incallui/autoresizetext/res/values/attrs.xml create mode 100644 java/com/android/incallui/baseui/BaseFragment.java create mode 100644 java/com/android/incallui/baseui/Presenter.java create mode 100644 java/com/android/incallui/baseui/Ui.java create mode 100644 java/com/android/incallui/bindings/ContactUtils.java create mode 100644 java/com/android/incallui/bindings/DistanceHelper.java create mode 100644 java/com/android/incallui/bindings/InCallUiBindings.java create mode 100644 java/com/android/incallui/bindings/InCallUiBindingsFactory.java create mode 100644 java/com/android/incallui/bindings/InCallUiBindingsStub.java create mode 100644 java/com/android/incallui/bindings/PhoneNumberService.java create mode 100644 java/com/android/incallui/call/CallList.java create mode 100644 java/com/android/incallui/call/DialerCall.java create mode 100644 java/com/android/incallui/call/DialerCallDelegate.java create mode 100644 java/com/android/incallui/call/DialerCallListener.java create mode 100644 java/com/android/incallui/call/ExternalCallList.java create mode 100644 java/com/android/incallui/call/InCallServiceListener.java create mode 100644 java/com/android/incallui/call/InCallUiLegacyBindings.java create mode 100644 java/com/android/incallui/call/InCallUiLegacyBindingsFactory.java create mode 100644 java/com/android/incallui/call/InCallUiLegacyBindingsStub.java create mode 100644 java/com/android/incallui/call/InCallVideoCallCallback.java create mode 100644 java/com/android/incallui/call/InCallVideoCallCallbackNotifier.java create mode 100644 java/com/android/incallui/call/TelecomAdapter.java create mode 100644 java/com/android/incallui/call/VideoUtils.java create mode 100644 java/com/android/incallui/commontheme/AndroidManifest.xml create mode 100644 java/com/android/incallui/commontheme/res/animator/button_state.xml create mode 100644 java/com/android/incallui/commontheme/res/animator/disabled_alpha.xml create mode 100644 java/com/android/incallui/commontheme/res/color/incall_button_ripple.xml create mode 100644 java/com/android/incallui/commontheme/res/color/incall_button_white.xml create mode 100644 java/com/android/incallui/commontheme/res/drawable-hdpi/ic_phone_audio_white_36dp.png create mode 100644 java/com/android/incallui/commontheme/res/drawable-mdpi/ic_phone_audio_white_36dp.png create mode 100644 java/com/android/incallui/commontheme/res/drawable-xhdpi/ic_phone_audio_white_36dp.png create mode 100644 java/com/android/incallui/commontheme/res/drawable-xxhdpi/ic_phone_audio_white_36dp.png create mode 100644 java/com/android/incallui/commontheme/res/drawable-xxxhdpi/ic_phone_audio_white_36dp.png create mode 100644 java/com/android/incallui/commontheme/res/drawable/answer_answer_background.xml create mode 100644 java/com/android/incallui/commontheme/res/drawable/answer_decline_background.xml create mode 100644 java/com/android/incallui/commontheme/res/drawable/incall_end_call_background.xml create mode 100644 java/com/android/incallui/commontheme/res/values-w260dp-h520dp/dimens.xml create mode 100644 java/com/android/incallui/commontheme/res/values-w520dp-h260dp-land/dimens.xml create mode 100644 java/com/android/incallui/commontheme/res/values/colors.xml create mode 100644 java/com/android/incallui/commontheme/res/values/dimens.xml create mode 100644 java/com/android/incallui/commontheme/res/values/strings.xml create mode 100644 java/com/android/incallui/commontheme/res/values/styles.xml create mode 100644 java/com/android/incallui/contactgrid/AndroidManifest.xml create mode 100644 java/com/android/incallui/contactgrid/BottomRow.java create mode 100644 java/com/android/incallui/contactgrid/ContactGridManager.java create mode 100644 java/com/android/incallui/contactgrid/TopRow.java create mode 100644 java/com/android/incallui/contactgrid/res/layout/incall_contactgrid_bottom_row.xml create mode 100644 java/com/android/incallui/contactgrid/res/layout/incall_contactgrid_top_row.xml create mode 100644 java/com/android/incallui/contactgrid/res/values/ids.xml create mode 100644 java/com/android/incallui/contactgrid/res/values/strings.xml create mode 100644 java/com/android/incallui/hold/AndroidManifest.xml create mode 100644 java/com/android/incallui/hold/OnHoldFragment.java create mode 100644 java/com/android/incallui/hold/res/layout/incall_on_hold_banner.xml create mode 100644 java/com/android/incallui/hold/res/values/strings.xml create mode 100644 java/com/android/incallui/incall/bindings/InCallBindings.java create mode 100644 java/com/android/incallui/incall/impl/AndroidManifest.xml create mode 100644 java/com/android/incallui/incall/impl/AutoValue_MappedButtonConfig_MappingInfo.java create mode 100644 java/com/android/incallui/incall/impl/ButtonChooser.java create mode 100644 java/com/android/incallui/incall/impl/ButtonChooserFactory.java create mode 100644 java/com/android/incallui/incall/impl/ButtonController.java create mode 100644 java/com/android/incallui/incall/impl/CheckableLabeledButton.java create mode 100644 java/com/android/incallui/incall/impl/InCallButtonGridFragment.java create mode 100644 java/com/android/incallui/incall/impl/InCallFragment.java create mode 100644 java/com/android/incallui/incall/impl/InCallPagerAdapter.java create mode 100644 java/com/android/incallui/incall/impl/MappedButtonConfig.java create mode 100644 java/com/android/incallui/incall/impl/res/animator/incall_button_elevation.xml create mode 100644 java/com/android/incallui/incall/impl/res/color/incall_button_icon.xml create mode 100644 java/com/android/incallui/incall/impl/res/drawable-mdpi/ic_addcall_white.png create mode 100644 java/com/android/incallui/incall/impl/res/drawable-xhdpi/ic_addcall_white.png create mode 100644 java/com/android/incallui/incall/impl/res/drawable/incall_button_background.xml create mode 100644 java/com/android/incallui/incall/impl/res/drawable/incall_button_background_checked.xml create mode 100644 java/com/android/incallui/incall/impl/res/drawable/incall_button_background_more.xml create mode 100644 java/com/android/incallui/incall/impl/res/drawable/incall_button_background_unchecked.xml create mode 100644 java/com/android/incallui/incall/impl/res/drawable/incall_ic_add_call.xml create mode 100644 java/com/android/incallui/incall/impl/res/drawable/incall_ic_dialpad.xml create mode 100644 java/com/android/incallui/incall/impl/res/drawable/incall_ic_manage.xml create mode 100644 java/com/android/incallui/incall/impl/res/drawable/incall_ic_merge.xml create mode 100644 java/com/android/incallui/incall/impl/res/drawable/incall_ic_pause.xml create mode 100644 java/com/android/incallui/incall/impl/res/drawable/tab_indicator_default.xml create mode 100644 java/com/android/incallui/incall/impl/res/drawable/tab_indicator_selected.xml create mode 100644 java/com/android/incallui/incall/impl/res/drawable/tab_selector.xml create mode 100644 java/com/android/incallui/incall/impl/res/layout/call_composer_data_fragment.xml create mode 100644 java/com/android/incallui/incall/impl/res/layout/frag_incall_voice.xml create mode 100644 java/com/android/incallui/incall/impl/res/layout/incall_button_grid.xml create mode 100644 java/com/android/incallui/incall/impl/res/values-h320dp/dimens.xml create mode 100644 java/com/android/incallui/incall/impl/res/values-h385dp/dimens.xml create mode 100644 java/com/android/incallui/incall/impl/res/values-h480dp/dimens.xml create mode 100644 java/com/android/incallui/incall/impl/res/values-h580dp/dimens.xml create mode 100644 java/com/android/incallui/incall/impl/res/values-h580dp/styles.xml create mode 100644 java/com/android/incallui/incall/impl/res/values-w260dp-h520dp/dimens.xml create mode 100644 java/com/android/incallui/incall/impl/res/values-w300dp-h540dp/dimens.xml create mode 100644 java/com/android/incallui/incall/impl/res/values/attrs.xml create mode 100644 java/com/android/incallui/incall/impl/res/values/dimens.xml create mode 100644 java/com/android/incallui/incall/impl/res/values/ids.xml create mode 100644 java/com/android/incallui/incall/impl/res/values/strings.xml create mode 100644 java/com/android/incallui/incall/impl/res/values/styles.xml create mode 100644 java/com/android/incallui/incall/protocol/ContactPhotoType.java create mode 100644 java/com/android/incallui/incall/protocol/InCallButtonIds.java create mode 100644 java/com/android/incallui/incall/protocol/InCallButtonIdsExtension.java create mode 100644 java/com/android/incallui/incall/protocol/InCallButtonUi.java create mode 100644 java/com/android/incallui/incall/protocol/InCallButtonUiDelegate.java create mode 100644 java/com/android/incallui/incall/protocol/InCallButtonUiDelegateFactory.java create mode 100644 java/com/android/incallui/incall/protocol/InCallScreen.java create mode 100644 java/com/android/incallui/incall/protocol/InCallScreenDelegate.java create mode 100644 java/com/android/incallui/incall/protocol/InCallScreenDelegateFactory.java create mode 100644 java/com/android/incallui/incall/protocol/PrimaryCallState.java create mode 100644 java/com/android/incallui/incall/protocol/PrimaryInfo.java create mode 100644 java/com/android/incallui/incall/protocol/SecondaryInfo.java create mode 100644 java/com/android/incallui/latencyreport/LatencyReport.java create mode 100644 java/com/android/incallui/legacyblocking/BlockedNumberContentObserver.java create mode 100644 java/com/android/incallui/legacyblocking/DeleteBlockedCallTask.java create mode 100644 java/com/android/incallui/maps/StaticMapBinding.java create mode 100644 java/com/android/incallui/maps/StaticMapFactory.java create mode 100644 java/com/android/incallui/res/anim/activity_open_enter.xml create mode 100644 java/com/android/incallui/res/anim/activity_open_exit.xml create mode 100644 java/com/android/incallui/res/anim/decelerate_cubic.xml create mode 100644 java/com/android/incallui/res/anim/decelerate_quint.xml create mode 100644 java/com/android/incallui/res/anim/on_going_call.xml create mode 100644 java/com/android/incallui/res/color/ota_title_color.xml create mode 100644 java/com/android/incallui/res/drawable-hdpi/ic_block_grey600_24dp.png create mode 100644 java/com/android/incallui/res/drawable-hdpi/ic_call_end_white_24dp.png create mode 100644 java/com/android/incallui/res/drawable-hdpi/ic_call_split_white_24dp.png create mode 100644 java/com/android/incallui/res/drawable-hdpi/ic_close_grey600_24dp.png create mode 100644 java/com/android/incallui/res/drawable-hdpi/ic_location_on_white_24dp.png create mode 100644 java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_01.png create mode 100644 java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_02.png create mode 100644 java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_03.png create mode 100644 java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_04.png create mode 100644 java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_05.png create mode 100644 java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_06.png create mode 100644 java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_07.png create mode 100644 java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_08.png create mode 100644 java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_09.png create mode 100644 java/com/android/incallui/res/drawable-hdpi/ic_person_add_grey600_24dp.png create mode 100644 java/com/android/incallui/res/drawable-hdpi/ic_phone_paused_white_24dp.png create mode 100644 java/com/android/incallui/res/drawable-hdpi/ic_question_mark.png create mode 100644 java/com/android/incallui/res/drawable-hdpi/ic_schedule_white_24dp.png create mode 100644 java/com/android/incallui/res/drawable-hdpi/img_business.png create mode 100644 java/com/android/incallui/res/drawable-hdpi/img_conference.png create mode 100644 java/com/android/incallui/res/drawable-hdpi/img_no_image.png create mode 100644 java/com/android/incallui/res/drawable-hdpi/img_phone.png create mode 100644 java/com/android/incallui/res/drawable-mdpi/ic_block_grey600_24dp.png create mode 100644 java/com/android/incallui/res/drawable-mdpi/ic_call_end_white_24dp.png create mode 100644 java/com/android/incallui/res/drawable-mdpi/ic_call_split_white_24dp.png create mode 100644 java/com/android/incallui/res/drawable-mdpi/ic_close_grey600_24dp.png create mode 100644 java/com/android/incallui/res/drawable-mdpi/ic_location_on_white_24dp.png create mode 100644 java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_01.png create mode 100644 java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_02.png create mode 100644 java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_03.png create mode 100644 java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_04.png create mode 100644 java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_05.png create mode 100644 java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_06.png create mode 100644 java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_07.png create mode 100644 java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_08.png create mode 100644 java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_09.png create mode 100644 java/com/android/incallui/res/drawable-mdpi/ic_person_add_grey600_24dp.png create mode 100644 java/com/android/incallui/res/drawable-mdpi/ic_phone_paused_white_24dp.png create mode 100644 java/com/android/incallui/res/drawable-mdpi/ic_question_mark.png create mode 100644 java/com/android/incallui/res/drawable-mdpi/ic_schedule_white_24dp.png create mode 100644 java/com/android/incallui/res/drawable-mdpi/img_business.png create mode 100644 java/com/android/incallui/res/drawable-mdpi/img_conference.png create mode 100644 java/com/android/incallui/res/drawable-mdpi/img_no_image.png create mode 100644 java/com/android/incallui/res/drawable-mdpi/img_phone.png create mode 100644 java/com/android/incallui/res/drawable-xhdpi/ic_block_grey600_24dp.png create mode 100644 java/com/android/incallui/res/drawable-xhdpi/ic_call_end_white_24dp.png create mode 100644 java/com/android/incallui/res/drawable-xhdpi/ic_call_split_white_24dp.png create mode 100644 java/com/android/incallui/res/drawable-xhdpi/ic_close_grey600_24dp.png create mode 100644 java/com/android/incallui/res/drawable-xhdpi/ic_location_on_white_24dp.png create mode 100644 java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_01.png create mode 100644 java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_02.png create mode 100644 java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_03.png create mode 100644 java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_04.png create mode 100644 java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_05.png create mode 100644 java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_06.png create mode 100644 java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_07.png create mode 100644 java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_08.png create mode 100644 java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_09.png create mode 100644 java/com/android/incallui/res/drawable-xhdpi/ic_person_add_grey600_24dp.png create mode 100644 java/com/android/incallui/res/drawable-xhdpi/ic_phone_paused_white_24dp.png create mode 100644 java/com/android/incallui/res/drawable-xhdpi/ic_question_mark.png create mode 100644 java/com/android/incallui/res/drawable-xhdpi/ic_schedule_white_24dp.png create mode 100644 java/com/android/incallui/res/drawable-xhdpi/img_business.png create mode 100644 java/com/android/incallui/res/drawable-xhdpi/img_conference.png create mode 100644 java/com/android/incallui/res/drawable-xhdpi/img_no_image.png create mode 100644 java/com/android/incallui/res/drawable-xhdpi/img_phone.png create mode 100644 java/com/android/incallui/res/drawable-xxhdpi/ic_block_grey600_24dp.png create mode 100644 java/com/android/incallui/res/drawable-xxhdpi/ic_call_end_white_24dp.png create mode 100644 java/com/android/incallui/res/drawable-xxhdpi/ic_call_split_white_24dp.png create mode 100644 java/com/android/incallui/res/drawable-xxhdpi/ic_close_grey600_24dp.png create mode 100644 java/com/android/incallui/res/drawable-xxhdpi/ic_location_on_white_24dp.png create mode 100644 java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_01.png create mode 100644 java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_02.png create mode 100644 java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_03.png create mode 100644 java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_04.png create mode 100644 java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_05.png create mode 100644 java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_06.png create mode 100644 java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_07.png create mode 100644 java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_08.png create mode 100644 java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_09.png create mode 100644 java/com/android/incallui/res/drawable-xxhdpi/ic_person_add_grey600_24dp.png create mode 100644 java/com/android/incallui/res/drawable-xxhdpi/ic_phone_paused_white_24dp.png create mode 100644 java/com/android/incallui/res/drawable-xxhdpi/ic_question_mark.png create mode 100644 java/com/android/incallui/res/drawable-xxhdpi/ic_schedule_white_24dp.png create mode 100644 java/com/android/incallui/res/drawable-xxhdpi/img_business.png create mode 100644 java/com/android/incallui/res/drawable-xxhdpi/img_conference.png create mode 100644 java/com/android/incallui/res/drawable-xxhdpi/img_no_image.png create mode 100644 java/com/android/incallui/res/drawable-xxhdpi/img_phone.png create mode 100644 java/com/android/incallui/res/drawable-xxxhdpi/ic_block_grey600_24dp.png create mode 100644 java/com/android/incallui/res/drawable-xxxhdpi/ic_call_end_white_24dp.png create mode 100644 java/com/android/incallui/res/drawable-xxxhdpi/ic_call_split_white_24dp.png create mode 100644 java/com/android/incallui/res/drawable-xxxhdpi/ic_close_grey600_24dp.png create mode 100644 java/com/android/incallui/res/drawable-xxxhdpi/ic_location_on_white_24dp.png create mode 100644 java/com/android/incallui/res/drawable-xxxhdpi/ic_person_add_grey600_24dp.png create mode 100644 java/com/android/incallui/res/drawable-xxxhdpi/ic_question_mark.png create mode 100644 java/com/android/incallui/res/drawable-xxxhdpi/ic_schedule_white_24dp.png create mode 100644 java/com/android/incallui/res/drawable-xxxhdpi/img_business.png create mode 100644 java/com/android/incallui/res/drawable-xxxhdpi/img_conference.png create mode 100644 java/com/android/incallui/res/drawable-xxxhdpi/img_no_image.png create mode 100644 java/com/android/incallui/res/drawable-xxxhdpi/img_phone.png create mode 100644 java/com/android/incallui/res/drawable/img_conference_automirrored.xml create mode 100644 java/com/android/incallui/res/drawable/img_no_image_automirrored.xml create mode 100644 java/com/android/incallui/res/drawable/incall_background_gradient.xml create mode 100644 java/com/android/incallui/res/drawable/spam_notification_icon.xml create mode 100644 java/com/android/incallui/res/drawable/unknown_notification_icon.xml create mode 100644 java/com/android/incallui/res/layout/activity_manage_conference.xml create mode 100644 java/com/android/incallui/res/layout/caller_in_conference.xml create mode 100644 java/com/android/incallui/res/layout/conference_manager_fragment.xml create mode 100644 java/com/android/incallui/res/layout/incall_dialpad_fragment.xml create mode 100644 java/com/android/incallui/res/layout/incall_screen.xml create mode 100644 java/com/android/incallui/res/layout/video_call_lte_to_wifi_failed.xml create mode 100644 java/com/android/incallui/res/values-sw360dp/dimens.xml create mode 100644 java/com/android/incallui/res/values-w500dp-land/colors.xml create mode 100644 java/com/android/incallui/res/values-w500dp-land/dimens.xml create mode 100644 java/com/android/incallui/res/values/animation_constants.xml create mode 100644 java/com/android/incallui/res/values/colors.xml create mode 100644 java/com/android/incallui/res/values/config.xml create mode 100644 java/com/android/incallui/res/values/dimens.xml create mode 100644 java/com/android/incallui/res/values/strings.xml create mode 100644 java/com/android/incallui/res/values/styles.xml create mode 100644 java/com/android/incallui/ringtone/DialerRingtoneManager.java create mode 100644 java/com/android/incallui/ringtone/InCallTonePlayer.java create mode 100644 java/com/android/incallui/ringtone/ToneGeneratorFactory.java create mode 100644 java/com/android/incallui/sessiondata/AndroidManifest.xml create mode 100644 java/com/android/incallui/sessiondata/AvatarPresenter.java create mode 100644 java/com/android/incallui/sessiondata/MultimediaFragment.java create mode 100644 java/com/android/incallui/sessiondata/res/drawable/answer_data_background.xml create mode 100644 java/com/android/incallui/sessiondata/res/layout/fragment_composer_frag.xml create mode 100644 java/com/android/incallui/sessiondata/res/layout/fragment_composer_image.xml create mode 100644 java/com/android/incallui/sessiondata/res/layout/fragment_composer_image_frag.xml create mode 100644 java/com/android/incallui/sessiondata/res/layout/fragment_composer_text.xml create mode 100644 java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_frag.xml create mode 100644 java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image.xml create mode 100644 java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image_frag.xml create mode 100644 java/com/android/incallui/sessiondata/res/values/dimens.xml create mode 100644 java/com/android/incallui/sessiondata/res/values/ids.xml create mode 100644 java/com/android/incallui/sessiondata/res/values/styles.xml create mode 100644 java/com/android/incallui/spam/NumberInCallHistoryTask.java create mode 100644 java/com/android/incallui/spam/SpamCallListListener.java create mode 100644 java/com/android/incallui/spam/SpamNotificationActivity.java create mode 100644 java/com/android/incallui/spam/SpamNotificationService.java create mode 100644 java/com/android/incallui/util/AccessibilityUtil.java create mode 100644 java/com/android/incallui/util/TelecomCallUtil.java create mode 100644 java/com/android/incallui/video/bindings/VideoBindings.java create mode 100644 java/com/android/incallui/video/impl/AndroidManifest.xml create mode 100644 java/com/android/incallui/video/impl/CameraPermissionDialogFragment.java create mode 100644 java/com/android/incallui/video/impl/CheckableImageButton.java create mode 100644 java/com/android/incallui/video/impl/SpeakerButtonController.java create mode 100644 java/com/android/incallui/video/impl/SwitchOnHoldCallController.java create mode 100644 java/com/android/incallui/video/impl/VideoCallFragment.java create mode 100644 java/com/android/incallui/video/impl/res/color/videocall_button_icon_tint.xml create mode 100644 java/com/android/incallui/video/impl/res/drawable-hdpi/ic_switch_camera.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_checked.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_checked_disabled.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_checked_pressed.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_default.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_disabled.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_pressed.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-mdpi/ic_switch_camera.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_checked.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_checked_disabled.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_checked_pressed.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_default.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_disabled.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_pressed.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-xhdpi/ic_switch_camera.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_checked.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_checked_disabled.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_checked_pressed.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_default.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_disabled.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_pressed.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-xxhdpi/ic_switch_camera.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_checked.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_checked_disabled.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_checked_pressed.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_default.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_disabled.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_pressed.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-xxxhdpi/ic_switch_camera.png create mode 100644 java/com/android/incallui/video/impl/res/drawable/videocall_background_circle_white.xml create mode 100644 java/com/android/incallui/video/impl/res/drawable/videocall_video_button_background.xml create mode 100644 java/com/android/incallui/video/impl/res/layout-v21/switch_camera_button.xml create mode 100644 java/com/android/incallui/video/impl/res/layout/frag_videocall.xml create mode 100644 java/com/android/incallui/video/impl/res/layout/frag_videocall_land.xml create mode 100644 java/com/android/incallui/video/impl/res/layout/switch_camera_button.xml create mode 100644 java/com/android/incallui/video/impl/res/layout/video_contact_grid.xml create mode 100644 java/com/android/incallui/video/impl/res/layout/videocall_controls.xml create mode 100644 java/com/android/incallui/video/impl/res/layout/videocall_controls_land.xml create mode 100644 java/com/android/incallui/video/impl/res/values-h580dp/dimens.xml create mode 100644 java/com/android/incallui/video/impl/res/values-w460dp/dimens.xml create mode 100644 java/com/android/incallui/video/impl/res/values/attrs.xml create mode 100644 java/com/android/incallui/video/impl/res/values/dimens.xml create mode 100644 java/com/android/incallui/video/impl/res/values/strings.xml create mode 100644 java/com/android/incallui/video/impl/res/values/styles.xml create mode 100644 java/com/android/incallui/video/protocol/VideoCallScreen.java create mode 100644 java/com/android/incallui/video/protocol/VideoCallScreenDelegate.java create mode 100644 java/com/android/incallui/video/protocol/VideoCallScreenDelegateFactory.java create mode 100644 java/com/android/incallui/videosurface/bindings/VideoSurfaceBindings.java create mode 100644 java/com/android/incallui/videosurface/impl/VideoScale.java create mode 100644 java/com/android/incallui/videosurface/impl/VideoSurfaceTextureImpl.java create mode 100644 java/com/android/incallui/videosurface/protocol/VideoSurfaceDelegate.java create mode 100644 java/com/android/incallui/videosurface/protocol/VideoSurfaceTexture.java create mode 100644 java/com/android/incallui/wifi/AndroidManifest.xml create mode 100644 java/com/android/incallui/wifi/EnableWifiCallingPrompt.java create mode 100644 java/com/android/incallui/wifi/res/values/strings.xml create mode 100644 java/com/android/voicemailomtp/ActivationTask.java create mode 100644 java/com/android/voicemailomtp/AndroidManifest.xml create mode 100644 java/com/android/voicemailomtp/Assert.java create mode 100644 java/com/android/voicemailomtp/DefaultOmtpEventHandler.java create mode 100644 java/com/android/voicemailomtp/NeededForTesting.java create mode 100644 java/com/android/voicemailomtp/OmtpConstants.java create mode 100644 java/com/android/voicemailomtp/OmtpEvents.java create mode 100644 java/com/android/voicemailomtp/OmtpService.java create mode 100644 java/com/android/voicemailomtp/OmtpVvmCarrierConfigHelper.java create mode 100644 java/com/android/voicemailomtp/SubscriptionInfoHelper.java create mode 100644 java/com/android/voicemailomtp/TelephonyManagerStub.java create mode 100644 java/com/android/voicemailomtp/TelephonyVvmConfigManager.java create mode 100644 java/com/android/voicemailomtp/VisualVoicemailPreferences.java create mode 100644 java/com/android/voicemailomtp/Voicemail.java create mode 100644 java/com/android/voicemailomtp/VoicemailStatus.java create mode 100644 java/com/android/voicemailomtp/VvmLog.java create mode 100644 java/com/android/voicemailomtp/VvmPackageInstallReceiver.java create mode 100644 java/com/android/voicemailomtp/VvmPhoneStateListener.java create mode 100644 java/com/android/voicemailomtp/fetch/FetchVoicemailReceiver.java create mode 100644 java/com/android/voicemailomtp/fetch/VoicemailFetchedCallback.java create mode 100644 java/com/android/voicemailomtp/imap/ImapHelper.java create mode 100644 java/com/android/voicemailomtp/imap/VoicemailPayload.java create mode 100644 java/com/android/voicemailomtp/mail/Address.java create mode 100644 java/com/android/voicemailomtp/mail/AuthenticationFailedException.java create mode 100644 java/com/android/voicemailomtp/mail/Base64Body.java create mode 100644 java/com/android/voicemailomtp/mail/Body.java create mode 100644 java/com/android/voicemailomtp/mail/BodyPart.java create mode 100644 java/com/android/voicemailomtp/mail/CertificateValidationException.java create mode 100644 java/com/android/voicemailomtp/mail/FetchProfile.java create mode 100644 java/com/android/voicemailomtp/mail/Fetchable.java create mode 100644 java/com/android/voicemailomtp/mail/FixedLengthInputStream.java create mode 100644 java/com/android/voicemailomtp/mail/Flag.java create mode 100644 java/com/android/voicemailomtp/mail/MailTransport.java create mode 100644 java/com/android/voicemailomtp/mail/MeetingInfo.java create mode 100644 java/com/android/voicemailomtp/mail/Message.java create mode 100644 java/com/android/voicemailomtp/mail/MessageDateComparator.java create mode 100644 java/com/android/voicemailomtp/mail/MessagingException.java create mode 100644 java/com/android/voicemailomtp/mail/Multipart.java create mode 100644 java/com/android/voicemailomtp/mail/PackedString.java create mode 100644 java/com/android/voicemailomtp/mail/Part.java create mode 100644 java/com/android/voicemailomtp/mail/PeekableInputStream.java create mode 100644 java/com/android/voicemailomtp/mail/TempDirectory.java create mode 100644 java/com/android/voicemailomtp/mail/internet/BinaryTempFileBody.java create mode 100644 java/com/android/voicemailomtp/mail/internet/MimeBodyPart.java create mode 100644 java/com/android/voicemailomtp/mail/internet/MimeHeader.java create mode 100644 java/com/android/voicemailomtp/mail/internet/MimeMessage.java create mode 100644 java/com/android/voicemailomtp/mail/internet/MimeMultipart.java create mode 100644 java/com/android/voicemailomtp/mail/internet/MimeUtility.java create mode 100644 java/com/android/voicemailomtp/mail/internet/TextBody.java create mode 100644 java/com/android/voicemailomtp/mail/store/ImapConnection.java create mode 100644 java/com/android/voicemailomtp/mail/store/ImapFolder.java create mode 100644 java/com/android/voicemailomtp/mail/store/ImapStore.java create mode 100644 java/com/android/voicemailomtp/mail/store/imap/DigestMd5Utils.java create mode 100644 java/com/android/voicemailomtp/mail/store/imap/ImapConstants.java create mode 100644 java/com/android/voicemailomtp/mail/store/imap/ImapElement.java create mode 100644 java/com/android/voicemailomtp/mail/store/imap/ImapList.java create mode 100644 java/com/android/voicemailomtp/mail/store/imap/ImapMemoryLiteral.java create mode 100644 java/com/android/voicemailomtp/mail/store/imap/ImapResponse.java create mode 100644 java/com/android/voicemailomtp/mail/store/imap/ImapResponseParser.java create mode 100644 java/com/android/voicemailomtp/mail/store/imap/ImapSimpleString.java create mode 100644 java/com/android/voicemailomtp/mail/store/imap/ImapString.java create mode 100644 java/com/android/voicemailomtp/mail/store/imap/ImapTempFileLiteral.java create mode 100644 java/com/android/voicemailomtp/mail/store/imap/ImapUtility.java create mode 100644 java/com/android/voicemailomtp/mail/utility/CountingOutputStream.java create mode 100644 java/com/android/voicemailomtp/mail/utility/EOLConvertingOutputStream.java create mode 100644 java/com/android/voicemailomtp/mail/utils/LogUtils.java create mode 100644 java/com/android/voicemailomtp/mail/utils/Utility.java create mode 100644 java/com/android/voicemailomtp/permissions.xml create mode 100644 java/com/android/voicemailomtp/protocol/CvvmProtocol.java create mode 100644 java/com/android/voicemailomtp/protocol/OmtpProtocol.java create mode 100644 java/com/android/voicemailomtp/protocol/ProtocolHelper.java create mode 100644 java/com/android/voicemailomtp/protocol/VisualVoicemailProtocol.java create mode 100644 java/com/android/voicemailomtp/protocol/VisualVoicemailProtocolFactory.java create mode 100644 java/com/android/voicemailomtp/protocol/Vvm3EventHandler.java create mode 100644 java/com/android/voicemailomtp/protocol/Vvm3Protocol.java create mode 100644 java/com/android/voicemailomtp/protocol/Vvm3Subscriber.java create mode 100644 java/com/android/voicemailomtp/res/layout/voicemail_change_pin.xml create mode 100644 java/com/android/voicemailomtp/res/values/arrays.xml create mode 100644 java/com/android/voicemailomtp/res/values/attrs.xml create mode 100644 java/com/android/voicemailomtp/res/values/colors.xml create mode 100644 java/com/android/voicemailomtp/res/values/config.xml create mode 100644 java/com/android/voicemailomtp/res/values/dimens.xml create mode 100644 java/com/android/voicemailomtp/res/values/ids.xml create mode 100644 java/com/android/voicemailomtp/res/values/strings.xml create mode 100644 java/com/android/voicemailomtp/res/values/styles.xml create mode 100644 java/com/android/voicemailomtp/res/xml/voicemail_settings.xml create mode 100644 java/com/android/voicemailomtp/res/xml/vvm_config.xml create mode 100644 java/com/android/voicemailomtp/scheduling/BaseTask.java create mode 100644 java/com/android/voicemailomtp/scheduling/BlockerTask.java create mode 100644 java/com/android/voicemailomtp/scheduling/MinimalIntervalPolicy.java create mode 100644 java/com/android/voicemailomtp/scheduling/Policy.java create mode 100644 java/com/android/voicemailomtp/scheduling/PostponePolicy.java create mode 100644 java/com/android/voicemailomtp/scheduling/RetryPolicy.java create mode 100644 java/com/android/voicemailomtp/scheduling/Task.java create mode 100644 java/com/android/voicemailomtp/scheduling/TaskSchedulerService.java create mode 100644 java/com/android/voicemailomtp/settings/VisualVoicemailSettingsUtil.java create mode 100644 java/com/android/voicemailomtp/settings/VoicemailChangePinActivity.java create mode 100644 java/com/android/voicemailomtp/settings/VoicemailSettingsActivity.java create mode 100644 java/com/android/voicemailomtp/sms/LegacyModeSmsHandler.java create mode 100644 java/com/android/voicemailomtp/sms/OmtpCvvmMessageSender.java create mode 100644 java/com/android/voicemailomtp/sms/OmtpMessageReceiver.java create mode 100644 java/com/android/voicemailomtp/sms/OmtpMessageSender.java create mode 100644 java/com/android/voicemailomtp/sms/OmtpStandardMessageSender.java create mode 100644 java/com/android/voicemailomtp/sms/StatusMessage.java create mode 100644 java/com/android/voicemailomtp/sms/StatusSmsFetcher.java create mode 100644 java/com/android/voicemailomtp/sms/SyncMessage.java create mode 100644 java/com/android/voicemailomtp/sms/Vvm3MessageSender.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/commons/io/IOUtils.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/BodyDescriptor.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/CloseShieldInputStream.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/ContentHandler.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/EOLConvertingInputStream.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/Log.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/LogFactory.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/MimeBoundaryInputStream.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/MimeStreamParser.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/RootInputStream.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/codec/EncoderUtil.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/Base64InputStream.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/ByteQueue.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/DecoderUtil.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/QuotedPrintableInputStream.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/UnboundedFifoByteBuffer.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/AddressListField.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/ContentTransferEncodingField.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/ContentTypeField.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/DateTimeField.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/DefaultFieldParser.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/DelegatingFieldParser.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/Field.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/FieldParser.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/MailboxField.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/MailboxListField.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/UnstructuredField.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Address.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/AddressList.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Builder.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/DomainList.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Group.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Mailbox.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/MailboxList.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/NamedMailbox.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTaddr_spec.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTaddress.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTaddress_list.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTangle_addr.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTdomain.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTgroup_body.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTlocal_part.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTmailbox.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTname_addr.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTphrase.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTroute.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParser.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParser.jj create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserConstants.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserTokenManager.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserTreeConstants.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserVisitor.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/BaseNode.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/JJTAddressListParserState.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/Node.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ParseException.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/SimpleCharStream.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/SimpleNode.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/Token.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/TokenMgrError.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParser.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParserConstants.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParserTokenManager.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ParseException.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/SimpleCharStream.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/Token.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/TokenMgrError.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/DateTime.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParser.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParserConstants.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParserTokenManager.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/ParseException.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/SimpleCharStream.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/Token.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/TokenMgrError.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/util/CharsetUtil.java create mode 100644 java/com/android/voicemailomtp/sync/OmtpVvmSourceManager.java create mode 100644 java/com/android/voicemailomtp/sync/OmtpVvmSyncReceiver.java create mode 100644 java/com/android/voicemailomtp/sync/OmtpVvmSyncService.java create mode 100644 java/com/android/voicemailomtp/sync/SyncOneTask.java create mode 100644 java/com/android/voicemailomtp/sync/SyncTask.java create mode 100644 java/com/android/voicemailomtp/sync/UploadTask.java create mode 100644 java/com/android/voicemailomtp/sync/VoicemailProviderChangeReceiver.java create mode 100644 java/com/android/voicemailomtp/sync/VoicemailStatusQueryHelper.java create mode 100644 java/com/android/voicemailomtp/sync/VoicemailsQueryHelper.java create mode 100644 java/com/android/voicemailomtp/sync/VvmNetworkRequest.java create mode 100644 java/com/android/voicemailomtp/sync/VvmNetworkRequestCallback.java create mode 100644 java/com/android/voicemailomtp/utils/IndentingPrintWriter.java create mode 100644 java/com/android/voicemailomtp/utils/VoicemailDatabaseUtil.java create mode 100644 java/com/android/voicemailomtp/utils/VvmDumpHandler.java create mode 100644 java/com/android/voicemailomtp/utils/XmlUtils.java delete mode 100644 res/color/settings_text_color_primary.xml delete mode 100644 res/color/settings_text_color_secondary.xml delete mode 100644 res/color/tab_text_color.xml delete mode 100644 res/drawable-hdpi/empty_call_log.png delete mode 100644 res/drawable-hdpi/empty_contacts.png delete mode 100644 res/drawable-hdpi/empty_speed_dial.png delete mode 100644 res/drawable-hdpi/fab_blue.png delete mode 100644 res/drawable-hdpi/fab_ic_call.png delete mode 100644 res/drawable-hdpi/fab_ic_dial.png delete mode 100644 res/drawable-hdpi/ic_archive_white_24dp.png delete mode 100644 res/drawable-hdpi/ic_block_24dp.png delete mode 100644 res/drawable-hdpi/ic_call_arrow.png delete mode 100644 res/drawable-hdpi/ic_content_copy_24dp.png delete mode 100644 res/drawable-hdpi/ic_delete_24dp.png delete mode 100755 res/drawable-hdpi/ic_dialer_fork_add_call.png delete mode 100755 res/drawable-hdpi/ic_dialer_fork_current_call.png delete mode 100755 res/drawable-hdpi/ic_dialer_fork_tt_keypad.png delete mode 100644 res/drawable-hdpi/ic_grade_24dp.png delete mode 100644 res/drawable-hdpi/ic_handle.png delete mode 100644 res/drawable-hdpi/ic_menu_history_lt.png delete mode 100644 res/drawable-hdpi/ic_mic_grey600.png delete mode 100644 res/drawable-hdpi/ic_more_vert_24dp.png delete mode 100644 res/drawable-hdpi/ic_not_interested_googblue_24dp.png delete mode 100644 res/drawable-hdpi/ic_pause_24dp.png delete mode 100644 res/drawable-hdpi/ic_people_24dp.png delete mode 100644 res/drawable-hdpi/ic_phone_24dp.png delete mode 100644 res/drawable-hdpi/ic_play_arrow_24dp.png delete mode 100644 res/drawable-hdpi/ic_remove.png delete mode 100644 res/drawable-hdpi/ic_report_24dp.png delete mode 100644 res/drawable-hdpi/ic_results_phone.png delete mode 100644 res/drawable-hdpi/ic_schedule_24dp.png delete mode 100644 res/drawable-hdpi/ic_share_white_24dp.png delete mode 100644 res/drawable-hdpi/ic_star.png delete mode 100644 res/drawable-hdpi/ic_unblock.png delete mode 100644 res/drawable-hdpi/ic_videocam_24dp.png delete mode 100644 res/drawable-hdpi/ic_vm_sound_off_dis.png delete mode 100644 res/drawable-hdpi/ic_vm_sound_off_dk.png delete mode 100644 res/drawable-hdpi/ic_vm_sound_on_dis.png delete mode 100644 res/drawable-hdpi/ic_vm_sound_on_dk.png delete mode 100644 res/drawable-hdpi/ic_voicemail_24dp.png delete mode 100644 res/drawable-hdpi/ic_volume_down_24dp.png delete mode 100644 res/drawable-hdpi/ic_volume_up_24dp.png delete mode 100644 res/drawable-hdpi/search_shadow.9.png delete mode 100644 res/drawable-hdpi/shadow_contact_photo.png delete mode 100644 res/drawable-mdpi/empty_call_log.png delete mode 100644 res/drawable-mdpi/empty_contacts.png delete mode 100644 res/drawable-mdpi/empty_speed_dial.png delete mode 100644 res/drawable-mdpi/fab_blue.png delete mode 100644 res/drawable-mdpi/fab_ic_call.png delete mode 100644 res/drawable-mdpi/fab_ic_dial.png delete mode 100644 res/drawable-mdpi/ic_archive_white_24dp.png delete mode 100644 res/drawable-mdpi/ic_block_24dp.png delete mode 100644 res/drawable-mdpi/ic_call_arrow.png delete mode 100644 res/drawable-mdpi/ic_content_copy_24dp.png delete mode 100644 res/drawable-mdpi/ic_delete_24dp.png delete mode 100644 res/drawable-mdpi/ic_dialer_fork_add_call.png delete mode 100644 res/drawable-mdpi/ic_dialer_fork_current_call.png delete mode 100644 res/drawable-mdpi/ic_dialer_fork_tt_keypad.png delete mode 100644 res/drawable-mdpi/ic_grade_24dp.png delete mode 100644 res/drawable-mdpi/ic_handle.png delete mode 100644 res/drawable-mdpi/ic_menu_history_lt.png delete mode 100644 res/drawable-mdpi/ic_mic_grey600.png delete mode 100644 res/drawable-mdpi/ic_more_vert_24dp.png delete mode 100644 res/drawable-mdpi/ic_not_interested_googblue_24dp.png delete mode 100644 res/drawable-mdpi/ic_pause_24dp.png delete mode 100644 res/drawable-mdpi/ic_people_24dp.png delete mode 100644 res/drawable-mdpi/ic_phone_24dp.png delete mode 100644 res/drawable-mdpi/ic_play_arrow_24dp.png delete mode 100644 res/drawable-mdpi/ic_remove.png delete mode 100644 res/drawable-mdpi/ic_report_24dp.png delete mode 100644 res/drawable-mdpi/ic_results_phone.png delete mode 100644 res/drawable-mdpi/ic_schedule_24dp.png delete mode 100644 res/drawable-mdpi/ic_share_white_24dp.png delete mode 100644 res/drawable-mdpi/ic_star.png delete mode 100644 res/drawable-mdpi/ic_unblock.png delete mode 100644 res/drawable-mdpi/ic_videocam_24dp.png delete mode 100644 res/drawable-mdpi/ic_vm_sound_off_dis.png delete mode 100644 res/drawable-mdpi/ic_vm_sound_off_dk.png delete mode 100644 res/drawable-mdpi/ic_vm_sound_on_dis.png delete mode 100644 res/drawable-mdpi/ic_vm_sound_on_dk.png delete mode 100644 res/drawable-mdpi/ic_voicemail_24dp.png delete mode 100644 res/drawable-mdpi/ic_volume_down_24dp.png delete mode 100644 res/drawable-mdpi/ic_volume_up_24dp.png delete mode 100644 res/drawable-mdpi/search_shadow.9.png delete mode 100644 res/drawable-mdpi/shadow_contact_photo.png delete mode 100644 res/drawable-xhdpi/empty_call_log.png delete mode 100644 res/drawable-xhdpi/empty_contacts.png delete mode 100644 res/drawable-xhdpi/empty_speed_dial.png delete mode 100644 res/drawable-xhdpi/fab_blue.png delete mode 100644 res/drawable-xhdpi/fab_ic_call.png delete mode 100644 res/drawable-xhdpi/fab_ic_dial.png delete mode 100644 res/drawable-xhdpi/ic_archive_white_24dp.png delete mode 100644 res/drawable-xhdpi/ic_block_24dp.png delete mode 100644 res/drawable-xhdpi/ic_call_arrow.png delete mode 100644 res/drawable-xhdpi/ic_content_copy_24dp.png delete mode 100644 res/drawable-xhdpi/ic_delete_24dp.png delete mode 100644 res/drawable-xhdpi/ic_dialer_fork_add_call.png delete mode 100644 res/drawable-xhdpi/ic_dialer_fork_current_call.png delete mode 100644 res/drawable-xhdpi/ic_dialer_fork_tt_keypad.png delete mode 100644 res/drawable-xhdpi/ic_grade_24dp.png delete mode 100644 res/drawable-xhdpi/ic_handle.png delete mode 100644 res/drawable-xhdpi/ic_menu_history_lt.png delete mode 100644 res/drawable-xhdpi/ic_mic_grey600.png delete mode 100644 res/drawable-xhdpi/ic_more_vert_24dp.png delete mode 100644 res/drawable-xhdpi/ic_not_interested_googblue_24dp.png delete mode 100644 res/drawable-xhdpi/ic_pause_24dp.png delete mode 100644 res/drawable-xhdpi/ic_people_24dp.png delete mode 100644 res/drawable-xhdpi/ic_phone_24dp.png delete mode 100644 res/drawable-xhdpi/ic_play_arrow_24dp.png delete mode 100644 res/drawable-xhdpi/ic_remove.png delete mode 100644 res/drawable-xhdpi/ic_report_24dp.png delete mode 100644 res/drawable-xhdpi/ic_results_phone.png delete mode 100644 res/drawable-xhdpi/ic_schedule_24dp.png delete mode 100644 res/drawable-xhdpi/ic_share_white_24dp.png delete mode 100644 res/drawable-xhdpi/ic_star.png delete mode 100644 res/drawable-xhdpi/ic_unblock.png delete mode 100644 res/drawable-xhdpi/ic_videocam_24dp.png delete mode 100644 res/drawable-xhdpi/ic_vm_sound_off_dis.png delete mode 100644 res/drawable-xhdpi/ic_vm_sound_off_dk.png delete mode 100644 res/drawable-xhdpi/ic_vm_sound_on_dis.png delete mode 100644 res/drawable-xhdpi/ic_vm_sound_on_dk.png delete mode 100644 res/drawable-xhdpi/ic_voicemail_24dp.png delete mode 100644 res/drawable-xhdpi/ic_volume_down_24dp.png delete mode 100644 res/drawable-xhdpi/ic_volume_up_24dp.png delete mode 100644 res/drawable-xhdpi/search_shadow.9.png delete mode 100644 res/drawable-xhdpi/shadow_contact_photo.png delete mode 100644 res/drawable-xxhdpi/empty_call_log.png delete mode 100644 res/drawable-xxhdpi/empty_contacts.png delete mode 100644 res/drawable-xxhdpi/empty_speed_dial.png delete mode 100644 res/drawable-xxhdpi/fab_blue.png delete mode 100644 res/drawable-xxhdpi/fab_ic_call.png delete mode 100644 res/drawable-xxhdpi/fab_ic_dial.png delete mode 100644 res/drawable-xxhdpi/ic_archive_white_24dp.png delete mode 100644 res/drawable-xxhdpi/ic_block_24dp.png delete mode 100644 res/drawable-xxhdpi/ic_call_arrow.png delete mode 100644 res/drawable-xxhdpi/ic_content_copy_24dp.png delete mode 100644 res/drawable-xxhdpi/ic_delete_24dp.png delete mode 100644 res/drawable-xxhdpi/ic_dialer_fork_add_call.png delete mode 100644 res/drawable-xxhdpi/ic_dialer_fork_current_call.png delete mode 100644 res/drawable-xxhdpi/ic_dialer_fork_tt_keypad.png delete mode 100644 res/drawable-xxhdpi/ic_grade_24dp.png delete mode 100644 res/drawable-xxhdpi/ic_handle.png delete mode 100644 res/drawable-xxhdpi/ic_menu_history_lt.png delete mode 100644 res/drawable-xxhdpi/ic_mic_grey600.png delete mode 100644 res/drawable-xxhdpi/ic_more_vert_24dp.png delete mode 100644 res/drawable-xxhdpi/ic_not_interested_googblue_24dp.png delete mode 100644 res/drawable-xxhdpi/ic_pause_24dp.png delete mode 100644 res/drawable-xxhdpi/ic_people_24dp.png delete mode 100644 res/drawable-xxhdpi/ic_phone_24dp.png delete mode 100644 res/drawable-xxhdpi/ic_play_arrow_24dp.png delete mode 100644 res/drawable-xxhdpi/ic_remove.png delete mode 100644 res/drawable-xxhdpi/ic_report_24dp.png delete mode 100644 res/drawable-xxhdpi/ic_results_phone.png delete mode 100644 res/drawable-xxhdpi/ic_schedule_24dp.png delete mode 100644 res/drawable-xxhdpi/ic_share_white_24dp.png delete mode 100644 res/drawable-xxhdpi/ic_star.png delete mode 100644 res/drawable-xxhdpi/ic_unblock.png delete mode 100644 res/drawable-xxhdpi/ic_videocam_24dp.png delete mode 100644 res/drawable-xxhdpi/ic_vm_sound_off_dis.png delete mode 100644 res/drawable-xxhdpi/ic_vm_sound_off_dk.png delete mode 100644 res/drawable-xxhdpi/ic_vm_sound_on_dis.png delete mode 100644 res/drawable-xxhdpi/ic_vm_sound_on_dk.png delete mode 100644 res/drawable-xxhdpi/ic_voicemail_24dp.png delete mode 100644 res/drawable-xxhdpi/ic_volume_down_24dp.png delete mode 100644 res/drawable-xxhdpi/ic_volume_up_24dp.png delete mode 100644 res/drawable-xxhdpi/search_shadow.9.png delete mode 100644 res/drawable-xxhdpi/shadow_contact_photo.png delete mode 100644 res/drawable-xxxhdpi/empty_call_log.png delete mode 100644 res/drawable-xxxhdpi/empty_contacts.png delete mode 100644 res/drawable-xxxhdpi/fab_blue.png delete mode 100644 res/drawable-xxxhdpi/fab_ic_call.png delete mode 100644 res/drawable-xxxhdpi/fab_ic_dial.png delete mode 100644 res/drawable-xxxhdpi/ic_archive_white_24dp.png delete mode 100644 res/drawable-xxxhdpi/ic_block_24dp.png delete mode 100644 res/drawable-xxxhdpi/ic_call_arrow.png delete mode 100644 res/drawable-xxxhdpi/ic_content_copy_24dp.png delete mode 100644 res/drawable-xxxhdpi/ic_delete_24dp.png delete mode 100644 res/drawable-xxxhdpi/ic_grade_24dp.png delete mode 100644 res/drawable-xxxhdpi/ic_handle.png delete mode 100644 res/drawable-xxxhdpi/ic_mic_grey600.png delete mode 100644 res/drawable-xxxhdpi/ic_more_vert_24dp.png delete mode 100644 res/drawable-xxxhdpi/ic_not_interested_googblue_24dp.png delete mode 100644 res/drawable-xxxhdpi/ic_pause_24dp.png delete mode 100644 res/drawable-xxxhdpi/ic_people_24dp.png delete mode 100644 res/drawable-xxxhdpi/ic_phone_24dp.png delete mode 100644 res/drawable-xxxhdpi/ic_play_arrow_24dp.png delete mode 100644 res/drawable-xxxhdpi/ic_report_24dp.png delete mode 100644 res/drawable-xxxhdpi/ic_results_phone.png delete mode 100644 res/drawable-xxxhdpi/ic_schedule_24dp.png delete mode 100644 res/drawable-xxxhdpi/ic_share_white_24dp.png delete mode 100644 res/drawable-xxxhdpi/ic_unblock.png delete mode 100644 res/drawable-xxxhdpi/ic_videocam_24dp.png delete mode 100644 res/drawable-xxxhdpi/ic_voicemail_24dp.png delete mode 100644 res/drawable-xxxhdpi/ic_volume_down_24dp.png delete mode 100644 res/drawable-xxxhdpi/ic_volume_up_24dp.png delete mode 100644 res/drawable/background_dial_holo_dark.xml delete mode 100644 res/drawable/blocked_contact.xml delete mode 100644 res/drawable/floating_action_button.xml delete mode 100644 res/drawable/ic_call_detail_block.xml delete mode 100644 res/drawable/ic_call_detail_content_copy.xml delete mode 100644 res/drawable/ic_call_detail_edit.xml delete mode 100644 res/drawable/ic_call_detail_report.xml delete mode 100644 res/drawable/ic_call_detail_unblock.xml delete mode 100644 res/drawable/ic_pause.xml delete mode 100644 res/drawable/ic_play_arrow.xml delete mode 100644 res/drawable/ic_search_phone.xml delete mode 100644 res/drawable/ic_speakerphone_off.xml delete mode 100644 res/drawable/ic_speakerphone_on.xml delete mode 100644 res/drawable/ic_voicemail_seek_handle.xml delete mode 100644 res/drawable/ic_voicemail_seek_handle_disabled.xml delete mode 100644 res/drawable/oval_ripple.xml delete mode 100644 res/drawable/overflow_menu.xml delete mode 100644 res/drawable/rounded_corner.xml delete mode 100644 res/drawable/seekbar_drawable.xml delete mode 100644 res/drawable/selectable_primary_flat_button.xml delete mode 100644 res/drawable/shadow_fade_left.xml delete mode 100644 res/drawable/shadow_fade_up.xml delete mode 100644 res/layout-land/dialpad_fragment.xml delete mode 100644 res/layout/account_filter_header_for_phone_favorite.xml delete mode 100644 res/layout/all_contacts_activity.xml delete mode 100644 res/layout/all_contacts_fragment.xml delete mode 100644 res/layout/block_report_spam_dialog.xml delete mode 100644 res/layout/blocked_number_footer.xml delete mode 100644 res/layout/blocked_number_fragment.xml delete mode 100644 res/layout/blocked_number_header.xml delete mode 100644 res/layout/blocked_number_item.xml delete mode 100644 res/layout/blocked_numbers_activity.xml delete mode 100644 res/layout/call_detail.xml delete mode 100644 res/layout/call_detail_footer.xml delete mode 100644 res/layout/call_detail_header.xml delete mode 100644 res/layout/call_detail_history_item.xml delete mode 100644 res/layout/call_log_activity.xml delete mode 100644 res/layout/call_log_fragment.xml delete mode 100644 res/layout/call_log_list_item.xml delete mode 100644 res/layout/call_log_list_item_actions.xml delete mode 100644 res/layout/dialpad_chooser_list_item.xml delete mode 100644 res/layout/dialpad_fragment.xml delete mode 100644 res/layout/dialtacts_activity.xml delete mode 100644 res/layout/empty_content_view.xml delete mode 100644 res/layout/keyguard_preview.xml delete mode 100644 res/layout/lists_fragment.xml delete mode 100755 res/layout/phone_disambig_item.xml delete mode 100644 res/layout/phone_favorite_tile_view.xml delete mode 100644 res/layout/search_edittext.xml delete mode 100644 res/layout/set_primary_checkbox.xml delete mode 100644 res/layout/speed_dial_fragment.xml delete mode 100644 res/layout/view_numbers_to_import_fragment.xml delete mode 100644 res/layout/voicemail_playback_layout.xml delete mode 100644 res/layout/voicemail_promo_card.xml delete mode 100644 res/menu/call_log_options.xml delete mode 100644 res/menu/dialpad_options.xml delete mode 100644 res/menu/dialtacts_options.xml delete mode 100644 res/mipmap-hdpi/ic_launcher_phone.png delete mode 100644 res/mipmap-mdpi/ic_launcher_phone.png delete mode 100644 res/mipmap-xhdpi/ic_launcher_phone.png delete mode 100644 res/mipmap-xxhdpi/ic_launcher_phone.png delete mode 100644 res/mipmap-xxxhdpi/ic_launcher_phone.png delete mode 100644 res/values-af/strings.xml delete mode 100644 res/values-am/strings.xml delete mode 100644 res/values-ar/strings.xml delete mode 100644 res/values-az/strings.xml delete mode 100644 res/values-b+sr+Latn/strings.xml delete mode 100644 res/values-be/strings.xml delete mode 100644 res/values-bg/strings.xml delete mode 100644 res/values-bn/strings.xml delete mode 100644 res/values-bs/strings.xml delete mode 100644 res/values-ca/strings.xml delete mode 100644 res/values-cs/strings.xml delete mode 100644 res/values-da/strings.xml delete mode 100644 res/values-de/strings.xml delete mode 100644 res/values-el/strings.xml delete mode 100644 res/values-en-rAU/strings.xml delete mode 100644 res/values-en-rGB/strings.xml delete mode 100644 res/values-en-rIN/strings.xml delete mode 100644 res/values-es-rUS/strings.xml delete mode 100644 res/values-es/strings.xml delete mode 100644 res/values-et/strings.xml delete mode 100644 res/values-eu/strings.xml delete mode 100644 res/values-fa/strings.xml delete mode 100644 res/values-fi/strings.xml delete mode 100644 res/values-fr-rCA/strings.xml delete mode 100644 res/values-fr/strings.xml delete mode 100644 res/values-gl/strings.xml delete mode 100644 res/values-gu/strings.xml delete mode 100644 res/values-hi/strings.xml delete mode 100644 res/values-hr/strings.xml delete mode 100644 res/values-hu/strings.xml delete mode 100644 res/values-hy/strings.xml delete mode 100644 res/values-in/strings.xml delete mode 100644 res/values-is/strings.xml delete mode 100644 res/values-it/strings.xml delete mode 100644 res/values-iw/strings.xml delete mode 100644 res/values-ja/strings.xml delete mode 100644 res/values-ka/strings.xml delete mode 100644 res/values-kk/strings.xml delete mode 100644 res/values-km/strings.xml delete mode 100644 res/values-kn/strings.xml delete mode 100644 res/values-ko/strings.xml delete mode 100644 res/values-ky/strings.xml delete mode 100644 res/values-lo/strings.xml delete mode 100644 res/values-lt/strings.xml delete mode 100644 res/values-lv/strings.xml delete mode 100644 res/values-mk/strings.xml delete mode 100644 res/values-ml/strings.xml delete mode 100644 res/values-mn/strings.xml delete mode 100644 res/values-mr/strings.xml delete mode 100644 res/values-ms/strings.xml delete mode 100644 res/values-my/strings.xml delete mode 100644 res/values-nb/strings.xml delete mode 100644 res/values-ne/strings.xml delete mode 100644 res/values-nl/strings.xml delete mode 100644 res/values-pa/strings.xml delete mode 100644 res/values-pl/strings.xml delete mode 100644 res/values-pt-rBR/strings.xml delete mode 100644 res/values-pt-rPT/strings.xml delete mode 100644 res/values-pt/strings.xml delete mode 100644 res/values-ro/strings.xml delete mode 100644 res/values-ru/strings.xml delete mode 100644 res/values-si/strings.xml delete mode 100644 res/values-sk/strings.xml delete mode 100644 res/values-sl/strings.xml delete mode 100644 res/values-sq/strings.xml delete mode 100644 res/values-sr/strings.xml delete mode 100644 res/values-sv/strings.xml delete mode 100644 res/values-sw/strings.xml delete mode 100644 res/values-ta/strings.xml delete mode 100644 res/values-te/strings.xml delete mode 100644 res/values-th/strings.xml delete mode 100644 res/values-tl/strings.xml delete mode 100644 res/values-tr/strings.xml delete mode 100644 res/values-uk/strings.xml delete mode 100644 res/values-ur/strings.xml delete mode 100644 res/values-uz/strings.xml delete mode 100644 res/values-vi/strings.xml delete mode 100644 res/values-zh-rCN/strings.xml delete mode 100644 res/values-zh-rHK/strings.xml delete mode 100644 res/values-zh-rTW/strings.xml delete mode 100644 res/values-zu/strings.xml delete mode 100644 res/values/animation_constants.xml delete mode 100644 res/values/attrs.xml delete mode 100644 res/values/colors.xml delete mode 100644 res/values/dimens.xml delete mode 100644 res/values/donottranslate_config.xml delete mode 100644 res/values/ids.xml delete mode 100644 res/values/strings.xml delete mode 100644 res/values/styles.xml delete mode 100644 res/xml/display_options_settings.xml delete mode 100644 res/xml/file_paths.xml delete mode 100644 res/xml/searchable.xml delete mode 100644 res/xml/sound_settings.xml delete mode 100644 src-N/com/android/dialer/SdkSelectionUtils.java delete mode 100644 src-N/com/android/dialer/compat/BlockedNumbersSdkCompat.java delete mode 100644 src-N/com/android/dialer/compat/CallsSdkCompat.java delete mode 100644 src-N/com/android/dialer/compat/UserManagerSdkCompat.java delete mode 100644 src-pre-N/com/android/dialer/SdkSelectionUtils.java delete mode 100644 src-pre-N/com/android/dialer/compat/BlockedNumbersSdkCompat.java delete mode 100644 src-pre-N/com/android/dialer/compat/CallsSdkCompat.java delete mode 100644 src-pre-N/com/android/dialer/compat/UserManagerSdkCompat.java delete mode 100644 src/com/android/dialer/CallDetailActivity.java delete mode 100644 src/com/android/dialer/DialerApplication.java delete mode 100644 src/com/android/dialer/DialerBackupAgent.java delete mode 100644 src/com/android/dialer/DialtactsActivity.java delete mode 100644 src/com/android/dialer/FloatingActionButtonBehavior.java delete mode 100644 src/com/android/dialer/NeededForReflection.java delete mode 100644 src/com/android/dialer/PhoneCallDetails.java delete mode 100644 src/com/android/dialer/SpecialCharSequenceMgr.java delete mode 100644 src/com/android/dialer/TransactionSafeActivity.java delete mode 100644 src/com/android/dialer/calllog/BlockReportSpamListener.java delete mode 100644 src/com/android/dialer/calllog/CallDetailHistoryAdapter.java delete mode 100644 src/com/android/dialer/calllog/CallLogActivity.java delete mode 100644 src/com/android/dialer/calllog/CallLogAdapter.java delete mode 100644 src/com/android/dialer/calllog/CallLogAsyncTaskUtil.java delete mode 100644 src/com/android/dialer/calllog/CallLogFragment.java delete mode 100644 src/com/android/dialer/calllog/CallLogGroupBuilder.java delete mode 100644 src/com/android/dialer/calllog/CallLogListItemHelper.java delete mode 100644 src/com/android/dialer/calllog/CallLogListItemViewHolder.java delete mode 100644 src/com/android/dialer/calllog/CallLogNotificationsHelper.java delete mode 100644 src/com/android/dialer/calllog/CallLogNotificationsService.java delete mode 100644 src/com/android/dialer/calllog/CallLogQuery.java delete mode 100644 src/com/android/dialer/calllog/CallLogQueryHandler.java delete mode 100644 src/com/android/dialer/calllog/CallLogReceiver.java delete mode 100644 src/com/android/dialer/calllog/CallTypeHelper.java delete mode 100644 src/com/android/dialer/calllog/CallTypeIconsView.java delete mode 100644 src/com/android/dialer/calllog/ClearCallLogDialog.java delete mode 100644 src/com/android/dialer/calllog/ContactInfo.java delete mode 100644 src/com/android/dialer/calllog/ContactInfoHelper.java delete mode 100644 src/com/android/dialer/calllog/DefaultVoicemailNotifier.java delete mode 100644 src/com/android/dialer/calllog/GroupingListAdapter.java delete mode 100644 src/com/android/dialer/calllog/IntentProvider.java delete mode 100644 src/com/android/dialer/calllog/MissedCallNotificationReceiver.java delete mode 100644 src/com/android/dialer/calllog/MissedCallNotifier.java delete mode 100644 src/com/android/dialer/calllog/PhoneAccountUtils.java delete mode 100644 src/com/android/dialer/calllog/PhoneCallDetailsHelper.java delete mode 100644 src/com/android/dialer/calllog/PhoneCallDetailsViews.java delete mode 100644 src/com/android/dialer/calllog/PhoneNumberDisplayUtil.java delete mode 100644 src/com/android/dialer/calllog/PhoneQuery.java delete mode 100644 src/com/android/dialer/calllog/PromoCardViewHolder.java delete mode 100644 src/com/android/dialer/calllog/VisualVoicemailCallLogFragment.java delete mode 100644 src/com/android/dialer/calllog/VoicemailQueryHandler.java delete mode 100644 src/com/android/dialer/calllog/calllogcache/CallLogCache.java delete mode 100644 src/com/android/dialer/calllog/calllogcache/CallLogCacheLollipop.java delete mode 100644 src/com/android/dialer/calllog/calllogcache/CallLogCacheLollipopMr1.java delete mode 100644 src/com/android/dialer/compat/DialerCompatUtils.java delete mode 100644 src/com/android/dialer/compat/FilteredNumberCompat.java delete mode 100644 src/com/android/dialer/compat/SettingsCompat.java delete mode 100644 src/com/android/dialer/compat/UserManagerCompat.java delete mode 100644 src/com/android/dialer/contact/ContactUpdateService.java delete mode 100644 src/com/android/dialer/contactinfo/ContactInfoCache.java delete mode 100644 src/com/android/dialer/contactinfo/ContactInfoRequest.java delete mode 100644 src/com/android/dialer/contactinfo/ContactPhotoLoader.java delete mode 100644 src/com/android/dialer/contactinfo/NumberWithCountryIso.java delete mode 100644 src/com/android/dialer/database/DialerDatabaseHelper.java delete mode 100644 src/com/android/dialer/database/FilteredNumberAsyncQueryHandler.java delete mode 100644 src/com/android/dialer/database/FilteredNumberContract.java delete mode 100644 src/com/android/dialer/database/FilteredNumberProvider.java delete mode 100644 src/com/android/dialer/database/VoicemailArchiveContract.java delete mode 100644 src/com/android/dialer/database/VoicemailArchiveProvider.java delete mode 100644 src/com/android/dialer/dialpad/DialpadFragment.java delete mode 100644 src/com/android/dialer/dialpad/LatinSmartDialMap.java delete mode 100644 src/com/android/dialer/dialpad/PseudoEmergencyAnimator.java delete mode 100644 src/com/android/dialer/dialpad/SmartDialCursorLoader.java delete mode 100644 src/com/android/dialer/dialpad/SmartDialMap.java delete mode 100644 src/com/android/dialer/dialpad/SmartDialMatchPosition.java delete mode 100644 src/com/android/dialer/dialpad/SmartDialNameMatcher.java delete mode 100644 src/com/android/dialer/dialpad/SmartDialPrefix.java delete mode 100644 src/com/android/dialer/dialpad/UnicodeDialerKeyListener.java delete mode 100644 src/com/android/dialer/filterednumber/BlockNumberDialogFragment.java delete mode 100644 src/com/android/dialer/filterednumber/BlockedNumbersAdapter.java delete mode 100644 src/com/android/dialer/filterednumber/BlockedNumbersAutoMigrator.java delete mode 100644 src/com/android/dialer/filterednumber/BlockedNumbersFragment.java delete mode 100644 src/com/android/dialer/filterednumber/BlockedNumbersMigrator.java delete mode 100644 src/com/android/dialer/filterednumber/BlockedNumbersSettingsActivity.java delete mode 100644 src/com/android/dialer/filterednumber/FilteredNumbersUtil.java delete mode 100644 src/com/android/dialer/filterednumber/MigrateBlockedNumbersDialogFragment.java delete mode 100644 src/com/android/dialer/filterednumber/NumbersAdapter.java delete mode 100644 src/com/android/dialer/filterednumber/ViewNumbersToImportAdapter.java delete mode 100644 src/com/android/dialer/filterednumber/ViewNumbersToImportFragment.java delete mode 100644 src/com/android/dialer/interactions/PhoneNumberInteraction.java delete mode 100644 src/com/android/dialer/interactions/UndemoteOutgoingCallReceiver.java delete mode 100644 src/com/android/dialer/list/AllContactsFragment.java delete mode 100644 src/com/android/dialer/list/BlockedListSearchAdapter.java delete mode 100644 src/com/android/dialer/list/BlockedListSearchFragment.java delete mode 100644 src/com/android/dialer/list/ContentChangedFilter.java delete mode 100644 src/com/android/dialer/list/DialerPhoneNumberListAdapter.java delete mode 100644 src/com/android/dialer/list/DragDropController.java delete mode 100644 src/com/android/dialer/list/ListsFragment.java delete mode 100644 src/com/android/dialer/list/OnDragDropListener.java delete mode 100644 src/com/android/dialer/list/OnListFragmentScrolledListener.java delete mode 100644 src/com/android/dialer/list/PhoneFavoriteListView.java delete mode 100644 src/com/android/dialer/list/PhoneFavoriteSquareTileView.java delete mode 100644 src/com/android/dialer/list/PhoneFavoriteTileView.java delete mode 100644 src/com/android/dialer/list/PhoneFavoritesTileAdapter.java delete mode 100644 src/com/android/dialer/list/RegularSearchFragment.java delete mode 100644 src/com/android/dialer/list/RegularSearchListAdapter.java delete mode 100644 src/com/android/dialer/list/RemoveView.java delete mode 100644 src/com/android/dialer/list/SearchFragment.java delete mode 100644 src/com/android/dialer/list/SmartDialNumberListAdapter.java delete mode 100644 src/com/android/dialer/list/SmartDialSearchFragment.java delete mode 100644 src/com/android/dialer/list/SpeedDialFragment.java delete mode 100644 src/com/android/dialer/logging/InteractionEvent.java delete mode 100644 src/com/android/dialer/logging/Logger.java delete mode 100644 src/com/android/dialer/logging/ScreenEvent.java delete mode 100644 src/com/android/dialer/service/CachedNumberLookupService.java delete mode 100644 src/com/android/dialer/service/ExtendedCallInfoService.java delete mode 100644 src/com/android/dialer/settings/AppCompatPreferenceActivity.java delete mode 100644 src/com/android/dialer/settings/DefaultRingtonePreference.java delete mode 100644 src/com/android/dialer/settings/DialerSettingsActivity.java delete mode 100644 src/com/android/dialer/settings/DisplayOptionsSettingsFragment.java delete mode 100644 src/com/android/dialer/settings/SoundSettingsFragment.java delete mode 100644 src/com/android/dialer/util/AppCompatConstants.java delete mode 100644 src/com/android/dialer/util/Assert.java delete mode 100644 src/com/android/dialer/util/AsyncTaskExecutor.java delete mode 100644 src/com/android/dialer/util/AsyncTaskExecutors.java delete mode 100644 src/com/android/dialer/util/BlockReportSpamDialogs.java delete mode 100644 src/com/android/dialer/util/DialerUtils.java delete mode 100644 src/com/android/dialer/util/EmptyLoader.java delete mode 100644 src/com/android/dialer/util/ExpirableCache.java delete mode 100644 src/com/android/dialer/util/IntentUtil.java delete mode 100644 src/com/android/dialer/util/MoreStrings.java delete mode 100644 src/com/android/dialer/util/OrientationUtil.java delete mode 100644 src/com/android/dialer/util/PhoneLookupUtil.java delete mode 100644 src/com/android/dialer/util/PhoneNumberUtil.java delete mode 100644 src/com/android/dialer/util/TelecomUtil.java delete mode 100644 src/com/android/dialer/voicemail/VisualVoicemailEnabledChecker.java delete mode 100644 src/com/android/dialer/voicemail/VoicemailArchiveActivity.java delete mode 100644 src/com/android/dialer/voicemail/VoicemailArchivePlaybackPresenter.java delete mode 100644 src/com/android/dialer/voicemail/VoicemailAsyncTaskUtil.java delete mode 100644 src/com/android/dialer/voicemail/VoicemailAudioManager.java delete mode 100644 src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java delete mode 100644 src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java delete mode 100644 src/com/android/dialer/voicemail/VoicemailStatusHelper.java delete mode 100644 src/com/android/dialer/voicemail/VoicemailStatusHelperImpl.java delete mode 100644 src/com/android/dialer/voicemail/WiredHeadsetManager.java delete mode 100644 src/com/android/dialer/widget/ActionBarController.java delete mode 100644 src/com/android/dialer/widget/EmptyContentView.java delete mode 100644 src/com/android/dialer/widget/SearchEditTextLayout.java delete mode 100644 src/com/android/dialerbind/DatabaseHelperManager.java delete mode 100644 src/com/android/dialerbind/ObjectFactory.java delete mode 100644 tests/Android.mk delete mode 100644 tests/AndroidManifest.xml delete mode 100644 tests/assets/README.txt delete mode 100644 tests/assets/quick_test_recording.mp3 delete mode 100644 tests/proguard.flags delete mode 100644 tests/res/drawable/phone_icon.png delete mode 100644 tests/res/layout/fill_call_log_test.xml delete mode 100644 tests/res/values/donottranslate_strings.xml delete mode 100644 tests/res/xml/iconset.xml delete mode 100644 tests/src/com/android/dialer/CallDetailActivityTest.java delete mode 100644 tests/src/com/android/dialer/DialerLaunchPerformance.java delete mode 100644 tests/src/com/android/dialer/calllog/BlockReportSpamListenerTest.java delete mode 100644 tests/src/com/android/dialer/calllog/CallLogAdapterTest.java delete mode 100644 tests/src/com/android/dialer/calllog/CallLogGroupBuilderTest.java delete mode 100644 tests/src/com/android/dialer/calllog/CallLogListItemHelperTest.java delete mode 100644 tests/src/com/android/dialer/calllog/CallLogNotificationsHelperTest.java delete mode 100644 tests/src/com/android/dialer/calllog/CallLogQueryTestUtils.java delete mode 100644 tests/src/com/android/dialer/calllog/ContactInfoHelperTest.java delete mode 100644 tests/src/com/android/dialer/calllog/GroupingListAdapterTests.java delete mode 100644 tests/src/com/android/dialer/calllog/PhoneAccountUtilsTest.java delete mode 100644 tests/src/com/android/dialer/calllog/PhoneCallDetailsHelperTest.java delete mode 100644 tests/src/com/android/dialer/calllog/PhoneCallDetailsTest.java delete mode 100644 tests/src/com/android/dialer/calllog/calllogcache/TestTelecomCallLogCache.java delete mode 100644 tests/src/com/android/dialer/compat/FilteredNumberCompatInstrumentationTest.java delete mode 100644 tests/src/com/android/dialer/compat/FilteredNumberCompatTest.java delete mode 100644 tests/src/com/android/dialer/compat/UserManagerCompatTest.java delete mode 100644 tests/src/com/android/dialer/contactinfo/ContactPhotoLoaderTest.java delete mode 100644 tests/src/com/android/dialer/database/DatabaseTestUtils.java delete mode 100644 tests/src/com/android/dialer/database/DialerDatabaseHelperTest.java delete mode 100644 tests/src/com/android/dialer/database/FilteredNumberAsyncQueryHandlerTest.java delete mode 100644 tests/src/com/android/dialer/database/FilteredNumberProviderTest.java delete mode 100644 tests/src/com/android/dialer/database/SmartDialPrefixTest.java delete mode 100644 tests/src/com/android/dialer/database/VoicemailArchiveProviderTest.java delete mode 100644 tests/src/com/android/dialer/dialpad/DialpadFragmentInstrumentationTest.java delete mode 100644 tests/src/com/android/dialer/dialpad/DialpadFragmentTest.java delete mode 100644 tests/src/com/android/dialer/dialpad/SmartDialNameMatcherTest.java delete mode 100644 tests/src/com/android/dialer/dialpad/UnicodeDialerKeyListenerTest.java delete mode 100644 tests/src/com/android/dialer/filterednumber/BlockedNumbersAutoMigratorTest.java delete mode 100644 tests/src/com/android/dialer/filterednumber/BlockedNumbersFragmentInstrumentationTest.java delete mode 100644 tests/src/com/android/dialer/filterednumber/BlockedNumbersMigratorTest.java delete mode 100644 tests/src/com/android/dialer/filterednumber/FilteredNumbersUtilTest.java delete mode 100644 tests/src/com/android/dialer/filterednumber/MigrateBlockedNumbersDialogFragmentInstrumentationTest.java delete mode 100644 tests/src/com/android/dialer/filterednumber/MigrateBlockedNumbersDialogFragmentTest.java delete mode 100644 tests/src/com/android/dialer/interactions/PhoneNumberInteractionTest.java delete mode 100644 tests/src/com/android/dialer/list/PhoneFavoritesTileAdapterTest.java delete mode 100644 tests/src/com/android/dialer/tests/calllog/FillCallLogTestActivity.java delete mode 100644 tests/src/com/android/dialer/util/DialerUtilsTest.java delete mode 100644 tests/src/com/android/dialer/util/ExpirableCacheTest.java delete mode 100644 tests/src/com/android/dialer/util/FakeAsyncTaskExecutor.java delete mode 100644 tests/src/com/android/dialer/util/LocaleTestUtils.java delete mode 100644 tests/src/com/android/dialer/util/TestConstants.java delete mode 100644 tests/src/com/android/dialer/voicemail/VoicemailActivityInstrumentationTestCase2.java delete mode 100644 tests/src/com/android/dialer/voicemail/VoicemailArchiveTest.java delete mode 100644 tests/src/com/android/dialer/voicemail/VoicemailAsyncTaskUtilTest.java delete mode 100644 tests/src/com/android/dialer/voicemail/VoicemailPlaybackTest.java delete mode 100644 tests/src/com/android/dialer/voicemail/VoicemailStatusHelperImplTest.java delete mode 100644 tests/src/com/android/dialer/widget/ActionBarControllerTest.java delete mode 100644 tools/gradle/android.properties delete mode 100755 tools/gradle/gradlew delete mode 100644 tools/gradle/repositories.properties delete mode 100644 tools/gradle/settings.gradle diff --git a/Android.mk b/Android.mk index 0978bec05..f7219302a 100644 --- a/Android.mk +++ b/Android.mk @@ -1,73 +1,251 @@ +# Local modifications: +# * All location/maps code has been removed from the incallui. +# * Precompiled AutoValue classes have been included. +# * Precompiled Dagger classes have been included. +# * All autovalue imports and annotations have been stripped. +# * Precompiled proto classes have been included. LOCAL_PATH:= $(call my-dir) include $(CLEAR_VARS) -LOCAL_MODULE_TAGS := optional - -incallui_dir := InCallUI -contacts_common_dir := ../ContactsCommon -phone_common_dir := ../PhoneCommon - ifeq ($(TARGET_BUILD_APPS),) support_library_root_dir := frameworks/support else support_library_root_dir := prebuilts/sdk/current/support endif -src_dirs := src \ - $(incallui_dir)/src \ - $(contacts_common_dir)/src \ - $(phone_common_dir)/src +# The base directory for Dialer sources. +BASE_DIR := java/com/android -res_dirs := res \ - $(incallui_dir)/res \ - $(contacts_common_dir)/res \ - $(contacts_common_dir)/icons/res \ - $(phone_common_dir)/res +# Primary dialer module sources. +SRC_DIRS := \ + $(BASE_DIR)/contacts/common \ + $(BASE_DIR)/dialer \ + $(BASE_DIR)/incallui \ + $(BASE_DIR)/voicemailomtp -src_dirs += \ - src-N +# All Dialers resources. +# find . -type d -name "res" | uniq | sort +RES_DIRS := \ + assets/product/res \ + assets/quantum/res \ + $(BASE_DIR)/contacts/common/res \ + $(BASE_DIR)/dialer/app/res \ + $(BASE_DIR)/dialer/app/voicemail/error/res \ + $(BASE_DIR)/dialer/blocking/res \ + $(BASE_DIR)/dialer/callcomposer/camera/camerafocus/res \ + $(BASE_DIR)/dialer/callcomposer/cameraui/res \ + $(BASE_DIR)/dialer/callcomposer/res \ + $(BASE_DIR)/dialer/common/res \ + $(BASE_DIR)/dialer/dialpadview/res \ + $(BASE_DIR)/dialer/interactions/res \ + $(BASE_DIR)/dialer/phonenumberutil/res \ + $(BASE_DIR)/dialer/shortcuts/res \ + $(BASE_DIR)/dialer/theme/res \ + $(BASE_DIR)/dialer/util/res \ + $(BASE_DIR)/dialer/voicemailstatus/res \ + $(BASE_DIR)/dialer/widget/res \ + $(BASE_DIR)/incallui/answer/impl/affordance/res \ + $(BASE_DIR)/incallui/answer/impl/answermethod/res \ + $(BASE_DIR)/incallui/answer/impl/hint/res \ + $(BASE_DIR)/incallui/answer/impl/res \ + $(BASE_DIR)/incallui/audioroute/res \ + $(BASE_DIR)/incallui/autoresizetext/res \ + $(BASE_DIR)/incallui/commontheme/res \ + $(BASE_DIR)/incallui/contactgrid/res \ + $(BASE_DIR)/incallui/hold/res \ + $(BASE_DIR)/incallui/incall/impl/res \ + $(BASE_DIR)/incallui/res \ + $(BASE_DIR)/incallui/sessiondata/res \ + $(BASE_DIR)/incallui/video/impl/res \ + $(BASE_DIR)/incallui/wifi/res \ + $(BASE_DIR)/voicemailomtp/res -LOCAL_SRC_FILES := $(call all-java-files-under, $(src_dirs)) -LOCAL_RESOURCE_DIR := $(addprefix $(LOCAL_PATH)/, $(res_dirs)) \ - $(support_library_root_dir)/v7/cardview/res \ - $(support_library_root_dir)/v7/recyclerview/res \ - $(support_library_root_dir)/v7/appcompat/res \ - $(support_library_root_dir)/design/res \ - $(support_library_root_dir)/transition/res +# Dialer manifest files to merge. +# find . -type f -name "AndroidManifest.xml" | uniq | sort +DIALER_MANIFEST_FILES += \ + $(BASE_DIR)/contacts/common/AndroidManifest.xml \ + $(BASE_DIR)/dialer/app/AndroidManifest.xml \ + $(BASE_DIR)/dialer/app/manifests/activities/AndroidManifest.xml \ + $(BASE_DIR)/dialer/app/voicemail/error/AndroidManifest.xml \ + $(BASE_DIR)/dialer/backup/AndroidManifest.xml \ + $(BASE_DIR)/dialer/blocking/AndroidManifest.xml \ + $(BASE_DIR)/dialer/callcomposer/AndroidManifest.xml \ + $(BASE_DIR)/dialer/callcomposer/camera/AndroidManifest.xml \ + $(BASE_DIR)/dialer/callcomposer/camera/camerafocus/AndroidManifest.xml \ + $(BASE_DIR)/dialer/callcomposer/cameraui/AndroidManifest.xml \ + $(BASE_DIR)/dialer/common/AndroidManifest.xml \ + $(BASE_DIR)/dialer/debug/AndroidManifest.xml \ + $(BASE_DIR)/dialer/debug/impl/AndroidManifest.xml \ + $(BASE_DIR)/dialer/dialpadview/AndroidManifest.xml \ + $(BASE_DIR)/dialer/interactions/AndroidManifest.xml \ + $(BASE_DIR)/dialer/phonenumberutil/AndroidManifest.xml \ + $(BASE_DIR)/dialer/shortcuts/AndroidManifest.xml \ + $(BASE_DIR)/dialer/simulator/impl/AndroidManifest.xml \ + $(BASE_DIR)/dialer/theme/AndroidManifest.xml \ + $(BASE_DIR)/dialer/util/AndroidManifest.xml \ + $(BASE_DIR)/dialer/voicemailstatus/AndroidManifest.xml \ + $(BASE_DIR)/dialer/widget/AndroidManifest.xml \ + $(BASE_DIR)/incallui/AndroidManifest.xml \ + $(BASE_DIR)/incallui/answer/impl/affordance/AndroidManifest.xml \ + $(BASE_DIR)/incallui/answer/impl/AndroidManifest.xml \ + $(BASE_DIR)/incallui/answer/impl/answermethod/AndroidManifest.xml \ + $(BASE_DIR)/incallui/answer/impl/hint/AndroidManifest.xml \ + $(BASE_DIR)/incallui/audioroute/AndroidManifest.xml \ + $(BASE_DIR)/incallui/autoresizetext/AndroidManifest.xml \ + $(BASE_DIR)/incallui/commontheme/AndroidManifest.xml \ + $(BASE_DIR)/incallui/contactgrid/AndroidManifest.xml \ + $(BASE_DIR)/incallui/hold/AndroidManifest.xml \ + $(BASE_DIR)/incallui/incall/impl/AndroidManifest.xml \ + $(BASE_DIR)/incallui/sessiondata/AndroidManifest.xml \ + $(BASE_DIR)/incallui/video/impl/AndroidManifest.xml \ + $(BASE_DIR)/incallui/wifi/AndroidManifest.xml \ + $(BASE_DIR)/voicemailomtp/AndroidManifest.xml +# Merge all manifest files. +LOCAL_FULL_LIBS_MANIFEST_FILES := \ + $(addprefix $(LOCAL_PATH)/, $(DIALER_MANIFEST_FILES)) +LOCAL_SRC_FILES := $(call all-java-files-under, $(SRC_DIRS)) +LOCAL_RESOURCE_DIR := \ + $(addprefix $(LOCAL_PATH)/, $(RES_DIRS)) \ + $(support_library_root_dir)/design/res \ + $(support_library_root_dir)/v7/appcompat/res \ + $(support_library_root_dir)/v7/cardview/res \ + $(support_library_root_dir)/v7/recyclerview/res + +# We specify each package explicitly to glob resource files. LOCAL_AAPT_FLAGS := \ - --auto-add-overlay \ - --extra-packages android.support.v7.appcompat \ - --extra-packages android.support.v7.cardview \ - --extra-packages android.support.v7.recyclerview \ - --extra-packages android.support.design \ - --extra-packages android.support.transition \ - --extra-packages com.android.incallui \ - --extra-packages com.android.contacts.common \ - --extra-packages com.android.phone.common + --auto-add-overlay \ + --extra-packages android.support.design \ + --extra-packages android.support.transition \ + --extra-packages android.support.v7.appcompat \ + --extra-packages android.support.v7.cardview \ + --extra-packages android.support.v7.recyclerview \ + --extra-packages com.android.contacts.common \ + --extra-packages com.android.dialer.app \ + --extra-packages com.android.dialer.app.voicemail.error \ + --extra-packages com.android.dialer.blocking \ + --extra-packages com.android.dialer.callcomposer \ + --extra-packages com.android.dialer.callcomposer \ + --extra-packages com.android.dialer.callcomposer.camera \ + --extra-packages com.android.dialer.callcomposer.camera.camerafocus \ + --extra-packages com.android.dialer.callcomposer.cameraui \ + --extra-packages com.android.dialer.common \ + --extra-packages com.android.dialer.dialpadview \ + --extra-packages com.android.dialer.interactions \ + --extra-packages com.android.dialer.phonenumberutil \ + --extra-packages com.android.dialer.shortcuts \ + --extra-packages com.android.dialer.util \ + --extra-packages com.android.dialer.voicemailstatus \ + --extra-packages com.android.dialer.widget \ + --extra-packages com.android.incallui \ + --extra-packages com.android.incallui.answer.impl \ + --extra-packages com.android.incallui.answer.impl.affordance \ + --extra-packages com.android.incallui.answer.impl.affordance \ + --extra-packages com.android.incallui.answer.impl.answermethod \ + --extra-packages com.android.incallui.answer.impl.hint \ + --extra-packages com.android.incallui.audioroute \ + --extra-packages com.android.incallui.autoresizetext \ + --extra-packages com.android.incallui.commontheme \ + --extra-packages com.android.incallui.contactgrid \ + --extra-packages com.android.incallui.hold \ + --extra-packages com.android.incallui.incall.impl \ + --extra-packages com.android.incallui.sessiondata \ + --extra-packages com.android.incallui.video \ + --extra-packages com.android.incallui.video.impl \ + --extra-packages com.android.incallui.wifi \ + --extra-packages com.android.phone.common \ + --extra-packages com.android.voicemailomtp \ + --extra-packages com.android.voicemailomtp.settings \ + --extra-packages me.leolin.shortcutbadger LOCAL_STATIC_JAVA_LIBRARIES := \ - android-common \ - android-support-v13 \ - android-support-v4 \ - android-support-v7-appcompat \ - android-support-v7-cardview \ - android-support-v7-recyclerview \ - android-support-design \ - android-support-transition \ - com.android.vcard \ - guava \ - libphonenumber + android-common \ + android-support-design \ + android-support-v13 \ + android-support-v4 \ + android-support-v7-appcompat \ + android-support-v7-cardview \ + android-support-v7-recyclerview \ + com.android.vcard \ + dailer-dagger2-compiler \ + dialer-dagger2 \ + dialer-dagger2-producers \ + dialer-glide \ + dialer-javax-annotation-api \ + dialer-javax-inject \ + dialer-libshortcutbadger \ + jsr305 \ + libphonenumber \ + libprotobuf-java-nano \ + org.apache.http.legacy.boot \ + volley + +LOCAL_JAVA_LIBRARIES := \ + android-support-annotations \ + android-support-transition \ + dailer-dagger2-compiler \ + dialer-dagger2 \ + dialer-dagger2-producers \ + dialer-glide \ + dialer-guava \ + dialer-javax-annotation-api \ + dialer-javax-inject \ + dialer-libshortcutbadger \ + jsr305 \ + libprotobuf-java-nano + +# Libraries needed by the compiler (JACK) to generate code. +PROCESSOR_LIBRARIES_TARGET := \ + dailer-dagger2-compiler \ + dialer-dagger2 \ + dialer-dagger2-producers \ + dialer-guava \ + dialer-javax-annotation-api \ + dialer-javax-inject + +# TODO: Include when JACK properly supports AutoValue b/35360557 +# (builders not generated successfully, javac duplicate issues) in +# LOCAL_STATIC_JAVA_LIBRARIES, LOCAL_JAVA_LIBRARIES, PROCESSOR_LIBRARIES_TARGET +# dialer-auto-value + +# Resolve the jar paths. +PROCESSOR_JARS := $(call java-lib-deps, $(PROCESSOR_LIBRARIES_TARGET)) +LOCAL_ADDITIONAL_DEPENDENCIES += $(PROCESSOR_JARS) + +LOCAL_JACK_FLAGS += --processorpath $(call normalize-path-list,$(PROCESSOR_JARS)) + +LOCAL_PROGUARD_FLAG_FILES := proguard.flags $(incallui_dir)/proguard.flags +LOCAL_SDK_VERSION := current +LOCAL_MODULE_TAGS := optional LOCAL_PACKAGE_NAME := Dialer LOCAL_CERTIFICATE := shared LOCAL_PRIVILEGED_MODULE := true +include $(BUILD_PACKAGE) -LOCAL_PROGUARD_FLAG_FILES := proguard.flags $(incallui_dir)/proguard.flags +# Cleanup local state +BASE_DIR := +SRC_DIRS := +RES_DIRS := +DIALER_MANIFEST_FILES := +PROCESSOR_LIBRARIES_TARGET := +PROCESSOR_JARS := -LOCAL_SDK_VERSION := current +# Create references to prebuilt libraries. +include $(CLEAR_VARS) -include $(BUILD_PACKAGE) +LOCAL_PREBUILT_STATIC_JAVA_LIBRARIES := \ + dailer-dagger2-compiler:../../../prebuilts/tools/common/m2/repository/com/google/dagger/dagger-compiler/2.6/dagger-compiler-2.6$(COMMON_JAVA_PACKAGE_SUFFIX) \ + dialer-auto-common:../../../prebuilts/tools/common/m2/repository/com/google/auto/auto-common/0.6/auto-common-0.6$(COMMON_JAVA_PACKAGE_SUFFIX) \ + dialer-auto-value:../../../prebuilts/tools/common/m2/repository/com/google/auto/value/auto-value/1.3/auto-value-1.3$(COMMON_JAVA_PACKAGE_SUFFIX) \ + dialer-dagger2:../../../prebuilts/tools/common/m2/repository/com/google/dagger/dagger/2.6/dagger-2.6$(COMMON_JAVA_PACKAGE_SUFFIX) \ + dialer-dagger2-producers:../../../prebuilts/tools/common/m2/repository/com/google/dagger/dagger-producers/2.6/dagger-producers-2.6$(COMMON_JAVA_PACKAGE_SUFFIX) \ + dialer-glide:../../../prebuilts/maven_repo/bumptech/com/github/bumptech/glide/glide/4.0.0-SNAPSHOT/glide-4.0.0-SNAPSHOT$(COMMON_JAVA_PACKAGE_SUFFIX) \ + dialer-guava:../../../prebuilts/tools/common/m2/repository/com/google/guava/guava/20.0/guava-20.0$(COMMON_JAVA_PACKAGE_SUFFIX) \ + dialer-javax-annotation-api:../../../prebuilts/tools/common/m2/repository/javax/annotation/javax.annotation-api/1.2/javax.annotation-api-1.2$(COMMON_JAVA_PACKAGE_SUFFIX) \ + dialer-javax-inject:../../../prebuilts/tools/common/m2/repository/javax/inject/javax.inject/1/javax.inject-1$(COMMON_JAVA_PACKAGE_SUFFIX) \ + dialer-libshortcutbadger:../../../prebuilts/tools/common/m2/repository/me/leolin/ShortcutBadger/1.1.13/ShortcutBadger-1.1.13$(COMMON_JAVA_PACKAGE_SUFFIX) -# Use the following include to make our test apk. -include $(call all-makefiles-under,$(LOCAL_PATH)) +include $(BUILD_MULTI_PREBUILT) + +include $(CLEAR_VARS) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index d83080376..85ed1981c 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1,5 +1,4 @@ - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + coreApp="true" + package="com.android.dialer" + android:versionCode="90000" + android:versionName="9.0"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - diff --git a/CONTRIBUTING b/CONTRIBUTING new file mode 100644 index 000000000..2827b7d3f --- /dev/null +++ b/CONTRIBUTING @@ -0,0 +1,27 @@ +Want to contribute? Great! First, read this page (including the small print at the end). + +### Before you contribute +Before we can use your code, you must sign the +[Google Individual Contributor License Agreement] +(https://cla.developers.google.com/about/google-individual) +(CLA), which you can do online. The CLA is necessary mainly because you own the +copyright to your changes, even after your contribution becomes part of our +codebase, so we need your permission to use and distribute your code. We also +need to be sure of various other things—for instance that you'll tell us if you +know that your code infringes on other people's patents. You don't have to sign +the CLA until after you've submitted your code for review and a member has +approved it, but you must do it before we can put your code into our codebase. +Before you start working on a larger contribution, you should get in touch with +us first through the issue tracker with your idea so that we can help out and +possibly guide you. Coordinating up front makes it much easier to avoid +frustration later on. + +### Code reviews +All submissions, including submissions by project members, require review. We +use Github pull requests for this purpose. + +### The small print +Contributions made by corporations are covered by a different agreement than +the one above, the +[Software Grant and Corporate Contributor License Agreement] +(https://cla.developers.google.com/about/google-corporate). diff --git a/InCallUI/AndroidManifest.xml b/InCallUI/AndroidManifest.xml deleted file mode 100644 index 5c758edaa..000000000 --- a/InCallUI/AndroidManifest.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - diff --git a/InCallUI/build.gradle b/InCallUI/build.gradle deleted file mode 100644 index de4725199..000000000 --- a/InCallUI/build.gradle +++ /dev/null @@ -1,14 +0,0 @@ -apply plugin: 'com.android.library' - -android { - sourceSets.main { - manifest.srcFile 'AndroidManifest.xml' - res.srcDirs = ['res'] - } -} - -dependencies { - compile 'com.android.support:support-v4:23.1.+' - compile project(':phonecommon') - compile project(':contactscommon') -} diff --git a/InCallUI/proguard.flags b/InCallUI/proguard.flags deleted file mode 100644 index 4e8310ca9..000000000 --- a/InCallUI/proguard.flags +++ /dev/null @@ -1,14 +0,0 @@ --keep class com.android.incallui.widget.multiwaveview.* { - *; -} - -# Keep names that are used only by animation framework. --keepclasseswithmembers class com.android.incallui.AnimationUtils$CrossFadeDrawable { - *** setCrossFadeAlpha(...); -} - -# Any class or method annotated with NeededForTesting or NeededForReflection. --keepclassmembers class * { -@com.android.contacts.common.test.NeededForTesting *; -@com.android.incallui.NeededForReflection *; -} diff --git a/InCallUI/res/anim/activity_open_enter.xml b/InCallUI/res/anim/activity_open_enter.xml deleted file mode 100644 index 303b9ddc0..000000000 --- a/InCallUI/res/anim/activity_open_enter.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/InCallUI/res/anim/activity_open_exit.xml b/InCallUI/res/anim/activity_open_exit.xml deleted file mode 100644 index afa7c5e72..000000000 --- a/InCallUI/res/anim/activity_open_exit.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/InCallUI/res/anim/call_status_pulse.xml b/InCallUI/res/anim/call_status_pulse.xml deleted file mode 100644 index abda25b73..000000000 --- a/InCallUI/res/anim/call_status_pulse.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - diff --git a/InCallUI/res/anim/decelerate_cubic.xml b/InCallUI/res/anim/decelerate_cubic.xml deleted file mode 100644 index f98809165..000000000 --- a/InCallUI/res/anim/decelerate_cubic.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - diff --git a/InCallUI/res/anim/decelerate_quint.xml b/InCallUI/res/anim/decelerate_quint.xml deleted file mode 100644 index ff2d5a9d0..000000000 --- a/InCallUI/res/anim/decelerate_quint.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - diff --git a/InCallUI/res/color/ota_title_color.xml b/InCallUI/res/color/ota_title_color.xml deleted file mode 100644 index 14a283a6b..000000000 --- a/InCallUI/res/color/ota_title_color.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - diff --git a/InCallUI/res/color/selectable_icon_tint.xml b/InCallUI/res/color/selectable_icon_tint.xml deleted file mode 100644 index b8aad1303..000000000 --- a/InCallUI/res/color/selectable_icon_tint.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - diff --git a/InCallUI/res/drawable-hdpi/fab_blue.png b/InCallUI/res/drawable-hdpi/fab_blue.png deleted file mode 100644 index 8ff3d2918..000000000 Binary files a/InCallUI/res/drawable-hdpi/fab_blue.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/fab_ic_call.png b/InCallUI/res/drawable-hdpi/fab_ic_call.png deleted file mode 100644 index 548a391a6..000000000 Binary files a/InCallUI/res/drawable-hdpi/fab_ic_call.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/fab_ic_end_call.png b/InCallUI/res/drawable-hdpi/fab_ic_end_call.png deleted file mode 100644 index b7f54d3bb..000000000 Binary files a/InCallUI/res/drawable-hdpi/fab_ic_end_call.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/fab_ic_message.png b/InCallUI/res/drawable-hdpi/fab_ic_message.png deleted file mode 100644 index a1cf2ad82..000000000 Binary files a/InCallUI/res/drawable-hdpi/fab_ic_message.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/fab_red.png b/InCallUI/res/drawable-hdpi/fab_red.png deleted file mode 100644 index 497cc7916..000000000 Binary files a/InCallUI/res/drawable-hdpi/fab_red.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/ic_block_grey600_24dp.png b/InCallUI/res/drawable-hdpi/ic_block_grey600_24dp.png deleted file mode 100644 index 1e9294c12..000000000 Binary files a/InCallUI/res/drawable-hdpi/ic_block_grey600_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/ic_business_white_24dp.png b/InCallUI/res/drawable-hdpi/ic_business_white_24dp.png deleted file mode 100644 index d10ebb766..000000000 Binary files a/InCallUI/res/drawable-hdpi/ic_business_white_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/ic_call_end_white_24dp.png b/InCallUI/res/drawable-hdpi/ic_call_end_white_24dp.png deleted file mode 100644 index 757d339c4..000000000 Binary files a/InCallUI/res/drawable-hdpi/ic_call_end_white_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/ic_call_split_white_24dp.png b/InCallUI/res/drawable-hdpi/ic_call_split_white_24dp.png deleted file mode 100644 index 4e3dbf55d..000000000 Binary files a/InCallUI/res/drawable-hdpi/ic_call_split_white_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/ic_call_white_24dp.png b/InCallUI/res/drawable-hdpi/ic_call_white_24dp.png deleted file mode 100644 index 1902e721b..000000000 Binary files a/InCallUI/res/drawable-hdpi/ic_call_white_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/ic_close_grey600_24dp.png b/InCallUI/res/drawable-hdpi/ic_close_grey600_24dp.png deleted file mode 100644 index 9ab350e9a..000000000 Binary files a/InCallUI/res/drawable-hdpi/ic_close_grey600_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/ic_forward_white_24dp.png b/InCallUI/res/drawable-hdpi/ic_forward_white_24dp.png deleted file mode 100644 index a0711d377..000000000 Binary files a/InCallUI/res/drawable-hdpi/ic_forward_white_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/ic_hd_24dp.png b/InCallUI/res/drawable-hdpi/ic_hd_24dp.png deleted file mode 100644 index 35bf51a4f..000000000 Binary files a/InCallUI/res/drawable-hdpi/ic_hd_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/ic_location_on_white_24dp.png b/InCallUI/res/drawable-hdpi/ic_location_on_white_24dp.png deleted file mode 100644 index 7c281c3f5..000000000 Binary files a/InCallUI/res/drawable-hdpi/ic_location_on_white_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/ic_lockscreen_glowdot.png b/InCallUI/res/drawable-hdpi/ic_lockscreen_glowdot.png deleted file mode 100644 index 983c45e2c..000000000 Binary files a/InCallUI/res/drawable-hdpi/ic_lockscreen_glowdot.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/ic_person_add_grey600_24dp.png b/InCallUI/res/drawable-hdpi/ic_person_add_grey600_24dp.png deleted file mode 100644 index 185d03393..000000000 Binary files a/InCallUI/res/drawable-hdpi/ic_person_add_grey600_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/ic_phone_paused_white_24dp.png b/InCallUI/res/drawable-hdpi/ic_phone_paused_white_24dp.png deleted file mode 100644 index a2177f58a..000000000 Binary files a/InCallUI/res/drawable-hdpi/ic_phone_paused_white_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/ic_question_mark.png b/InCallUI/res/drawable-hdpi/ic_question_mark.png deleted file mode 100644 index adab6c13f..000000000 Binary files a/InCallUI/res/drawable-hdpi/ic_question_mark.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/ic_report_white_36dp.png b/InCallUI/res/drawable-hdpi/ic_report_white_36dp.png deleted file mode 100644 index 919a872e0..000000000 Binary files a/InCallUI/res/drawable-hdpi/ic_report_white_36dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/ic_schedule_white_24dp.png b/InCallUI/res/drawable-hdpi/ic_schedule_white_24dp.png deleted file mode 100644 index f3581d104..000000000 Binary files a/InCallUI/res/drawable-hdpi/ic_schedule_white_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/ic_toolbar_add_call.png b/InCallUI/res/drawable-hdpi/ic_toolbar_add_call.png deleted file mode 100644 index 06603f21c..000000000 Binary files a/InCallUI/res/drawable-hdpi/ic_toolbar_add_call.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/ic_toolbar_arrow_whitespace.png b/InCallUI/res/drawable-hdpi/ic_toolbar_arrow_whitespace.png deleted file mode 100644 index ea02daad2..000000000 Binary files a/InCallUI/res/drawable-hdpi/ic_toolbar_arrow_whitespace.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/ic_toolbar_audio_bluetooth.png b/InCallUI/res/drawable-hdpi/ic_toolbar_audio_bluetooth.png deleted file mode 100644 index 05e19bc25..000000000 Binary files a/InCallUI/res/drawable-hdpi/ic_toolbar_audio_bluetooth.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/ic_toolbar_audio_headphones.png b/InCallUI/res/drawable-hdpi/ic_toolbar_audio_headphones.png deleted file mode 100644 index 413fdff26..000000000 Binary files a/InCallUI/res/drawable-hdpi/ic_toolbar_audio_headphones.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/ic_toolbar_audio_phone.png b/InCallUI/res/drawable-hdpi/ic_toolbar_audio_phone.png deleted file mode 100644 index 90ee1fb5f..000000000 Binary files a/InCallUI/res/drawable-hdpi/ic_toolbar_audio_phone.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/ic_toolbar_dialpad.png b/InCallUI/res/drawable-hdpi/ic_toolbar_dialpad.png deleted file mode 100644 index 69ece11be..000000000 Binary files a/InCallUI/res/drawable-hdpi/ic_toolbar_dialpad.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/ic_toolbar_hold.png b/InCallUI/res/drawable-hdpi/ic_toolbar_hold.png deleted file mode 100644 index f32d6d552..000000000 Binary files a/InCallUI/res/drawable-hdpi/ic_toolbar_hold.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/ic_toolbar_merge.png b/InCallUI/res/drawable-hdpi/ic_toolbar_merge.png deleted file mode 100644 index 2871555e4..000000000 Binary files a/InCallUI/res/drawable-hdpi/ic_toolbar_merge.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/ic_toolbar_mic_off.png b/InCallUI/res/drawable-hdpi/ic_toolbar_mic_off.png deleted file mode 100644 index b142ca869..000000000 Binary files a/InCallUI/res/drawable-hdpi/ic_toolbar_mic_off.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/ic_toolbar_speaker_on.png b/InCallUI/res/drawable-hdpi/ic_toolbar_speaker_on.png deleted file mode 100644 index c934b1344..000000000 Binary files a/InCallUI/res/drawable-hdpi/ic_toolbar_speaker_on.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/ic_toolbar_swap.png b/InCallUI/res/drawable-hdpi/ic_toolbar_swap.png deleted file mode 100644 index e673f3251..000000000 Binary files a/InCallUI/res/drawable-hdpi/ic_toolbar_swap.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/ic_toolbar_video.png b/InCallUI/res/drawable-hdpi/ic_toolbar_video.png deleted file mode 100644 index cef47aaff..000000000 Binary files a/InCallUI/res/drawable-hdpi/ic_toolbar_video.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/ic_toolbar_video_off.png b/InCallUI/res/drawable-hdpi/ic_toolbar_video_off.png deleted file mode 100644 index 968ded7d8..000000000 Binary files a/InCallUI/res/drawable-hdpi/ic_toolbar_video_off.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/ic_toolbar_video_switch.png b/InCallUI/res/drawable-hdpi/ic_toolbar_video_switch.png deleted file mode 100644 index cdd623dc0..000000000 Binary files a/InCallUI/res/drawable-hdpi/ic_toolbar_video_switch.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/img_business.png b/InCallUI/res/drawable-hdpi/img_business.png deleted file mode 100644 index f70634262..000000000 Binary files a/InCallUI/res/drawable-hdpi/img_business.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/img_conference.png b/InCallUI/res/drawable-hdpi/img_conference.png deleted file mode 100644 index 3d9f683a5..000000000 Binary files a/InCallUI/res/drawable-hdpi/img_conference.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/img_no_image.png b/InCallUI/res/drawable-hdpi/img_no_image.png deleted file mode 100644 index fd0ab3211..000000000 Binary files a/InCallUI/res/drawable-hdpi/img_no_image.png and /dev/null differ diff --git a/InCallUI/res/drawable-hdpi/img_phone.png b/InCallUI/res/drawable-hdpi/img_phone.png deleted file mode 100644 index 748312e6e..000000000 Binary files a/InCallUI/res/drawable-hdpi/img_phone.png and /dev/null differ diff --git a/InCallUI/res/drawable-land/rounded_call_card_background.xml b/InCallUI/res/drawable-land/rounded_call_card_background.xml deleted file mode 100644 index f41ecda79..000000000 --- a/InCallUI/res/drawable-land/rounded_call_card_background.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/InCallUI/res/drawable-mdpi/fab_blue.png b/InCallUI/res/drawable-mdpi/fab_blue.png deleted file mode 100644 index 2ca6b4bdf..000000000 Binary files a/InCallUI/res/drawable-mdpi/fab_blue.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/fab_ic_call.png b/InCallUI/res/drawable-mdpi/fab_ic_call.png deleted file mode 100644 index ff7b345e1..000000000 Binary files a/InCallUI/res/drawable-mdpi/fab_ic_call.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/fab_ic_end_call.png b/InCallUI/res/drawable-mdpi/fab_ic_end_call.png deleted file mode 100644 index 76ce3973d..000000000 Binary files a/InCallUI/res/drawable-mdpi/fab_ic_end_call.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/fab_ic_message.png b/InCallUI/res/drawable-mdpi/fab_ic_message.png deleted file mode 100644 index 74876fe77..000000000 Binary files a/InCallUI/res/drawable-mdpi/fab_ic_message.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/fab_red.png b/InCallUI/res/drawable-mdpi/fab_red.png deleted file mode 100644 index c9e76a057..000000000 Binary files a/InCallUI/res/drawable-mdpi/fab_red.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/ic_block_grey600_24dp.png b/InCallUI/res/drawable-mdpi/ic_block_grey600_24dp.png deleted file mode 100644 index edd666b73..000000000 Binary files a/InCallUI/res/drawable-mdpi/ic_block_grey600_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/ic_business_white_24dp.png b/InCallUI/res/drawable-mdpi/ic_business_white_24dp.png deleted file mode 100644 index 7b9227c06..000000000 Binary files a/InCallUI/res/drawable-mdpi/ic_business_white_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/ic_call_end_white_24dp.png b/InCallUI/res/drawable-mdpi/ic_call_end_white_24dp.png deleted file mode 100644 index 17eb4824e..000000000 Binary files a/InCallUI/res/drawable-mdpi/ic_call_end_white_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/ic_call_split_white_24dp.png b/InCallUI/res/drawable-mdpi/ic_call_split_white_24dp.png deleted file mode 100644 index cb7ee1f35..000000000 Binary files a/InCallUI/res/drawable-mdpi/ic_call_split_white_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/ic_call_white_24dp.png b/InCallUI/res/drawable-mdpi/ic_call_white_24dp.png deleted file mode 100644 index d4e5f5d7d..000000000 Binary files a/InCallUI/res/drawable-mdpi/ic_call_white_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/ic_close_grey600_24dp.png b/InCallUI/res/drawable-mdpi/ic_close_grey600_24dp.png deleted file mode 100644 index 73faf52eb..000000000 Binary files a/InCallUI/res/drawable-mdpi/ic_close_grey600_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/ic_forward_white_24dp.png b/InCallUI/res/drawable-mdpi/ic_forward_white_24dp.png deleted file mode 100644 index 65f73299f..000000000 Binary files a/InCallUI/res/drawable-mdpi/ic_forward_white_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/ic_hd_24dp.png b/InCallUI/res/drawable-mdpi/ic_hd_24dp.png deleted file mode 100644 index 30938fe4d..000000000 Binary files a/InCallUI/res/drawable-mdpi/ic_hd_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/ic_location_on_white_24dp.png b/InCallUI/res/drawable-mdpi/ic_location_on_white_24dp.png deleted file mode 100644 index 933eb5148..000000000 Binary files a/InCallUI/res/drawable-mdpi/ic_location_on_white_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/ic_lockscreen_glowdot.png b/InCallUI/res/drawable-mdpi/ic_lockscreen_glowdot.png deleted file mode 100644 index 056c3f175..000000000 Binary files a/InCallUI/res/drawable-mdpi/ic_lockscreen_glowdot.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/ic_person_add_grey600_24dp.png b/InCallUI/res/drawable-mdpi/ic_person_add_grey600_24dp.png deleted file mode 100644 index ec3237086..000000000 Binary files a/InCallUI/res/drawable-mdpi/ic_person_add_grey600_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/ic_phone_paused_white_24dp.png b/InCallUI/res/drawable-mdpi/ic_phone_paused_white_24dp.png deleted file mode 100644 index 7dc920b2b..000000000 Binary files a/InCallUI/res/drawable-mdpi/ic_phone_paused_white_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/ic_question_mark.png b/InCallUI/res/drawable-mdpi/ic_question_mark.png deleted file mode 100644 index cfe64f696..000000000 Binary files a/InCallUI/res/drawable-mdpi/ic_question_mark.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/ic_report_white_36dp.png b/InCallUI/res/drawable-mdpi/ic_report_white_36dp.png deleted file mode 100644 index dc0c995c1..000000000 Binary files a/InCallUI/res/drawable-mdpi/ic_report_white_36dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/ic_schedule_white_24dp.png b/InCallUI/res/drawable-mdpi/ic_schedule_white_24dp.png deleted file mode 100644 index 501ee842e..000000000 Binary files a/InCallUI/res/drawable-mdpi/ic_schedule_white_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/ic_toolbar_add_call.png b/InCallUI/res/drawable-mdpi/ic_toolbar_add_call.png deleted file mode 100644 index 1ee2fb1f5..000000000 Binary files a/InCallUI/res/drawable-mdpi/ic_toolbar_add_call.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/ic_toolbar_arrow_whitespace.png b/InCallUI/res/drawable-mdpi/ic_toolbar_arrow_whitespace.png deleted file mode 100644 index c39990deb..000000000 Binary files a/InCallUI/res/drawable-mdpi/ic_toolbar_arrow_whitespace.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/ic_toolbar_audio_bluetooth.png b/InCallUI/res/drawable-mdpi/ic_toolbar_audio_bluetooth.png deleted file mode 100644 index a6634ed66..000000000 Binary files a/InCallUI/res/drawable-mdpi/ic_toolbar_audio_bluetooth.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/ic_toolbar_audio_headphones.png b/InCallUI/res/drawable-mdpi/ic_toolbar_audio_headphones.png deleted file mode 100644 index b387e850a..000000000 Binary files a/InCallUI/res/drawable-mdpi/ic_toolbar_audio_headphones.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/ic_toolbar_audio_phone.png b/InCallUI/res/drawable-mdpi/ic_toolbar_audio_phone.png deleted file mode 100644 index b4d887cf3..000000000 Binary files a/InCallUI/res/drawable-mdpi/ic_toolbar_audio_phone.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/ic_toolbar_dialpad.png b/InCallUI/res/drawable-mdpi/ic_toolbar_dialpad.png deleted file mode 100644 index 9baa21b95..000000000 Binary files a/InCallUI/res/drawable-mdpi/ic_toolbar_dialpad.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/ic_toolbar_hold.png b/InCallUI/res/drawable-mdpi/ic_toolbar_hold.png deleted file mode 100644 index c8372738b..000000000 Binary files a/InCallUI/res/drawable-mdpi/ic_toolbar_hold.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/ic_toolbar_merge.png b/InCallUI/res/drawable-mdpi/ic_toolbar_merge.png deleted file mode 100644 index 2fba86514..000000000 Binary files a/InCallUI/res/drawable-mdpi/ic_toolbar_merge.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/ic_toolbar_mic_off.png b/InCallUI/res/drawable-mdpi/ic_toolbar_mic_off.png deleted file mode 100644 index c6b02b82c..000000000 Binary files a/InCallUI/res/drawable-mdpi/ic_toolbar_mic_off.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/ic_toolbar_speaker_on.png b/InCallUI/res/drawable-mdpi/ic_toolbar_speaker_on.png deleted file mode 100644 index 008e245f8..000000000 Binary files a/InCallUI/res/drawable-mdpi/ic_toolbar_speaker_on.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/ic_toolbar_swap.png b/InCallUI/res/drawable-mdpi/ic_toolbar_swap.png deleted file mode 100644 index acc9850d5..000000000 Binary files a/InCallUI/res/drawable-mdpi/ic_toolbar_swap.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/ic_toolbar_video.png b/InCallUI/res/drawable-mdpi/ic_toolbar_video.png deleted file mode 100644 index 3f13f9c31..000000000 Binary files a/InCallUI/res/drawable-mdpi/ic_toolbar_video.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/ic_toolbar_video_off.png b/InCallUI/res/drawable-mdpi/ic_toolbar_video_off.png deleted file mode 100644 index 64a69f2a7..000000000 Binary files a/InCallUI/res/drawable-mdpi/ic_toolbar_video_off.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/ic_toolbar_video_switch.png b/InCallUI/res/drawable-mdpi/ic_toolbar_video_switch.png deleted file mode 100644 index 6d097c9e7..000000000 Binary files a/InCallUI/res/drawable-mdpi/ic_toolbar_video_switch.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/img_business.png b/InCallUI/res/drawable-mdpi/img_business.png deleted file mode 100644 index 90738a7ee..000000000 Binary files a/InCallUI/res/drawable-mdpi/img_business.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/img_conference.png b/InCallUI/res/drawable-mdpi/img_conference.png deleted file mode 100644 index 0694dbd55..000000000 Binary files a/InCallUI/res/drawable-mdpi/img_conference.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/img_no_image.png b/InCallUI/res/drawable-mdpi/img_no_image.png deleted file mode 100644 index 014a1c414..000000000 Binary files a/InCallUI/res/drawable-mdpi/img_no_image.png and /dev/null differ diff --git a/InCallUI/res/drawable-mdpi/img_phone.png b/InCallUI/res/drawable-mdpi/img_phone.png deleted file mode 100644 index 41a1d339d..000000000 Binary files a/InCallUI/res/drawable-mdpi/img_phone.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/fab_blue.png b/InCallUI/res/drawable-xhdpi/fab_blue.png deleted file mode 100644 index 300b07eb4..000000000 Binary files a/InCallUI/res/drawable-xhdpi/fab_blue.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/fab_ic_call.png b/InCallUI/res/drawable-xhdpi/fab_ic_call.png deleted file mode 100644 index 2bff65e0a..000000000 Binary files a/InCallUI/res/drawable-xhdpi/fab_ic_call.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/fab_ic_end_call.png b/InCallUI/res/drawable-xhdpi/fab_ic_end_call.png deleted file mode 100644 index 1c95e175a..000000000 Binary files a/InCallUI/res/drawable-xhdpi/fab_ic_end_call.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/fab_ic_message.png b/InCallUI/res/drawable-xhdpi/fab_ic_message.png deleted file mode 100644 index 5e3334ae0..000000000 Binary files a/InCallUI/res/drawable-xhdpi/fab_ic_message.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/fab_red.png b/InCallUI/res/drawable-xhdpi/fab_red.png deleted file mode 100644 index 373e49e8f..000000000 Binary files a/InCallUI/res/drawable-xhdpi/fab_red.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/ic_block_grey600_24dp.png b/InCallUI/res/drawable-xhdpi/ic_block_grey600_24dp.png deleted file mode 100644 index 36210a8cb..000000000 Binary files a/InCallUI/res/drawable-xhdpi/ic_block_grey600_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/ic_business_white_24dp.png b/InCallUI/res/drawable-xhdpi/ic_business_white_24dp.png deleted file mode 100644 index e5630455a..000000000 Binary files a/InCallUI/res/drawable-xhdpi/ic_business_white_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/ic_call_end_white_24dp.png b/InCallUI/res/drawable-xhdpi/ic_call_end_white_24dp.png deleted file mode 100644 index b00d82edd..000000000 Binary files a/InCallUI/res/drawable-xhdpi/ic_call_end_white_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/ic_call_split_white_24dp.png b/InCallUI/res/drawable-xhdpi/ic_call_split_white_24dp.png deleted file mode 100644 index 218cb1214..000000000 Binary files a/InCallUI/res/drawable-xhdpi/ic_call_split_white_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/ic_call_white_24dp.png b/InCallUI/res/drawable-xhdpi/ic_call_white_24dp.png deleted file mode 100644 index cde9cea3a..000000000 Binary files a/InCallUI/res/drawable-xhdpi/ic_call_white_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/ic_close_grey600_24dp.png b/InCallUI/res/drawable-xhdpi/ic_close_grey600_24dp.png deleted file mode 100644 index a3896c5c6..000000000 Binary files a/InCallUI/res/drawable-xhdpi/ic_close_grey600_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/ic_forward_white_24dp.png b/InCallUI/res/drawable-xhdpi/ic_forward_white_24dp.png deleted file mode 100644 index 7a5df52bf..000000000 Binary files a/InCallUI/res/drawable-xhdpi/ic_forward_white_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/ic_hd_24dp.png b/InCallUI/res/drawable-xhdpi/ic_hd_24dp.png deleted file mode 100644 index 4c954d86f..000000000 Binary files a/InCallUI/res/drawable-xhdpi/ic_hd_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/ic_location_on_white_24dp.png b/InCallUI/res/drawable-xhdpi/ic_location_on_white_24dp.png deleted file mode 100644 index 814ca8ddc..000000000 Binary files a/InCallUI/res/drawable-xhdpi/ic_location_on_white_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/ic_lockscreen_glowdot.png b/InCallUI/res/drawable-xhdpi/ic_lockscreen_glowdot.png deleted file mode 100644 index cbd039afd..000000000 Binary files a/InCallUI/res/drawable-xhdpi/ic_lockscreen_glowdot.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/ic_person_add_grey600_24dp.png b/InCallUI/res/drawable-xhdpi/ic_person_add_grey600_24dp.png deleted file mode 100644 index e56481ed7..000000000 Binary files a/InCallUI/res/drawable-xhdpi/ic_person_add_grey600_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/ic_phone_paused_white_24dp.png b/InCallUI/res/drawable-xhdpi/ic_phone_paused_white_24dp.png deleted file mode 100644 index a8becf485..000000000 Binary files a/InCallUI/res/drawable-xhdpi/ic_phone_paused_white_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/ic_question_mark.png b/InCallUI/res/drawable-xhdpi/ic_question_mark.png deleted file mode 100644 index 8da487088..000000000 Binary files a/InCallUI/res/drawable-xhdpi/ic_question_mark.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/ic_report_white_36dp.png b/InCallUI/res/drawable-xhdpi/ic_report_white_36dp.png deleted file mode 100644 index aed766804..000000000 Binary files a/InCallUI/res/drawable-xhdpi/ic_report_white_36dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/ic_schedule_white_24dp.png b/InCallUI/res/drawable-xhdpi/ic_schedule_white_24dp.png deleted file mode 100644 index 2e27936a4..000000000 Binary files a/InCallUI/res/drawable-xhdpi/ic_schedule_white_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/ic_toolbar_add_call.png b/InCallUI/res/drawable-xhdpi/ic_toolbar_add_call.png deleted file mode 100644 index b251d6bd8..000000000 Binary files a/InCallUI/res/drawable-xhdpi/ic_toolbar_add_call.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/ic_toolbar_arrow_whitespace.png b/InCallUI/res/drawable-xhdpi/ic_toolbar_arrow_whitespace.png deleted file mode 100644 index cdaa79d37..000000000 Binary files a/InCallUI/res/drawable-xhdpi/ic_toolbar_arrow_whitespace.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/ic_toolbar_audio_bluetooth.png b/InCallUI/res/drawable-xhdpi/ic_toolbar_audio_bluetooth.png deleted file mode 100644 index 88f6bb945..000000000 Binary files a/InCallUI/res/drawable-xhdpi/ic_toolbar_audio_bluetooth.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/ic_toolbar_audio_headphones.png b/InCallUI/res/drawable-xhdpi/ic_toolbar_audio_headphones.png deleted file mode 100644 index 1acfcafbd..000000000 Binary files a/InCallUI/res/drawable-xhdpi/ic_toolbar_audio_headphones.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/ic_toolbar_audio_phone.png b/InCallUI/res/drawable-xhdpi/ic_toolbar_audio_phone.png deleted file mode 100644 index 0ba8f1e3e..000000000 Binary files a/InCallUI/res/drawable-xhdpi/ic_toolbar_audio_phone.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/ic_toolbar_dialpad.png b/InCallUI/res/drawable-xhdpi/ic_toolbar_dialpad.png deleted file mode 100644 index cf803d1c1..000000000 Binary files a/InCallUI/res/drawable-xhdpi/ic_toolbar_dialpad.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/ic_toolbar_hold.png b/InCallUI/res/drawable-xhdpi/ic_toolbar_hold.png deleted file mode 100644 index 8fecf7514..000000000 Binary files a/InCallUI/res/drawable-xhdpi/ic_toolbar_hold.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/ic_toolbar_merge.png b/InCallUI/res/drawable-xhdpi/ic_toolbar_merge.png deleted file mode 100644 index 777483eb0..000000000 Binary files a/InCallUI/res/drawable-xhdpi/ic_toolbar_merge.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/ic_toolbar_mic_off.png b/InCallUI/res/drawable-xhdpi/ic_toolbar_mic_off.png deleted file mode 100644 index cf2041ad6..000000000 Binary files a/InCallUI/res/drawable-xhdpi/ic_toolbar_mic_off.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/ic_toolbar_speaker_on.png b/InCallUI/res/drawable-xhdpi/ic_toolbar_speaker_on.png deleted file mode 100644 index 5b5831cc0..000000000 Binary files a/InCallUI/res/drawable-xhdpi/ic_toolbar_speaker_on.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/ic_toolbar_swap.png b/InCallUI/res/drawable-xhdpi/ic_toolbar_swap.png deleted file mode 100644 index 38917cb88..000000000 Binary files a/InCallUI/res/drawable-xhdpi/ic_toolbar_swap.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/ic_toolbar_video.png b/InCallUI/res/drawable-xhdpi/ic_toolbar_video.png deleted file mode 100644 index b20f50498..000000000 Binary files a/InCallUI/res/drawable-xhdpi/ic_toolbar_video.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/ic_toolbar_video_off.png b/InCallUI/res/drawable-xhdpi/ic_toolbar_video_off.png deleted file mode 100644 index 1b269a6a7..000000000 Binary files a/InCallUI/res/drawable-xhdpi/ic_toolbar_video_off.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/ic_toolbar_video_switch.png b/InCallUI/res/drawable-xhdpi/ic_toolbar_video_switch.png deleted file mode 100644 index fae6bfdb1..000000000 Binary files a/InCallUI/res/drawable-xhdpi/ic_toolbar_video_switch.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/img_business.png b/InCallUI/res/drawable-xhdpi/img_business.png deleted file mode 100644 index 7b04d956f..000000000 Binary files a/InCallUI/res/drawable-xhdpi/img_business.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/img_conference.png b/InCallUI/res/drawable-xhdpi/img_conference.png deleted file mode 100644 index b0dbcc2dc..000000000 Binary files a/InCallUI/res/drawable-xhdpi/img_conference.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/img_no_image.png b/InCallUI/res/drawable-xhdpi/img_no_image.png deleted file mode 100644 index 4022207d0..000000000 Binary files a/InCallUI/res/drawable-xhdpi/img_no_image.png and /dev/null differ diff --git a/InCallUI/res/drawable-xhdpi/img_phone.png b/InCallUI/res/drawable-xhdpi/img_phone.png deleted file mode 100644 index 2e0ceec0f..000000000 Binary files a/InCallUI/res/drawable-xhdpi/img_phone.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/fab_blue.png b/InCallUI/res/drawable-xxhdpi/fab_blue.png deleted file mode 100644 index 76d68ac6a..000000000 Binary files a/InCallUI/res/drawable-xxhdpi/fab_blue.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/fab_ic_call.png b/InCallUI/res/drawable-xxhdpi/fab_ic_call.png deleted file mode 100644 index a756b95ad..000000000 Binary files a/InCallUI/res/drawable-xxhdpi/fab_ic_call.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/fab_ic_end_call.png b/InCallUI/res/drawable-xxhdpi/fab_ic_end_call.png deleted file mode 100644 index 37e826402..000000000 Binary files a/InCallUI/res/drawable-xxhdpi/fab_ic_end_call.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/fab_ic_message.png b/InCallUI/res/drawable-xxhdpi/fab_ic_message.png deleted file mode 100644 index 66984b1e3..000000000 Binary files a/InCallUI/res/drawable-xxhdpi/fab_ic_message.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/fab_red.png b/InCallUI/res/drawable-xxhdpi/fab_red.png deleted file mode 100644 index 92eb979d5..000000000 Binary files a/InCallUI/res/drawable-xxhdpi/fab_red.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/ic_block_grey600_24dp.png b/InCallUI/res/drawable-xxhdpi/ic_block_grey600_24dp.png deleted file mode 100644 index 9f5120373..000000000 Binary files a/InCallUI/res/drawable-xxhdpi/ic_block_grey600_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/ic_business_white_24dp.png b/InCallUI/res/drawable-xxhdpi/ic_business_white_24dp.png deleted file mode 100644 index 7dfc8dc52..000000000 Binary files a/InCallUI/res/drawable-xxhdpi/ic_business_white_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/ic_call_end_white_24dp.png b/InCallUI/res/drawable-xxhdpi/ic_call_end_white_24dp.png deleted file mode 100644 index aeabe4a81..000000000 Binary files a/InCallUI/res/drawable-xxhdpi/ic_call_end_white_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/ic_call_split_white_24dp.png b/InCallUI/res/drawable-xxhdpi/ic_call_split_white_24dp.png deleted file mode 100644 index 5ea577716..000000000 Binary files a/InCallUI/res/drawable-xxhdpi/ic_call_split_white_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/ic_call_white_24dp.png b/InCallUI/res/drawable-xxhdpi/ic_call_white_24dp.png deleted file mode 100644 index b761bc466..000000000 Binary files a/InCallUI/res/drawable-xxhdpi/ic_call_white_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/ic_close_grey600_24dp.png b/InCallUI/res/drawable-xxhdpi/ic_close_grey600_24dp.png deleted file mode 100644 index 22d7aa55e..000000000 Binary files a/InCallUI/res/drawable-xxhdpi/ic_close_grey600_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/ic_forward_white_24dp.png b/InCallUI/res/drawable-xxhdpi/ic_forward_white_24dp.png deleted file mode 100644 index 7bd5b1635..000000000 Binary files a/InCallUI/res/drawable-xxhdpi/ic_forward_white_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/ic_hd_24dp.png b/InCallUI/res/drawable-xxhdpi/ic_hd_24dp.png deleted file mode 100644 index dd08bbbec..000000000 Binary files a/InCallUI/res/drawable-xxhdpi/ic_hd_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/ic_location_on_white_24dp.png b/InCallUI/res/drawable-xxhdpi/ic_location_on_white_24dp.png deleted file mode 100644 index 078b10d4f..000000000 Binary files a/InCallUI/res/drawable-xxhdpi/ic_location_on_white_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/ic_lockscreen_glowdot.png b/InCallUI/res/drawable-xxhdpi/ic_lockscreen_glowdot.png deleted file mode 100644 index c0edd91c8..000000000 Binary files a/InCallUI/res/drawable-xxhdpi/ic_lockscreen_glowdot.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/ic_person_add_grey600_24dp.png b/InCallUI/res/drawable-xxhdpi/ic_person_add_grey600_24dp.png deleted file mode 100644 index c17dfe05f..000000000 Binary files a/InCallUI/res/drawable-xxhdpi/ic_person_add_grey600_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/ic_phone_paused_white_24dp.png b/InCallUI/res/drawable-xxhdpi/ic_phone_paused_white_24dp.png deleted file mode 100644 index baf0cf27f..000000000 Binary files a/InCallUI/res/drawable-xxhdpi/ic_phone_paused_white_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/ic_question_mark.png b/InCallUI/res/drawable-xxhdpi/ic_question_mark.png deleted file mode 100644 index b9b6b00e7..000000000 Binary files a/InCallUI/res/drawable-xxhdpi/ic_question_mark.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/ic_report_white_36dp.png b/InCallUI/res/drawable-xxhdpi/ic_report_white_36dp.png deleted file mode 100644 index f7cfacbd4..000000000 Binary files a/InCallUI/res/drawable-xxhdpi/ic_report_white_36dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/ic_schedule_white_24dp.png b/InCallUI/res/drawable-xxhdpi/ic_schedule_white_24dp.png deleted file mode 100644 index bfc72736a..000000000 Binary files a/InCallUI/res/drawable-xxhdpi/ic_schedule_white_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/ic_toolbar_add_call.png b/InCallUI/res/drawable-xxhdpi/ic_toolbar_add_call.png deleted file mode 100644 index 6e343c74e..000000000 Binary files a/InCallUI/res/drawable-xxhdpi/ic_toolbar_add_call.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/ic_toolbar_arrow_whitespace.png b/InCallUI/res/drawable-xxhdpi/ic_toolbar_arrow_whitespace.png deleted file mode 100644 index 737704018..000000000 Binary files a/InCallUI/res/drawable-xxhdpi/ic_toolbar_arrow_whitespace.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/ic_toolbar_audio_bluetooth.png b/InCallUI/res/drawable-xxhdpi/ic_toolbar_audio_bluetooth.png deleted file mode 100644 index b8a385d14..000000000 Binary files a/InCallUI/res/drawable-xxhdpi/ic_toolbar_audio_bluetooth.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/ic_toolbar_audio_headphones.png b/InCallUI/res/drawable-xxhdpi/ic_toolbar_audio_headphones.png deleted file mode 100644 index 62d0ae331..000000000 Binary files a/InCallUI/res/drawable-xxhdpi/ic_toolbar_audio_headphones.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/ic_toolbar_audio_phone.png b/InCallUI/res/drawable-xxhdpi/ic_toolbar_audio_phone.png deleted file mode 100644 index 0e88501d6..000000000 Binary files a/InCallUI/res/drawable-xxhdpi/ic_toolbar_audio_phone.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/ic_toolbar_dialpad.png b/InCallUI/res/drawable-xxhdpi/ic_toolbar_dialpad.png deleted file mode 100644 index a754f6875..000000000 Binary files a/InCallUI/res/drawable-xxhdpi/ic_toolbar_dialpad.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/ic_toolbar_hold.png b/InCallUI/res/drawable-xxhdpi/ic_toolbar_hold.png deleted file mode 100644 index f3757a8b5..000000000 Binary files a/InCallUI/res/drawable-xxhdpi/ic_toolbar_hold.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/ic_toolbar_merge.png b/InCallUI/res/drawable-xxhdpi/ic_toolbar_merge.png deleted file mode 100644 index 5d046008c..000000000 Binary files a/InCallUI/res/drawable-xxhdpi/ic_toolbar_merge.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/ic_toolbar_mic_off.png b/InCallUI/res/drawable-xxhdpi/ic_toolbar_mic_off.png deleted file mode 100644 index ae41d5c35..000000000 Binary files a/InCallUI/res/drawable-xxhdpi/ic_toolbar_mic_off.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/ic_toolbar_speaker_on.png b/InCallUI/res/drawable-xxhdpi/ic_toolbar_speaker_on.png deleted file mode 100644 index d1bbb0947..000000000 Binary files a/InCallUI/res/drawable-xxhdpi/ic_toolbar_speaker_on.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/ic_toolbar_swap.png b/InCallUI/res/drawable-xxhdpi/ic_toolbar_swap.png deleted file mode 100644 index ea9127ee2..000000000 Binary files a/InCallUI/res/drawable-xxhdpi/ic_toolbar_swap.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/ic_toolbar_video.png b/InCallUI/res/drawable-xxhdpi/ic_toolbar_video.png deleted file mode 100644 index 5c52dd6c6..000000000 Binary files a/InCallUI/res/drawable-xxhdpi/ic_toolbar_video.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/ic_toolbar_video_off.png b/InCallUI/res/drawable-xxhdpi/ic_toolbar_video_off.png deleted file mode 100644 index 898b7c04d..000000000 Binary files a/InCallUI/res/drawable-xxhdpi/ic_toolbar_video_off.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/ic_toolbar_video_switch.png b/InCallUI/res/drawable-xxhdpi/ic_toolbar_video_switch.png deleted file mode 100644 index 4380a47ca..000000000 Binary files a/InCallUI/res/drawable-xxhdpi/ic_toolbar_video_switch.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/img_business.png b/InCallUI/res/drawable-xxhdpi/img_business.png deleted file mode 100644 index c17e4c9d8..000000000 Binary files a/InCallUI/res/drawable-xxhdpi/img_business.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/img_conference.png b/InCallUI/res/drawable-xxhdpi/img_conference.png deleted file mode 100644 index a8dba5ed0..000000000 Binary files a/InCallUI/res/drawable-xxhdpi/img_conference.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/img_no_image.png b/InCallUI/res/drawable-xxhdpi/img_no_image.png deleted file mode 100644 index 2cf7f23a0..000000000 Binary files a/InCallUI/res/drawable-xxhdpi/img_no_image.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxhdpi/img_phone.png b/InCallUI/res/drawable-xxhdpi/img_phone.png deleted file mode 100644 index 4eaaba509..000000000 Binary files a/InCallUI/res/drawable-xxhdpi/img_phone.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/fab_blue.png b/InCallUI/res/drawable-xxxhdpi/fab_blue.png deleted file mode 100644 index 1dd8a9260..000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/fab_blue.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/fab_ic_call.png b/InCallUI/res/drawable-xxxhdpi/fab_ic_call.png deleted file mode 100644 index 7af3396b4..000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/fab_ic_call.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/fab_ic_end_call.png b/InCallUI/res/drawable-xxxhdpi/fab_ic_end_call.png deleted file mode 100644 index aabdadec2..000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/fab_ic_end_call.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/fab_ic_message.png b/InCallUI/res/drawable-xxxhdpi/fab_ic_message.png deleted file mode 100644 index c5a108aba..000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/fab_ic_message.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/fab_red.png b/InCallUI/res/drawable-xxxhdpi/fab_red.png deleted file mode 100644 index f1b36f70b..000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/fab_red.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/ic_block_grey600_24dp.png b/InCallUI/res/drawable-xxxhdpi/ic_block_grey600_24dp.png deleted file mode 100644 index 01df2b52b..000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/ic_block_grey600_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/ic_business_white_24dp.png b/InCallUI/res/drawable-xxxhdpi/ic_business_white_24dp.png deleted file mode 100644 index c9aea72ce..000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/ic_business_white_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/ic_call_end_white_24dp.png b/InCallUI/res/drawable-xxxhdpi/ic_call_end_white_24dp.png deleted file mode 100644 index a6e8a7bc1..000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/ic_call_end_white_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/ic_call_split_white_24dp.png b/InCallUI/res/drawable-xxxhdpi/ic_call_split_white_24dp.png deleted file mode 100644 index 600cec8e6..000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/ic_call_split_white_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/ic_close_grey600_24dp.png b/InCallUI/res/drawable-xxxhdpi/ic_close_grey600_24dp.png deleted file mode 100644 index 7d1c061f7..000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/ic_close_grey600_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/ic_forward_white_24dp.png b/InCallUI/res/drawable-xxxhdpi/ic_forward_white_24dp.png deleted file mode 100644 index 428009cfe..000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/ic_forward_white_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/ic_hd_24dp.png b/InCallUI/res/drawable-xxxhdpi/ic_hd_24dp.png deleted file mode 100644 index 3f87b882e..000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/ic_hd_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/ic_location_on_white_24dp.png b/InCallUI/res/drawable-xxxhdpi/ic_location_on_white_24dp.png deleted file mode 100644 index 8bcb6f620..000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/ic_location_on_white_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/ic_person_add_grey600_24dp.png b/InCallUI/res/drawable-xxxhdpi/ic_person_add_grey600_24dp.png deleted file mode 100644 index e24919737..000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/ic_person_add_grey600_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/ic_question_mark.png b/InCallUI/res/drawable-xxxhdpi/ic_question_mark.png deleted file mode 100644 index 7ba34242c..000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/ic_question_mark.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/ic_report_white_36dp.png b/InCallUI/res/drawable-xxxhdpi/ic_report_white_36dp.png deleted file mode 100644 index 7ef0d7afc..000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/ic_report_white_36dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/ic_schedule_white_24dp.png b/InCallUI/res/drawable-xxxhdpi/ic_schedule_white_24dp.png deleted file mode 100644 index b94f4dfa1..000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/ic_schedule_white_24dp.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_add_call.png b/InCallUI/res/drawable-xxxhdpi/ic_toolbar_add_call.png deleted file mode 100644 index c97e4bb15..000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_add_call.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_arrow_whitespace.png b/InCallUI/res/drawable-xxxhdpi/ic_toolbar_arrow_whitespace.png deleted file mode 100644 index 1c11c5d0f..000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_arrow_whitespace.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_audio_bluetooth.png b/InCallUI/res/drawable-xxxhdpi/ic_toolbar_audio_bluetooth.png deleted file mode 100644 index f7fa12c8b..000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_audio_bluetooth.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_audio_headphones.png b/InCallUI/res/drawable-xxxhdpi/ic_toolbar_audio_headphones.png deleted file mode 100644 index 8199701ce..000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_audio_headphones.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_audio_phone.png b/InCallUI/res/drawable-xxxhdpi/ic_toolbar_audio_phone.png deleted file mode 100644 index ee14ea67a..000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_audio_phone.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_dialpad.png b/InCallUI/res/drawable-xxxhdpi/ic_toolbar_dialpad.png deleted file mode 100644 index e537112fb..000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_dialpad.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_hold.png b/InCallUI/res/drawable-xxxhdpi/ic_toolbar_hold.png deleted file mode 100644 index 883d0d609..000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_hold.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_merge.png b/InCallUI/res/drawable-xxxhdpi/ic_toolbar_merge.png deleted file mode 100644 index 4b6437507..000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_merge.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_mic_off.png b/InCallUI/res/drawable-xxxhdpi/ic_toolbar_mic_off.png deleted file mode 100644 index 2d8f279da..000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_mic_off.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_speaker_on.png b/InCallUI/res/drawable-xxxhdpi/ic_toolbar_speaker_on.png deleted file mode 100644 index 0560bb262..000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_speaker_on.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_swap.png b/InCallUI/res/drawable-xxxhdpi/ic_toolbar_swap.png deleted file mode 100644 index 6f03b3f66..000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_swap.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_video.png b/InCallUI/res/drawable-xxxhdpi/ic_toolbar_video.png deleted file mode 100644 index 0797fd019..000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_video.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_video_off.png b/InCallUI/res/drawable-xxxhdpi/ic_toolbar_video_off.png deleted file mode 100644 index 63f742bef..000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_video_off.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_video_switch.png b/InCallUI/res/drawable-xxxhdpi/ic_toolbar_video_switch.png deleted file mode 100644 index 77ff73cdb..000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/ic_toolbar_video_switch.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/img_business.png b/InCallUI/res/drawable-xxxhdpi/img_business.png deleted file mode 100644 index 88f14e999..000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/img_business.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/img_conference.png b/InCallUI/res/drawable-xxxhdpi/img_conference.png deleted file mode 100644 index eb42b5552..000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/img_conference.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/img_no_image.png b/InCallUI/res/drawable-xxxhdpi/img_no_image.png deleted file mode 100644 index 216574222..000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/img_no_image.png and /dev/null differ diff --git a/InCallUI/res/drawable-xxxhdpi/img_phone.png b/InCallUI/res/drawable-xxxhdpi/img_phone.png deleted file mode 100644 index 7cbfbd75e..000000000 Binary files a/InCallUI/res/drawable-xxxhdpi/img_phone.png and /dev/null differ diff --git a/InCallUI/res/drawable/btn_add.xml b/InCallUI/res/drawable/btn_add.xml deleted file mode 100644 index 7d5e90f71..000000000 --- a/InCallUI/res/drawable/btn_add.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/InCallUI/res/drawable/btn_background.xml b/InCallUI/res/drawable/btn_background.xml deleted file mode 100644 index 597885803..000000000 --- a/InCallUI/res/drawable/btn_background.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/InCallUI/res/drawable/btn_change_to_video.xml b/InCallUI/res/drawable/btn_change_to_video.xml deleted file mode 100644 index a26cee3e9..000000000 --- a/InCallUI/res/drawable/btn_change_to_video.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/InCallUI/res/drawable/btn_change_to_voice.xml b/InCallUI/res/drawable/btn_change_to_voice.xml deleted file mode 100644 index 86a7f21d5..000000000 --- a/InCallUI/res/drawable/btn_change_to_voice.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/InCallUI/res/drawable/btn_compound_audio.xml b/InCallUI/res/drawable/btn_compound_audio.xml deleted file mode 100644 index 25a64a6ab..000000000 --- a/InCallUI/res/drawable/btn_compound_audio.xml +++ /dev/null @@ -1,93 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/InCallUI/res/drawable/btn_compound_background.xml b/InCallUI/res/drawable/btn_compound_background.xml deleted file mode 100644 index 20e2a3056..000000000 --- a/InCallUI/res/drawable/btn_compound_background.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/InCallUI/res/drawable/btn_compound_dialpad.xml b/InCallUI/res/drawable/btn_compound_dialpad.xml deleted file mode 100644 index 1b78ead44..000000000 --- a/InCallUI/res/drawable/btn_compound_dialpad.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/InCallUI/res/drawable/btn_compound_hold.xml b/InCallUI/res/drawable/btn_compound_hold.xml deleted file mode 100644 index 7974efae5..000000000 --- a/InCallUI/res/drawable/btn_compound_hold.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/InCallUI/res/drawable/btn_compound_mute.xml b/InCallUI/res/drawable/btn_compound_mute.xml deleted file mode 100644 index 86708fb0c..000000000 --- a/InCallUI/res/drawable/btn_compound_mute.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/InCallUI/res/drawable/btn_compound_video_off.xml b/InCallUI/res/drawable/btn_compound_video_off.xml deleted file mode 100644 index b942cd0c3..000000000 --- a/InCallUI/res/drawable/btn_compound_video_off.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/InCallUI/res/drawable/btn_compound_video_switch.xml b/InCallUI/res/drawable/btn_compound_video_switch.xml deleted file mode 100644 index f8111866e..000000000 --- a/InCallUI/res/drawable/btn_compound_video_switch.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/InCallUI/res/drawable/btn_merge.xml b/InCallUI/res/drawable/btn_merge.xml deleted file mode 100644 index 2b4818a47..000000000 --- a/InCallUI/res/drawable/btn_merge.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/InCallUI/res/drawable/btn_overflow.xml b/InCallUI/res/drawable/btn_overflow.xml deleted file mode 100644 index 2eb26cc14..000000000 --- a/InCallUI/res/drawable/btn_overflow.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/InCallUI/res/drawable/btn_selected.xml b/InCallUI/res/drawable/btn_selected.xml deleted file mode 100644 index 1446e4163..000000000 --- a/InCallUI/res/drawable/btn_selected.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/InCallUI/res/drawable/btn_selected_focused.xml b/InCallUI/res/drawable/btn_selected_focused.xml deleted file mode 100644 index 2eda9bf8b..000000000 --- a/InCallUI/res/drawable/btn_selected_focused.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/InCallUI/res/drawable/btn_swap.xml b/InCallUI/res/drawable/btn_swap.xml deleted file mode 100644 index 5d6c8ecaf..000000000 --- a/InCallUI/res/drawable/btn_swap.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/InCallUI/res/drawable/btn_unselected.xml b/InCallUI/res/drawable/btn_unselected.xml deleted file mode 100644 index aed995cec..000000000 --- a/InCallUI/res/drawable/btn_unselected.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/InCallUI/res/drawable/btn_unselected_focused.xml b/InCallUI/res/drawable/btn_unselected_focused.xml deleted file mode 100644 index 66075d427..000000000 --- a/InCallUI/res/drawable/btn_unselected_focused.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/InCallUI/res/drawable/conference_ripple.xml b/InCallUI/res/drawable/conference_ripple.xml deleted file mode 100644 index 4e4a21304..000000000 --- a/InCallUI/res/drawable/conference_ripple.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/InCallUI/res/drawable/end_call_background.xml b/InCallUI/res/drawable/end_call_background.xml deleted file mode 100644 index c43deac4f..000000000 --- a/InCallUI/res/drawable/end_call_background.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - diff --git a/InCallUI/res/drawable/ic_incall_audio_handle.xml b/InCallUI/res/drawable/ic_incall_audio_handle.xml deleted file mode 100644 index 2e71a5b70..000000000 --- a/InCallUI/res/drawable/ic_incall_audio_handle.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/InCallUI/res/drawable/ic_incall_video_handle.xml b/InCallUI/res/drawable/ic_incall_video_handle.xml deleted file mode 100644 index a24e305c4..000000000 --- a/InCallUI/res/drawable/ic_incall_video_handle.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/InCallUI/res/drawable/ic_lockscreen_answer.xml b/InCallUI/res/drawable/ic_lockscreen_answer.xml deleted file mode 100644 index 3184111fb..000000000 --- a/InCallUI/res/drawable/ic_lockscreen_answer.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - diff --git a/InCallUI/res/drawable/ic_lockscreen_answer_activated_layer.xml b/InCallUI/res/drawable/ic_lockscreen_answer_activated_layer.xml deleted file mode 100644 index f22b87e34..000000000 --- a/InCallUI/res/drawable/ic_lockscreen_answer_activated_layer.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - diff --git a/InCallUI/res/drawable/ic_lockscreen_answer_normal_layer.xml b/InCallUI/res/drawable/ic_lockscreen_answer_normal_layer.xml deleted file mode 100644 index 31b884f99..000000000 --- a/InCallUI/res/drawable/ic_lockscreen_answer_normal_layer.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - diff --git a/InCallUI/res/drawable/ic_lockscreen_answer_video.xml b/InCallUI/res/drawable/ic_lockscreen_answer_video.xml deleted file mode 100644 index 05577979a..000000000 --- a/InCallUI/res/drawable/ic_lockscreen_answer_video.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - diff --git a/InCallUI/res/drawable/ic_lockscreen_answer_video_activated_layer.xml b/InCallUI/res/drawable/ic_lockscreen_answer_video_activated_layer.xml deleted file mode 100644 index 7895e1b6d..000000000 --- a/InCallUI/res/drawable/ic_lockscreen_answer_video_activated_layer.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - diff --git a/InCallUI/res/drawable/ic_lockscreen_answer_video_normal_layer.xml b/InCallUI/res/drawable/ic_lockscreen_answer_video_normal_layer.xml deleted file mode 100644 index 793a36e10..000000000 --- a/InCallUI/res/drawable/ic_lockscreen_answer_video_normal_layer.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - diff --git a/InCallUI/res/drawable/ic_lockscreen_decline.xml b/InCallUI/res/drawable/ic_lockscreen_decline.xml deleted file mode 100644 index 6643816d9..000000000 --- a/InCallUI/res/drawable/ic_lockscreen_decline.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - diff --git a/InCallUI/res/drawable/ic_lockscreen_decline_activated_layer.xml b/InCallUI/res/drawable/ic_lockscreen_decline_activated_layer.xml deleted file mode 100644 index 096c32b4a..000000000 --- a/InCallUI/res/drawable/ic_lockscreen_decline_activated_layer.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - diff --git a/InCallUI/res/drawable/ic_lockscreen_decline_normal_layer.xml b/InCallUI/res/drawable/ic_lockscreen_decline_normal_layer.xml deleted file mode 100644 index 4da5f8d66..000000000 --- a/InCallUI/res/drawable/ic_lockscreen_decline_normal_layer.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - - - diff --git a/InCallUI/res/drawable/ic_lockscreen_decline_video.xml b/InCallUI/res/drawable/ic_lockscreen_decline_video.xml deleted file mode 100644 index cedd49757..000000000 --- a/InCallUI/res/drawable/ic_lockscreen_decline_video.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - diff --git a/InCallUI/res/drawable/ic_lockscreen_decline_video_activated_layer.xml b/InCallUI/res/drawable/ic_lockscreen_decline_video_activated_layer.xml deleted file mode 100644 index 0790aed19..000000000 --- a/InCallUI/res/drawable/ic_lockscreen_decline_video_activated_layer.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - diff --git a/InCallUI/res/drawable/ic_lockscreen_decline_video_normal_layer.xml b/InCallUI/res/drawable/ic_lockscreen_decline_video_normal_layer.xml deleted file mode 100644 index e3b89b947..000000000 --- a/InCallUI/res/drawable/ic_lockscreen_decline_video_normal_layer.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - diff --git a/InCallUI/res/drawable/ic_lockscreen_outerring.xml b/InCallUI/res/drawable/ic_lockscreen_outerring.xml deleted file mode 100644 index 489515fbc..000000000 --- a/InCallUI/res/drawable/ic_lockscreen_outerring.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - diff --git a/InCallUI/res/drawable/ic_lockscreen_text.xml b/InCallUI/res/drawable/ic_lockscreen_text.xml deleted file mode 100644 index f9caac818..000000000 --- a/InCallUI/res/drawable/ic_lockscreen_text.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - diff --git a/InCallUI/res/drawable/ic_lockscreen_text_activated_layer.xml b/InCallUI/res/drawable/ic_lockscreen_text_activated_layer.xml deleted file mode 100644 index a74e36b31..000000000 --- a/InCallUI/res/drawable/ic_lockscreen_text_activated_layer.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - - diff --git a/InCallUI/res/drawable/ic_lockscreen_text_normal_layer.xml b/InCallUI/res/drawable/ic_lockscreen_text_normal_layer.xml deleted file mode 100644 index be32d0baa..000000000 --- a/InCallUI/res/drawable/ic_lockscreen_text_normal_layer.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - diff --git a/InCallUI/res/drawable/img_conference_automirrored.xml b/InCallUI/res/drawable/img_conference_automirrored.xml deleted file mode 100644 index fa1fd4920..000000000 --- a/InCallUI/res/drawable/img_conference_automirrored.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - \ No newline at end of file diff --git a/InCallUI/res/drawable/img_no_image_automirrored.xml b/InCallUI/res/drawable/img_no_image_automirrored.xml deleted file mode 100644 index f0cf0db31..000000000 --- a/InCallUI/res/drawable/img_no_image_automirrored.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - \ No newline at end of file diff --git a/InCallUI/res/drawable/incoming_sms_background.xml b/InCallUI/res/drawable/incoming_sms_background.xml deleted file mode 100644 index 81ff21c61..000000000 --- a/InCallUI/res/drawable/incoming_sms_background.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - diff --git a/InCallUI/res/drawable/outgoing_sms_background.xml b/InCallUI/res/drawable/outgoing_sms_background.xml deleted file mode 100644 index e4f868fea..000000000 --- a/InCallUI/res/drawable/outgoing_sms_background.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - diff --git a/InCallUI/res/drawable/spam_notification_icon.xml b/InCallUI/res/drawable/spam_notification_icon.xml deleted file mode 100644 index c8bafe085..000000000 --- a/InCallUI/res/drawable/spam_notification_icon.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/InCallUI/res/drawable/subject_bubble.xml b/InCallUI/res/drawable/subject_bubble.xml deleted file mode 100644 index adab67833..000000000 --- a/InCallUI/res/drawable/subject_bubble.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/InCallUI/res/drawable/unknown_notification_icon.xml b/InCallUI/res/drawable/unknown_notification_icon.xml deleted file mode 100644 index 85c50752c..000000000 --- a/InCallUI/res/drawable/unknown_notification_icon.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/InCallUI/res/layout-h400dp/call_card_fragment.xml b/InCallUI/res/layout-h400dp/call_card_fragment.xml deleted file mode 100644 index 2ef6e52da..000000000 --- a/InCallUI/res/layout-h400dp/call_card_fragment.xml +++ /dev/null @@ -1,172 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/InCallUI/res/layout-h600dp/manage_conference_call_button.xml b/InCallUI/res/layout-h600dp/manage_conference_call_button.xml deleted file mode 100644 index 9a83313ac..000000000 --- a/InCallUI/res/layout-h600dp/manage_conference_call_button.xml +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/InCallUI/res/layout-w500dp-land/call_card_fragment.xml b/InCallUI/res/layout-w500dp-land/call_card_fragment.xml deleted file mode 100644 index c71cf07a6..000000000 --- a/InCallUI/res/layout-w500dp-land/call_card_fragment.xml +++ /dev/null @@ -1,158 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/InCallUI/res/layout-w600dp-land/manage_conference_call_button.xml b/InCallUI/res/layout-w600dp-land/manage_conference_call_button.xml deleted file mode 100644 index 9a83313ac..000000000 --- a/InCallUI/res/layout-w600dp-land/manage_conference_call_button.xml +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/InCallUI/res/layout/accessible_answer_fragment.xml b/InCallUI/res/layout/accessible_answer_fragment.xml deleted file mode 100644 index 90fe57788..000000000 --- a/InCallUI/res/layout/accessible_answer_fragment.xml +++ /dev/null @@ -1,104 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/InCallUI/res/layout/answer_fragment.xml b/InCallUI/res/layout/answer_fragment.xml deleted file mode 100644 index ec6ef30ac..000000000 --- a/InCallUI/res/layout/answer_fragment.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - diff --git a/InCallUI/res/layout/business_contact_context_list_header.xml b/InCallUI/res/layout/business_contact_context_list_header.xml deleted file mode 100644 index 90521188e..000000000 --- a/InCallUI/res/layout/business_contact_context_list_header.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/InCallUI/res/layout/business_context_info_list_item.xml b/InCallUI/res/layout/business_context_info_list_item.xml deleted file mode 100644 index 616d219d9..000000000 --- a/InCallUI/res/layout/business_context_info_list_item.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/InCallUI/res/layout/call_button_fragment.xml b/InCallUI/res/layout/call_button_fragment.xml deleted file mode 100644 index 802e3de62..000000000 --- a/InCallUI/res/layout/call_button_fragment.xml +++ /dev/null @@ -1,171 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/InCallUI/res/layout/call_card_fragment.xml b/InCallUI/res/layout/call_card_fragment.xml deleted file mode 100644 index fabde378a..000000000 --- a/InCallUI/res/layout/call_card_fragment.xml +++ /dev/null @@ -1,158 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/InCallUI/res/layout/caller_in_conference.xml b/InCallUI/res/layout/caller_in_conference.xml deleted file mode 100644 index ac78096f6..000000000 --- a/InCallUI/res/layout/caller_in_conference.xml +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/InCallUI/res/layout/conference_manager_fragment.xml b/InCallUI/res/layout/conference_manager_fragment.xml deleted file mode 100644 index 7350bee14..000000000 --- a/InCallUI/res/layout/conference_manager_fragment.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - diff --git a/InCallUI/res/layout/incall_dialpad_fragment.xml b/InCallUI/res/layout/incall_dialpad_fragment.xml deleted file mode 100644 index b567dbbf2..000000000 --- a/InCallUI/res/layout/incall_dialpad_fragment.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - diff --git a/InCallUI/res/layout/incall_screen.xml b/InCallUI/res/layout/incall_screen.xml deleted file mode 100644 index 3922ea073..000000000 --- a/InCallUI/res/layout/incall_screen.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - diff --git a/InCallUI/res/layout/manage_conference_call_button.xml b/InCallUI/res/layout/manage_conference_call_button.xml deleted file mode 100644 index 01ca1bdc3..000000000 --- a/InCallUI/res/layout/manage_conference_call_button.xml +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/InCallUI/res/layout/outgoing_call_animation.xml b/InCallUI/res/layout/outgoing_call_animation.xml deleted file mode 100644 index 69ba3d3c6..000000000 --- a/InCallUI/res/layout/outgoing_call_animation.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - \ No newline at end of file diff --git a/InCallUI/res/layout/person_context_info_list_item.xml b/InCallUI/res/layout/person_context_info_list_item.xml deleted file mode 100644 index 4f973d564..000000000 --- a/InCallUI/res/layout/person_context_info_list_item.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/InCallUI/res/layout/primary_call_info.xml b/InCallUI/res/layout/primary_call_info.xml deleted file mode 100644 index cae915224..000000000 --- a/InCallUI/res/layout/primary_call_info.xml +++ /dev/null @@ -1,231 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/InCallUI/res/layout/secondary_call_info.xml b/InCallUI/res/layout/secondary_call_info.xml deleted file mode 100644 index e866795a6..000000000 --- a/InCallUI/res/layout/secondary_call_info.xml +++ /dev/null @@ -1,105 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/InCallUI/res/layout/video_call_fragment.xml b/InCallUI/res/layout/video_call_fragment.xml deleted file mode 100644 index d5e11ef4a..000000000 --- a/InCallUI/res/layout/video_call_fragment.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/InCallUI/res/layout/video_call_views.xml b/InCallUI/res/layout/video_call_views.xml deleted file mode 100644 index d514f6df1..000000000 --- a/InCallUI/res/layout/video_call_views.xml +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/InCallUI/res/menu/incall_audio_mode_menu.xml b/InCallUI/res/menu/incall_audio_mode_menu.xml deleted file mode 100644 index 070c1813a..000000000 --- a/InCallUI/res/menu/incall_audio_mode_menu.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/InCallUI/res/values-af/strings.xml b/InCallUI/res/values-af/strings.xml deleted file mode 100644 index 181ffce66..000000000 --- a/InCallUI/res/values-af/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Foon" - "Hou aan" - "Onbekend" - "Privaat nommer" - "Telefoonhokkie" - "Konferensie-oproep" - "Oproep is ontkoppel" - "Luidspreker" - "Selfoonoorfoon" - "Bedraade kopfoon" - "Bluetooth" - "Stuur die volgende luitone?\n" - "Stuur luitone\n" - "Stuur" - "Ja" - "Nee" - "Vervang die plekhouerkarakter met" - "Konferensie-oproep %s" - "Stemboodskapnommer" - "Bel" - "Bel tans weer" - "Konferensie-oproep" - "Inkomende oproep" - "Inkomende werkoproep" - "Oproep beëindig" - "Hou aan" - "Lui af" - "In oproep" - "My nommer is %s" - "Koppel tans video" - "Video-oproep" - "Versoek tans video" - "Kan nie video-oproep koppel nie" - "Videoversoek is verwerp" - "Jou terugbelnommer\n %1$s" - "Jou noodterugbelnommer\n %1$s" - "Bel" - "Gemiste oproep" - "Gemiste oproepe" - "%s gemiste oproepe" - "Gemiste oproep vanaf %s" - "Voortdurende oproep" - "Voortdurende werkoproep" - "Voortdurende Wi-Fi-oproep" - "Voortdurende Wi-Fi-werkoproep" - "Hou aan" - "Inkomende oproep" - "Inkomende werkoproep" - "Inkomende Wi-Fi-oproep" - "Inkomende Wi-Fi-werkoproep" - "Inkomende video-oproep" - "Inkomende verdagte strooipos-oproep" - "Inkomende videoversoek" - "Nuwe stemboodskap" - "Nuwe stemboodskap (%d)" - "Bel %s" - "Stemboodskapnommer is onbekend" - "Geen diens nie" - "Gekose netwerk (%s) is nie beskikbaar nie" - "Antwoord" - "Lui af" - "Video" - "Stem" - "Aanvaar" - "Maak toe" - "Bel terug" - "Boodskap" - "Voortgesette oproep op \'n ander toestel" - "Sit oproep deur" - "Skakel vliegtuigmodus eers af om \'n oproep te maak." - "Nie geregistreer op netwerk nie." - "Sellulêre netwerk is nie beskikbaar nie." - "Voer \'n geldige nommer in om \'n oproep te maak." - "Kan nie bel nie." - "Begin tans MMI-volgorde …" - "Diens word nie gesteun nie." - "Kan nie oproepe wissel nie." - "Kan nie oproep skei nie." - "Kan nie deurskakel nie." - "Kan nie konferensie-oproep maak nie." - "Kan nie oproep weier nie." - "Kan nie oproep(e) vrystel nie." - "SIP-oproep" - "Noodoproep" - "Skakel tans radio aan …" - "Geen sein nie. Probeer tans weer …" - "Kan nie bel nie. %s is nie \'n noodnommer nie." - "Kan nie bel nie. Bel \'n noodnommer." - "Gebruik sleutelbord om te bel" - "Hou oproep" - "Hervat oproep" - "Beëindig oproep" - "Wys belblad" - "Versteek belblad" - "Demp" - "Ontdemp" - "Voeg oproep by" - "Smelt oproepe saam" - "Ruil" - "Bestuur oproepe" - "Bestuur konferensie-oproep" - "Konferensie-oproep" - "Bestuur" - "Oudio" - "Video-oproep" - "Verander na stemoproep" - "Wissel kamera" - "Skakel kamera aan" - "Skakel kamera af" - "Nog opsies" - "Speler het begin" - "Speler het gestop" - "Kamera is nie gereed nie" - "Kamera is gereed" - "Onbekende oproepsessiegebeurtenis" - "Diens" - "Opstelling" - "<Nie gestel nie>" - "Ander oproepinstellings" - "Bel via %s" - "Inkomend via %s" - "kontakfoto" - "gaan privaat" - "kies kontak" - "Skryf jou eie …" - "Kanselleer" - "Stuur" - "Antwoord" - "Stuur SMS" - "Weier" - "Antwoord as video-oproep" - "Antwoord as oudio-oproep" - "Aanvaar videoversoek" - "Weier videoversoek" - "Aanvaar videoversendversoek" - "Weier videoversendversoek" - "Aanvaar video-ontvangversoek" - "Weier video-ontvangversoek" - "Gly op vir %s." - "Gly links vir %s." - "Gly regs vir %s." - "Gly af vir %s." - "Vibreer" - "Vibreer" - "Klank" - "Verstekklank (%1$s)" - "Foonluitoon" - "Vibreer wanneer dit lui" - "Luitoon en vibreer" - "Bestuur konferensie-oproep" - "Noodnommer" - "Profielfoto" - "Kamera is af" - "via %s" - "Nota is gestuur" - "Onlangse boodskappe" - "Besigheidinligting" - "%.1f myl ver" - "%.1f km ver" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "Maak môre om %s oop" - "Maak vandag om %s oop" - "Maak om %s toe" - "Het vandag om %s toegemaak" - "Nou oop" - "Nou gesluit" - "Verdagte strooiposbeller" - "Oproep het geëindig %1$s" - "Dit is die eerste keer wat hierdie nommer jou bel." - "Ons vermoed dat hierdie oproep strooipos was." - "Blokkeer/gee strooipos aan" - "Voeg kontak by" - "Nie strooipos nie" - diff --git a/InCallUI/res/values-am/strings.xml b/InCallUI/res/values-am/strings.xml deleted file mode 100644 index 99516cfec..000000000 --- a/InCallUI/res/values-am/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "ስልክ" - "ያዝና ቆይ" - "ያልታወቀ" - "የግል ቁጥር" - "የሕዝብ ስልክ" - "የስብሰባ ጥሪ" - "ጥሪው ተቋርጧል" - "ድምፅ ማጉያ" - "የስልክ ጆሮ ማዳመጫ" - "ባለ ገመድ የጆሮ ማዳመጫ" - "ብሉቱዝ" - "የሚከተሉትን የጥሪ ድምፆች ላክ?\n" - "የጥሪ ድምፆች በመላክ ላይ \n" - "ላክ" - "አዎ" - "አይ" - "የልቅ ምልክት ተካ በ" - "የስብሰባ ጥሪ %s" - "የድምፅ መልእክት ቁጥር" - "በመደወል ላይ" - "ዳግም በመደወል ላይ" - "የስብሰባ ጥሪ" - "ገቢ ጥሪ" - "ገቢ የሥራ ጥሪ" - "ጥሪ አብቅቷል" - "ያዝና ቆይ" - "በመዝጋት ላይ" - "ጥሪ ላይ" - "ቁጥሬ %s ነው" - "ቪድዮ በማገናኘት ላይ" - "የቪዲዮ ጥሪ" - "ቪድዮ በመጠየቅ ላይ" - "የቪዲዮ ጥሪን ማገናኘት አይቻልም" - "የቪዲዮ ጥያቄ ውድቅ ተደርጓል" - "የእርስዎ የመልሶ መደወያ ቁጥር\n%1$s" - "የእርስዎ የድንገተኛ አደጋ መልሶ መደወያ ቁጥር\n%1$s" - "በመደወል ላይ" - "ያመለጠ ጥሪ" - "ያመለጡ ጥሪዎች" - "%s ያመለጡ ጥሪዎች" - "ያልተመለሰ ጥሪ ከ%s" - "በሂደት ላይ ያለ ጥሪ" - "በሂደት ላይ ያለ የሥራ ጥሪ" - "በሂደት ላይ ያለ የWi-Fi ጥሪ" - "በሂደት ላይ ያለ የWi-Fi የሥራ ጥሪ" - "ያዝና ቆይ" - "ገቢ ጥሪ" - "ገቢ የሥራ ጥሪ" - "ገቢ የWi-Fi ጥሪ" - "ገቢ የWi-Fi የሥራ ጥሪ" - "ገቢ የቪዲዮ ጥሪ" - "መጪ የተጠረጠረ የአይፈለጌ መልዕክት ጥሪ" - "ገቢ የቪዲዮ ጥያቄ" - "አዲስ የድምፅ መልእክት" - "አዲስ የድምፅ መልእክት (%d)" - "ደውል %s" - "የማይታወቅ የድምፅ መልእክት ቁጥር" - "ምንም አገልግሎት የለም" - "የተመረጠ አውታረመረብ (%s) አይገኝም" - "መልስ" - "ዝጋ" - "ቪዲዮ" - "ድምፅ" - "ተቀበል" - "አስወግድ" - "መልሰህ ደውል" - "መልእክት" - "በሌላ መሳሪያ ጥሪ በመካሄድ ላይ ነው" - "ጥሪ አስተላልፍ" - "ለመደወል፣ መጀመሪያ የአውሮፕላኑን ሁኔታ ያጥፉ።" - "በአውታረ መረቡ ላይ አልተመዘገበም።" - "የተንቀሳቃሽ ስልክ አውታረ መረብ አይገኝም።" - "አንድ ጥሪ ለማድረግ የሚሠራ ቁጥር ያስገቡ።" - "መደወል አይቻልም።" - "የMMI ቅደመ ተከተል በማስጀመር ላይ…" - "አገልግሎት አይደገፍም።" - "ጥሪዎችን መቀያየር አይቻልም።" - "ጥሪን መለየት አይቻልም።" - "ማስተላለፍ አይቻልም።" - "የጉባዔ ጥሪ ማድረግ አይቻልም።" - "ጥሪውን መዝጋት አይቻልም።" - "ጥሪ(ዎች)ን መልቀቅ አይቻልም።" - "የSIP ጥሪ" - "የአስቸኳይ ጊዜ ጥሪ" - "ሬዲዮ በማብራት ላይ…" - "ምንም አገልግሎት የለም። ዳግም በመሞከር ላይ…" - "መደወል አይቻልም። %s የአስቸኳይ አደጋ ቁጥር አይደለም።" - "መደወል አይቻልም። ወደ የአስቸኳይ አደጋ ቁጥር ይደውሉ።" - "ለመደወል የሰሌዳ ቁልፍ ተጠቀም" - "ጥሪ አቆይ" - "ጥሪ ቀጥል" - "ጥሪ ጨርስ" - "የመደወያ ሰሌዳ አሳይ" - "የመደወያ ሰሌዳ ደብቅ" - "ድምፅ-ከል አድርግ" - "ድምፅ አታጥፋ" - "ጥሪ ያክሉ" - "ጥሪዎችን አዋህድ" - "በውዝ" - "ጥሪዎችን አደራጅ" - "የስብሰባ ስልክ ጥሪ አደራጅ" - "የስብሰባ ጥሪ" - "ያስተዳድሩ" - "ኦዲዮ" - "የቪዲዮ ጥሪ" - "ወደ ድምፅ ጥሪ ይለውጡ" - "ካሜራ ቀይር" - "ካሜራ ያብሩ" - "ካሜራ ያጥፉ" - "ተጨማሪ አማራጮች" - "አጫዋች ጀምሯል" - "አጫዋች ቆሟል" - "ካሜራ ዝግጁ አይደለም" - "ካሜራ ዝግጁ ነው" - "ያልታወቀ የጥሪ ክፍለጊዜ ክስተት" - "አገልግሎት" - "አዋቅር" - "<አልተዘጋጀም>" - "ሌሎች የጥሪ ቅንብሮች" - "በ%s በኩል በመደወል ላይ" - "በ%s በኩል የመጣ" - "የዕውቂያ ፎቶ" - "ወደ ግላዊነት ሂድ" - "ዕውቂያ ይምረጡ" - "የእራስዎን ይጻፉ..." - "ተወው" - "ላክ" - "መልስ" - "ኤስኤምኤስ ላክ" - "ውድቅ አድርግ" - "እንደ ቪድዮ ጥሪ ይመልሱ" - "እንደ ድምፅ ጥሪ ይመልሱ" - "የቪዲዮ ጥያቄ ተቀበል" - "የቪዲዮ ጥያቄ ውድቅ አድርግ" - "የቪዲዮ አስተላልፍ ጥያቄን ተቀበል" - "የቪዲዮ አስተላልፍ ጥያቄን ውድቅ አድርግ" - "የቪዲዮ ተቀበል ጥያቄን ተቀበል" - "የቪዲዮ ተቀበል ጥያቄን ውድቅ አድርግ" - "ለ%s ወደ ላይ ያንሸራትቱ።" - "ለ%s ወደ ግራ ያንሸራትቱ።" - "ለ%s ወደ ቀኝ ያንሸራትቱ።" - "ለ%s ወደ ታች ያንሸራትቱ።" - "ንዘር" - "ንዘር" - "ድምፅ" - "ነባሪ ድምፅ (%1$s)" - "የስልክ ጥሪ ቅላጼ" - "በሚደወልበት ጊዜ ንዘር" - "የጥሪ ቅላጼ እና ንዘረት" - "የስብሰባ ስልክ ጥሪ አደራጅ" - "የአደጋ ጊዜ ቁጥር" - "የመገለጫ ፎቶ" - "ካሜራ ጠፍቷል" - "በ%s በኩል" - "ማስታወሻ ተልኳል" - "የቅርብ ጊዜ መልእክቶች" - "የንግድ መረጃ" - "%.1f ማይል ርቀት ላይ" - "%.1f ኪሜ ርቀት ላይ" - "%1$s%2$s" - "%1$s - %2$s" - "%1$s%2$s" - "ነገ %s ላይ ይከፈታል" - "ዛሬ %s ላይ ይከፈታል" - "%s ላይ ይዘጋል" - "ዛሬ %s ላይ ተዘግቷል" - "አሁን ክፍት ነው" - "አሁን ዝግ ነው" - "የተጠረጠረ አይፈለጌ ጠሪ" - "ጥሪው አብቅቷል %1$s" - "ይህ ቁጥር ለእርስዎ ሲደውል ይህ የመጀመሪያው ነው።" - "ይህ ቁጥር አይፈለጌ ላኪ ነው ብለን እንገምታለን።" - "አይፈለጌ አግድ/ሪፖርት አድርግ" - "እውቂያ ያክሉ" - "አይፈለጌ አይደለም" - diff --git a/InCallUI/res/values-ar/strings.xml b/InCallUI/res/values-ar/strings.xml deleted file mode 100644 index e89f026b6..000000000 --- a/InCallUI/res/values-ar/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "الهاتف" - "معلقة" - "غير معروف" - "رقم خاص" - "هاتف بالعملة" - "مكالمة جماعية" - "تم قطع المكالمة" - "السماعة" - "سماعة الأذن للهاتف" - "سماعة رأس سلكية" - "بلوتوث" - "هل تريد إرسال النغمات التالية؟\n" - "إرسال النغمات\n" - "إرسال" - "نعم" - "لا" - "استبدال حرف البدل بـ" - "مكالمة جماعية %s" - "رقم البريد الصوتي" - "جارٍ الطلب" - "جارٍ إعادة الطلب" - "مكالمة جماعية" - "مكالمة واردة" - "مكالمة عمل واردة" - "تم إنهاء الاتصال" - "معلقة" - "جارٍ وقف المكالمة" - "بصدد مكالمة" - "رقمي %s" - "جارٍ الاتصال بالفيديو" - "مكالمة فيديو" - "جارٍ طلب الفيديو" - "يتعذر الاتصال بمكالمة فيديو" - "تم رفض طلب الفيديو" - "رقم معاودة الاتصال\n %1$s" - "رقم معاودة اتصال الطوارئ\n %1$s" - "جارٍ الطلب" - "مكالمة فائتة" - "المكالمات الفائتة" - "%s من المكالمات الفائتة" - "مكالمة فائتة من %s" - "مكالمة حالية" - "مكالمة عمل جارية" - "‏مكالمة جارية عبر Wi-Fi" - "‏مكالمة عمل جارية عبر Wi-Fi" - "معلقة" - "مكالمة واردة" - "مكالمة عمل واردة" - "‏مكالمة واردة عبر Wi-Fi" - "‏مكالمة عمل واردة عبر اتصال Wi-Fi" - "مكالمة فيديو واردة" - "مكالمة واردة يشتبه في كونها غير مرغوب فيها" - "طلب فيديو وارد" - "بريد صوتي جديد" - "بريد صوتي جديد (%d)" - "طلب %s" - "رقم البريد الصوتي غير معروف" - "لا تتوفر خدمة" - "الشبكة المحددة (%s) غير متاحة" - "رد" - "قطع الاتصال" - "فيديو" - "صوت" - "قبول" - "تجاهل" - "معاودة الاتصال" - "رسالة" - "مكالمة جارية على جهاز آخر" - "تحويل الاتصال" - "لإجراء مكالمة، أوقف تشغيل وضع الطائرة أولاً." - "غير مسجل على الشبكة." - "شبكة الجوّال غير متاحة." - "لإجراء مكالمة، أدخل رقمًا صالحًا." - "يتعذر الاتصال." - "‏جارٍ بدء تسلسل MMI…" - "الخدمة ليست متوفرة." - "يتعذر تبديل المكالمات." - "يتعذر فصل المكالمة." - "يتعذر النقل." - "يتعذر إجراء مكالمة جماعية." - "يتعذر رفض المكالمة." - "يتعذر تحرير المكالمات." - "‏مكالمة SIP" - "مكالمة طوارئ" - "جارٍ تشغيل اللاسلكي..." - "لا تتوفر خدمة. جارٍ إعادة المحاولة…" - "يتعذر الاتصال. لا يعد %s رقم طوارئ." - "يتعذر الاتصال. يمكنك طلب رقم طوارئ" - "استخدام لوحة المفاتيح للطلب" - "تعليق المكالمة" - "استئناف المكالمة" - "إنهاء المكالمة" - "عرض لوحة الاتصال" - "إخفاء لوحة الاتصال" - "تجاهل" - "إلغاء التجاهل" - "إضافة مكالمة" - "دمج المكالمات" - "تبديل" - "إدارة المكالمات" - "إدارة مكالمة جماعية" - "مكالمة جماعية" - "إدارة" - "صوت" - "مكالمة فيديو" - "التغيير إلى مكالمة صوتية" - "تبديل الكاميرا" - "تشغيل الكاميرا" - "إيقاف الكاميرا" - "خيارات أخرى" - "تم بدء المشغّل" - "تم إيقاف المشغّل" - "الكاميرا غير جاهزة" - "الكاميرا جاهزة" - "حدث جلسة اتصال غير معروف" - "الخدمة" - "الإعداد" - "‏<لم يتم التعيين>" - "إعدادات الاتصال الأخرى" - "الاتصال عبر %s" - "واردة عبر %s" - "صورة جهة الاتصال" - "انتقال إلى مكالمة خاصة" - "تحديد جهة اتصال" - "اكتب ردك…" - "إلغاء" - "إرسال" - "الرد" - "‏إرسال رسالة قصيرة SMS" - "الرفض" - "الرد بمكالمة فيديو" - "الرد بمكالمة صوتية" - "قبول طلب الفيديو" - "رفض طلب الفيديو" - "قبول طلب بث الفيديو" - "رفض طلب بث الفيديو" - "قبول طلب استلام مكالمة الفيديو" - "رفض طلب استلام مكالمة الفيديو" - "تمرير لأعلى لـ %s." - "تمرير لليسار لـ %s." - "تمرير لليمين لـ %s." - "تمرير لأسفل لـ %s." - "اهتزاز" - "اهتزاز" - "الصوت" - "الصوت الافتراضي (%1$s)" - "نغمة رنين الهاتف" - "اهتزاز عند الرنين" - "نغمة الرنين والاهتزاز" - "إدارة مكالمة جماعية" - "رقم الطوارئ" - "صورة الملف الشخصي" - "تم إيقاف الكاميرا" - "عبر %s" - "تم إرسال الملاحظة" - "الرسائل الأخيرة" - "معلومات النشاط التجاري" - "على بُعد %.1f ميل" - "على بُعد %.1f كم" - "%1$s، %2$s" - "%1$s - %2$s" - "%1$s، %2$s" - "مفتوح غدًا في %s" - "مفتوح اليوم في %s" - "مغلق في %s" - "مغلق اليوم في %s" - "مفتوح الآن" - "مغلق الآن" - "اشتباه في متصل غير مرغوب فيه" - "‏المكالمة انتهت %1$s" - "هذه هي المرة الأولى التي تتلقى فيها اتصالاً من هذا الرقم." - "لدينا شك أن هذه المكالمة واردة من متصل غير مرغوب فيه." - "حظر/إبلاغ عن رقم غير مرغوب فيه" - "إضافة جهة اتصال" - "ليس رقمًا غير مرغوب فيه" - diff --git a/InCallUI/res/values-az/strings.xml b/InCallUI/res/values-az/strings.xml deleted file mode 100644 index 4bb9652b9..000000000 --- a/InCallUI/res/values-az/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefon" - "Gözləmə mövqeyində" - "Naməlum" - "Şəxsi nömrə" - "Taksofon" - "Konfrans zəngi" - "Zəng bitdi" - "Dinamik" - "Dəstək qulaqlığı" - "Simli qulaqlıq" - "Bluetooth" - "Aşağıdakı tonlar göndərilsin?\n" - "Tonlar göndərilir\n" - "Göndər" - "Bəli" - "Xeyr" - "Joker simvolları əvəz edin" - "Konfrans zəngi %s" - "Səsli poçt nömrəsi" - "Nömrə yığılır" - "Yenidən yığır" - "Konfrans zəngi" - "Gələn zəng" - "Daxil olan iş çağrısı" - "Zəng sona çatdı" - "Gözləmə mövqeyində" - "Dəstək asılır" - "Çağrıda" - "Mənim nömrəm %s" - "Video qoşulur" - "Video zəng" - "Video sorğusu göndərilir" - "Video zəngə qoşulmaq mümkün deyil" - "Video sorğusu rədd edildi" - "Cavab zəngi nömrəniz\n %1$s" - "Təcili cavab zəngi nömrəniz\n %1$s" - "Nömrə yığılır" - "Buraxılmış zəng" - "Buraxılmış zənglər" - "%s buraxılmış zənglər" - "%s tərəfindən zəng buraxılıb" - "Davam edən çağrı" - "Davam edən iş çağrısı" - "Davam edən Wi-Fi zəngi" - "Davam edən Wi-Fi iş çağrısı" - "Gözləmə mövqeyində" - "Gələn zəng" - "Daxil olan iş çağrısı" - "Gələn Wi-Fi zəngi" - "Daxil olan Wi-Fi iş çağrısı" - "Gələn video zəng" - "Şübhəli spam zəngi" - "Gələn video çağrı" - "Yeni səsli poçt" - "Yeni səsli poçt (%d)" - "Yığın %s" - "Səsli poçt nömrəsi naməlumdur" - "Xidmət yoxdur" - "Seçilmiş (%s) şəbəkə əlçatmazdır" - "Cavab" - "Dəstəyi qoyun" - "Videolar" - "Səs" - "Qəbul edin" - "Rədd edin" - "Geriyə zəng" - "Mesaj" - "Digər cihazda davam etməkdə olan zəng" - "Zəngi Transfer edin" - "Zəng etmək üçün ilk olaraq Uçuş Rejimini söndürün." - "Şəbəkədə qeydə alınmayıb." - "Mobil şəbəkə əlçatan deyil" - "Zəngi yerləşdirmək üçün düzgün nömrə daxil edin." - "Zəng etmək mümkün deyil." - "MMI başlanma ardıcıllığı…" - "Xidmət dəstəklənmir." - "Zəngləri keçirmək mümkün deyil." - "Zəngi ayırmaq mümkün deyil." - "Ötürmək mümkün deyil." - "Konfrans keçirmək mümkün deyil." - "Zəngi rədd etmək mümkün deyil." - "Zəngləri buraxmaq mümkün deyil." - "SIP çağrısıs" - "Təcili zəng" - "Radio yandırılır ..." - "Xidmət yoxdur. Yenidən cəhd edilir…" - "Zəng etmək mümkün deyil. %s fövqəladə nömrə deyil." - "Zəng etmək mümkün deyil. Fövqəladə nömrəni yı" - "Nömrə yığmaq üçün klaviaturadan istifadə ediin" - "Zəngi gözlədin" - "Zəngə davam edin" - "Zəngi bitirin" - "Yığım düymələrini göstərin" - "Yığım düymələrini gizlədin" - "Susdurun" - "Susdurmayın" - "Zəng əlavə edin" - "Zəngləri birləşdirin" - "Dəyişdirin" - "Zəngləri idarə edin" - "Konfrans çağrısını idarə edin" - "Konfrans zəngi" - "İdarə edin" - "Audio" - "Video zəng" - "Səsli çağrıya dəyişin" - "Kameraya keçin" - "Kameranı yandırın" - "Kameranı söndürün" - "Daha çox seçim" - "Pleyer Başladıldı" - "Pleyer Dayandırıldı" - "Kamera hazır deyil" - "Kamera hazırdır" - "Naməlum zəng sessiyası" - "Xidmət" - "Quraşdırma" - "<Təyin edilməyib>" - "Digər zəng parametrləri" - "%s vasitəsi ilə zəng edilir" - "%s vasitəsilə gələn" - "kontakt fotosu" - "şəxsi rejimə keçin" - "kontakt seçin" - "Özünüzünkünü yazın" - "Ləğv edin" - "Göndər" - "Cavab" - "SMS göndərin" - "İmtina edin" - "Video çağrı olaraq cavab verin" - "Audio çağrı olaraq cavab verin" - "Video sorğusunu qəbul edin" - "Video sorğusunu ləğv edin" - "Video ötürmə sorğusunu qəbul edin" - "Video ötürmə sorğusunu ləğv edin" - "Video qəbuletmə sorğusunu qəbul edin" - "Video qəbuletmə sorğusunu ləğv edin" - "%s üçün yuxarı sürüşdürün." - "%s üçün sola sürüşdürün." - "%s üçün sağa sürüşdürün." - "%s üçün aşağı sürüşdürün." - "Vibrasiya" - "Vibrasiya" - "Səs" - "Defolt səs (%1$s)" - "Telefon zəng səsi" - "Zəng çalanda vibrasiya olsun" - "Zəng səsi & Vibrasiya" - "Konfrans çağrısını idarə edin" - "Təcili nömrə" - "Profil fotosu" - "Kamera deaktivdir" - "%s vasitəsilə" - "Qeyd göndərildi" - "Son mesajlar" - "Biznes məlumatı" - "%.1f mil uzaqlıqda" - "%.1f km uzaqlıqda" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "Sabah saat %s açılır" - "Bu gün saat %s açılır" - "Saat %s bağlanır" - "Bu gün saat %s bağlanıb" - "İndi açın" - "İndi bağlandı" - "Şübhəli spam çağrıcısı" - "Zəng bitdi %1$s" - "Bu nömrə ilk dəfədir Sizə zəng edir." - "Bu zəngin spam olduğundan şübhələnirik." - "Spamı blok edin/bildirin" - "Kontakt əlavə edin" - "Spam deyil" - diff --git a/InCallUI/res/values-b+sr+Latn/strings.xml b/InCallUI/res/values-b+sr+Latn/strings.xml deleted file mode 100644 index 9770f90a9..000000000 --- a/InCallUI/res/values-b+sr+Latn/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefon" - "Na čekanju" - "Nepoznat" - "Privatan broj" - "Telefonska govornica" - "Konferencijski poziv" - "Poziv je prekinut" - "Zvučnik" - "Slušalica telefona" - "Žičane naglavne slušalice" - "Bluetooth" - "Želite li da pošaljete sledeće tonove?\n" - "Tonovi se šalju\n" - "Pošalji" - "Da" - "Ne" - "Zamenite džoker znak sa" - "Konferencijski poziv %s" - "Broj govorne pošte" - "Poziva se" - "Ponovo se bira" - "Konferencijski poziv" - "Dolazni poziv" - "Dolazni poziv za Work" - "Poziv je završen" - "Na čekanju" - "Veza se prekida" - "Poziv je u toku" - "Moj broj je %s" - "Povezuje se video poziv" - "Video poziv" - "Zahteva se video poziv" - "Povezivanje video poziva nije uspelo" - "Zahtev za video poziv je odbijen" - "Broj za povratni poziv\n %1$s" - "Broj za hitan povratni poziv\n %1$s" - "Poziva se" - "Propušten poziv" - "Propušteni pozivi" - "Broj propuštenih poziva: %s" - "Propušten poziv od: %s" - "Tekući poziv" - "Tekući poziv za Work" - "Tekući Wi-Fi poziv" - "Tekući poziv za Work preko Wi-Fi-ja" - "Na čekanju" - "Dolazni poziv" - "Dolazni poziv za Work" - "Dolazni Wi-Fi poziv" - "Dolazni poziv za Work preko Wi-Fi-ja" - "Dolazni video poziv" - "Sumnja na nepoželjan dolazni poziv" - "Zahtev za dolazni video poziv" - "Nova poruka govorne pošte" - "Nova poruka govorne pošte (%d)" - "Pozovi %s" - "Nepoznat broj govorne pošte" - "Mobilna mreža nije dostupna" - "Izabrana mreža (%s) nije dostupna" - "Odgovori" - "Prekini vezu" - "Video" - "Glasovni" - "Prihvatam" - "Odbaci" - "Uzvrati poziv" - "Pošalji SMS" - "Poziv je u toku na drugom uređaju" - "Prebaci poziv" - "Da biste uputili poziv, prvo isključite režim rada u avionu." - "Nije registrovano na mreži." - "Mobilna mreža nije dostupna." - "Da biste uputili poziv, unesite važeći broj." - "Poziv nije uspeo." - "Pokreće se MMI sekvenca..." - "Usluga nije podržana." - "Zamena poziva nije uspela." - "Razdvajanje poziva nije uspelo." - "Prebacivanje nije uspelo." - "Konferencijski poziv nije uspeo." - "Odbijanje poziva nije uspelo." - "Uspostavljanje poziva nije uspelo." - "SIP poziv" - "Hitni poziv" - "Uključuje se radio…" - "Mobilna mreža nije dostupna. Pokušavamo ponovo…" - "Poziv nije uspeo. %s nije broj za hitne slučajeve." - "Poziv nije uspeo. Pozovite broj za hitne slučajeve." - "Koristite tastaturu za pozivanje" - "Stavi poziv na čekanje" - "Nastavi poziv" - "Završi poziv" - "Prikaži numeričku tastaturu" - "Sakrij numeričku tastaturu" - "Isključi zvuk" - "Uključi zvuk" - "Dodaj poziv" - "Objedini pozive" - "Zameni" - "Upravljaj pozivima" - "Upravljaj konferencijskim pozivom" - "Konferencijski poziv" - "Upravljaj" - "Audio" - "Video poziv" - "Promeni u glasovni poziv" - "Promeni kameru" - "Uključi kameru" - "Isključi kameru" - "Još opcija" - "Plejer je pokrenut" - "Plejer je zaustavljen" - "Kamera nije spremna" - "Kamera je spremna" - "Nepoznat događaj sesije poziva" - "Usluga" - "Podešavanje" - "<Nije podešeno>" - "Druga podešavanja poziva" - "Poziva se preko dobavljača %s" - "Dolazni poziv preko %s" - "slika kontakta" - "idi na privatno" - "izaberite kontakt" - "Napišite sami…" - "Otkaži" - "Pošalji" - "Odgovori" - "Pošalji SMS" - "Odbij" - "Odgovori video pozivom" - "Odgovori audio-pozivom" - "Prihvati zahtev za video" - "Odbij zahtev za video" - "Prihvati zahtev za odlazni video poziv" - "Odbij zahtev za odlazni video poziv" - "Prihvati zahtev za dolazni video poziv" - "Odbij zahtev za dolazni video poziv" - "Prevucite nagore za %s." - "Prevucite ulevo za %s." - "Prevucite udesno za %s." - "Prevucite nadole za %s." - "Vibracija" - "Vibracija" - "Zvuk" - "Podrazumevani zvuk (%1$s)" - "Melodija zvona telefona" - "Vibriraj kada zvoni" - "Melodija zvona i vibracija" - "Upravljaj konferencijskim pozivom" - "Broj za hitne slučajeve" - "Slika profila" - "Kamera je isključena" - "na %s" - "Beleška je poslata" - "Nedavne poruke" - "Informacije o preduzeću" - "Udaljenost je %.1f mi" - "Udaljenost je %.1f km" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "Otvara se sutra u %s" - "Otvara se danas u %s" - "Zatvara se u %s" - "Zatvorilo se danas u %s" - "Trenutno otvoreno" - "Trenutno zatvoreno" - "Nepoželjan pozivalac" - "Poziv se završio u %1$s" - "Ovo je bio prvi poziv sa ovog broja." - "Sumnjamo da je ovaj poziv nepoželjan." - "Blokiraj/prijavi" - "Dodaj kontakt" - "Nije nepoželjan" - diff --git a/InCallUI/res/values-be/strings.xml b/InCallUI/res/values-be/strings.xml deleted file mode 100644 index 70145fb7c..000000000 --- a/InCallUI/res/values-be/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Тэлефон" - "На ўтрыманні" - "Невядомы" - "Прыватны нумар" - "Таксафон" - "Канферэнц-выклік" - "Выклік абарваўся" - "Дынамік" - "Дынамік тэлефона" - "Правадная гарнітура" - "Bluetooth" - "Адправіць гэтыя тоны?\n" - "Адпраўка тонаў\n" - "Адправiць" - "Так" - "Не" - "Замяніце знак падстаноўкі на" - "Канферэнц-выклік у %s" - "Нумар галасавой пошты" - "Набор нумара" - "Паўторны набор" - "Канферэнц-выклік" - "Уваходны выклік" - "Уваходны выклік па працы" - "Выклік скончаны" - "На ўтрыманні" - "Завяршэнне выкліку" - "У выкліку" - "Мой нумар - %s" - "Падлучэнне відэа" - "Відэавыклік" - "Запыт на відэа" - "Немагчыма падлучыць відэавыклік" - "Запыт на відэа адхілены" - "Ваш нумар зваротнага выкліку\n %1$s" - "Ваш нумар экстраннага зваротнага выкліку\n %1$s" - "Набор нумара" - "Прапушчаны выклік" - "Прапушчаныя выклікі" - "Прапушчаных выклікаў: %s" - "Прапушчаны выклiк ад %s" - "Бягучы выклік" - "Бягучы выклік па працы" - "Бягучы выклік праз Wi-Fi" - "Бягучы выклік па працы праз Wi-Fi" - "На ўтрыманні" - "Уваходны выклік" - "Уваходны выклік па працы" - "Уваходны выклік праз Wi-Fi" - "Уваходны выклік па працы праз Wi-Fi" - "Уваходны відэавыклік" - "Уваходны выклiк ад абанента, якога падазраваюць у спаме" - "Уваходны запыт на відэавыклік" - "Новая галасавая пошта" - "Новыя паведамленнi галасавой пошты (%d)" - "Набраць %s" - "Невядомы нумар галасавой пошты" - "Не абслугоўваецца" - "Выбраная сетка (%s) недаступная" - "Адказ" - "Сконч. разм." - "Відэа" - "Галасавы" - "Прыняць" - "Адхіліць" - "Звар. выклік" - "Паведамленне" - "Бягучы выклік на іншай прыладзе" - "Перадаць выклік" - "Каб зрабіць выклік, спачатку выключыце рэжым палёту." - "Не зарэгістраваны ў сетцы." - "Мабільная сетка недаступная." - "Каб зрабіць выклік, увядзіце сапраўдны нумар." - "Выклік немагчымы." - "Пачатак паслядоўнасці MMI…" - "Служба не падтрымліваецца." - "Немагчыма пераключыць выклікі." - "Немагчыма аддзяліць выклік." - "Немагчыма перадаць выклік." - "Немагчыма зрабіць канферэнц-выклік." - "Немагчыма адхіліць выклік." - "Немагчыма скончыць выклік(і)." - "SIP-выклік" - "Экстранны выклік" - "Уключэнне радыё…" - "Не абслугоўваецца. Паўтор спробы…" - "Выклік немагчымы. %s не з\'яўляецца нумарам экстраннай службы." - "Выклік немагчымы. Набярыце нумар экстраннай службы." - "Набраць нумар з клавіятуры" - "Паставіць выклік на ўтрыманне" - "Узнавіць выклік" - "Завяршыць выклік" - "Паказаць панэль набору" - "Схаваць панэль набору" - "Адключыць мікрафон" - "Уключыць мікрафон" - "Дадаць выклік" - "Аб\'яднаць выклікі" - "Пераключыць" - "Кіраваць выклікамі" - "Кіраванне канферэнц-выклікам" - "Канферэнц-выклік" - "Кіраванне" - "Аўдыя" - "Відэавыклік" - "Змяніць на галасавы выклік" - "Пераключыць камеру" - "Уключыць камеру" - "Адключыць камеру" - "Дадатковыя параметры" - "Прайгравальнік запушчаны" - "Прайгравальнік спынены" - "Камера не гатовая" - "Камера гатовая" - "Невядомая падзея сеансу выкліку" - "Сэрвіс" - "Наладка" - "<Не зададзены>" - "Іншыя налады выклікаў" - "Выклікі праз правайдара %s" - "Уваходны выклік праз %s" - "фаіаграфія кантакту" - "перайсці да прыватнай гаворкі" - "выбраць кантакт" - "Напiшыце сваё…" - "Скасаваць" - "Адправiць" - "Адказ" - "Адправiць SMS" - "Адхіліць" - "Адказаць відэавыклікам" - "Адказаць аўдыявыклікам" - "Прыняць запыт на відэа" - "Адхіліць запыт на відэа" - "Прыняць запыт на перадачу відэа" - "Адхіліць запыт на перадачу відэа" - "Прыняць запыт на атрыманне відэа" - "Адхіліць запыт на атрыманне відэа" - "Правядзіце пальцам уверх, каб %s." - "Правядзіце пальцам улева, каб %s." - "Правядзіце пальцам управа, каб %s." - "Правядзіце пальцам уніз, каб %s." - "Вібрацыя" - "Вібрацыя" - "Гук" - "Стандартны гук (%1$s)" - "Рынгтон тэлефона" - "Вібрацыя падчас званка" - "Рынгтон і вiбрацыя" - "Кіраванне канферэнц-выклікам" - "Нумар экстраннай службы" - "Фота профілю" - "Камера адключана" - "праз %s" - "Нататка адпраўлена" - "Апошнія паведамленні" - "Бізнес-інфармацыя" - "Адлеглаць у мілях: %.1f" - "Адлегласць %.1f км" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "Адкрываецца заўтра ў %s" - "Адкрываецца сёння ў %s" - "Закрываецца ў %s" - "Закрыта сёння ў %s" - "Адкрыць зараз" - "Зараз закрыта" - "Падазраваецца ў спаме" - "Выклік завершаны %1$s" - "Вы атрымліваеце выклік з гэтага нумара ўпершыню." - "Існуе падазрэнне, што гэты выклік – спамерскі." - "Забл./павед.пра спам" - "Дадаць кантакт" - "Не спам" - diff --git a/InCallUI/res/values-bg/strings.xml b/InCallUI/res/values-bg/strings.xml deleted file mode 100644 index adf504777..000000000 --- a/InCallUI/res/values-bg/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Телефон" - "Задържано" - "Неизвестно лице" - "Частен номер" - "Обществен телефон" - "Конферентно обаждане" - "Обаждането бе прекъснато" - "Високоговорител" - "Телефонна слушалка" - "Слушалки с кабел" - "Bluetooth" - "Да се изпратят ли следните мелодии? \n" - "Мелодиите се изпращат\n" - "Изпращане" - "Да" - "Не" - "Замяна на заместващия символ със:" - "Конферентно обаждане – %s" - "Номер за гласова поща" - "Набира се" - "Набира се отново" - "Конферентно обаждане" - "Входящо обаждане" - "Входящо служебно обаждане" - "Обаждането завърши" - "Задържано" - "Разговорът се приключва" - "Извършва се обаждане" - "Номерът ми е %s" - "Установява се видеовръзка" - "Видеообаждане" - "Заявява се видеовръзка" - "Видеообаждането не може да се осъществи" - "Заявката за видеовръзка е отхвърлена" - "Номерът ви за обратно обаждане\n– %1$s" - "Номерът ви за спешно обратно обаждане\n– %1$s" - "Набиране" - "Пропуснато обаждане" - "Пропуснати обаждания" - "%s пропуснати обаждания" - "Пропуснато обаждане от %s" - "Текущо обаждане" - "Текущо служебно обаждане" - "Текущо обаждане през Wi-Fi" - "Текущо служебно обаждане през Wi-Fi" - "Задържано" - "Входящо обаждане" - "Входящо служебно обаждане" - "Входящо обаждане през Wi-Fi" - "Входящо служебно обаждане през Wi-Fi" - "Входящо видеообаждане" - "Входящо обаждане – възможен спам" - "Заявка за входящо видеообаждане" - "Нова гласова поща" - "Нова гласова поща (%d)" - "Набиране на %s" - "Неизвестен номер за гласова поща" - "Няма покритие" - "Избраната мрежа (%s) не е налице" - "Приемане" - "Затваряне" - "Видеообажд." - "Гл. обаждане" - "Приемане" - "Отхвърляне" - "Обр. обажд." - "Съобщение" - "Текущо обаждане на друго устройство" - "Прехвърляне на обаждането" - "За да осъществите обаждане, първо изключете самолетния режим." - "Няма регистрация в мрежата." - "Няма достъп до клетъчната мрежа." - "За да извършите обаждане, въведете валиден номер." - "Не може да се извърши обаждане." - "Стартира се последователността MMI…" - "Услугата не се поддържа." - "Обажданията не могат да се превключат." - "Обаждането не може да се отдели." - "Не може да се прехвърли." - "Не може да се извърши конферентно обаждане." - "Обаждането не може да се отхвърли." - "Обаждането или съответно обажданията не могат да се освободят." - "Обаждане чрез SIP" - "Спешно обаждане" - "Радиомодулът се включва…" - "Няма услуга. Извършва се нов опит…" - "Не може да се извърши обаждане. %s не е номер за спешни случаи." - "Не може да се извърши обаждане. Наберете номер за спешни случаи." - "Използвайте клавиатурата за набиране" - "Задържане на обаждането" - "Възобновяване на обаждането" - "Край на обаждането" - "Показване на клавиатурата за набиране" - "Скриване на клавиатурата за набиране" - "Заглушаване" - "Пускане" - "Добавяне на обаждане" - "Обединяване на обаждания" - "Размяна" - "Управление на обажданията" - "Управление на конф. обаждане" - "Конферентно обаждане" - "Управление" - "Аудио" - "Видеообажд." - "Преминаване към гласово обаждане" - "Превключване на камерата" - "Включване на камерата" - "Изключване на камерата" - "Още опции" - "Плейърът е стартиран" - "Плейърът е спрян" - "Камерата не е в готовност" - "Камерата е в готовност" - "Неизвестно събитие в сесията на обаждане" - "Услуга" - "Настройване" - "<Не е зададено>" - "Други настройки за обаждане" - "Обаждане чрез %s" - "Входящо обаждане чрез %s" - "снимка на контакта" - "превключване към частно обаждане" - "избиране на контакта" - "Напишете свой собствен..." - "Отказ" - "Изпращане" - "Приемане" - "Изпращане на SMS" - "Отхвърляне" - "Приемане като видеообаждане" - "Приемане като аудиообаждане" - "Приемане на заявката за видеовръзка" - "Отхвърляне на заявката за видеовръзка" - "Приемане на заявката за предаване на видео" - "Отхвърляне на заявката за предаване на видео" - "Приемане на заявката за получаване на видеообаждане" - "Отхвърляне на заявката за получаване на видео" - "Плъзнете нагоре за %s." - "Плъзнете наляво за %s." - "Плъзнете надясно за %s." - "Плъзнете надолу за %s." - "Вибриране" - "Вибриране" - "Звук" - "Стандартен звук (%1$s)" - "Мелодия на телефона" - "Вибриране при звънене" - "Мелодия и вибриране" - "Управление на конферентното обаждане" - "Спешен номер" - "Снимка на потребителския профил" - "Камерата е изключена" - "чрез %s" - "Бележката е изпратена" - "Скорошни съобщения" - "Бизнес информация" - "На %.1f мили" - "На %.1f км" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s; %2$s" - "Отваря утре в %s" - "Отваря днес в %s" - "Затваря в %s" - "Затворено днес в %s" - "В момента работи" - "В момента не работи" - "Възможен спам" - "Обаждането завърши %1$s" - "За първи път ви се обаждат от този номер." - "Подозирахме, че това обаждане може да е от разпространител на спам." - "Блокиране/спам" - "Добавяне на контакт" - "Не е спам" - diff --git a/InCallUI/res/values-bn/strings.xml b/InCallUI/res/values-bn/strings.xml deleted file mode 100644 index 2f561ce6e..000000000 --- a/InCallUI/res/values-bn/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "ফোন" - "হোল্ডে রয়েছে" - "অজানা" - "ব্যক্তিগত নম্বর" - "অর্থের বিনিময়ে কল করার ফোন" - "কনফারেন্স কল" - "কল সমাপ্ত হয়েছে" - "স্পিকার" - "হ্যান্ডসেট ইয়ারপিস" - "তারযুক্ত হেডসেট" - "ব্লুটুথ" - "নিম্নলিখিত টোনগুলি পাঠাবেন?\n" - "টোনগুলি পাঠানো হচ্ছে\n" - "পাঠান" - "হ্যাঁ" - "না" - "ওয়াইল্ড অক্ষরগুলিকে এর মাধ্যমে প্রতিস্থাপিত করুন" - "কনফারেন্স কল %s" - "ভয়েসমেল নম্বর" - "ডায়াল করা হচ্ছে" - "আবার ডায়াল করা হচ্ছে" - "কনফারেন্স কল" - "আগত কল" - "আগত কাজের কল" - "কল সমাপ্ত হয়েছে" - "হোল্ডে রয়েছে" - "কল নামিয়ে রাখা হচ্ছে" - "কলের সময়ে" - "আমার নম্বর হল %s" - "ভিডিও সংযুক্ত করছে" - "ভিডিও কল" - "ভিডিওর অনুরোধ করছে" - "ভিডিও কলে সংযোগ করা যাচ্ছে না" - "ভিডিওর অনুরোধ প্রত্যাখ্যান করা হয়েছে" - "আপনার কলব্যাক নম্বর\n%1$s" - "আপনার জরুরী কলব্যাক নম্বর\n%1$s" - "ডায়াল করা হচ্ছে" - "মিসড কল" - "মিসড কলগুলি" - "%sটি মিসড কল" - "%s এর থেকে মিসড কল" - "চলমান কল" - "চলমান কাজের কল" - "চলমান ওয়াই-ফাই কল" - "চলমান ওয়াই-ফাই কাজের কল" - "হোল্ডে রয়েছে" - "আগত কল" - "আগত কাজের কল" - "আগত ওয়াই-ফাই কল" - "আগত ওয়াই-ফাই কাজের কল" - "আগত ভিডিও কল" - "আগত সন্দেহভাজন স্প্যাম কল" - "আগত ভিডিও অনুরোধ" - "নতুন ভয়েসমেল" - "নতুন ভয়েসমেল (%dটি)" - "%s ডায়াল করুন" - "ভয়েসমেল নম্বর অজানা" - "কোনো পরিষেবা নেই" - "নির্বাচিত নেটওয়ার্ক (%s) অনুপলব্ধ" - "উত্তর" - "কল নামিয়ে রাখুন" - "ভিডিও" - "ভয়েস" - "স্বীকার করুন" - "খারিজ করুন" - "ঘুরিয়ে কল করুন" - "বার্তা" - "অন্য ডিভাইসে চালু থাকা কল" - "কল স্থানান্তর করুন" - "একটি কল করতে, প্রথমে বিমানমোড বন্ধ করুন৷" - "নেটওয়ার্কে নিবন্ধিত নয়৷" - "সেলুলার নেটওয়ার্ক উপলব্ধ নয়।" - "কোনো কল স্থাপন করতে, একটি বৈধ নম্বর লিখুন৷" - "কল করা যাবে না৷" - "MMI ক্রম চালু হচ্ছে…" - "পরিষেবা সমর্থিত নয়৷" - "কলগুলি স্যুইচ করা যাবে না৷" - "কল আলাদা করা যাবে না৷" - "হস্তান্তর করা যাবে না৷" - "কনফারেন্স করা যাবে না৷" - "কল প্রত্যাখ্যান কলা যাবে না৷" - "কল(গুলি) কাটা যাবে না৷" - "SIP কল" - "জরুরি কল" - "রেডিও চালু করা হচ্ছে…" - "কোন পরিষেবা নেই৷ আবার চেষ্টা করা হচ্ছে..." - "কল করা যাবে না৷ %s কোনো জরুরী নম্বর নয়৷" - "কল করা যাবে না৷ কোনো জরুরী নম্বর ডায়াল করুন৷" - "ডায়াল করতে কীবোর্ড ব্যবহার করুন" - "কল হোল্ডে রাখুন" - "কল আবার শুরু করুন" - "কল শেষ করুন" - "ডায়ালপ্যাড দেখান" - "ডায়ালপ্যাড লুকান" - "নিঃশব্দ করুন" - "সশব্দ করুন" - "কল যোগ করুন" - "কলগুলি মার্জ করুন" - "সোয়াপ করুন" - "কলগুলি পরিচালনা করুন" - "কনফারেন্স কল পরিচালনা করুন" - "কনফারেন্স কল" - "পরিচালনা করুন" - "অডিও" - "ভিডিও কল" - "ভয়েস কলে পরিবর্তন করুন" - "ক্যামেরা স্যুইচ করুন" - "ক্যামেরা চালু করুন" - "ক্যামেরা বন্ধ করুন" - "আরো বিকল্প" - "প্লেয়ার শুরু হয়েছে" - "প্লেয়ার বন্ধ হয়ে গেছে" - "ক্যামেরা রেডি নয়" - "ক্যামেরা রেডি" - "অজানা কল অধিবেশনের ইভেন্ট" - "পরিষেবা" - "সেটআপ" - "<সেট করা নেই>" - "অন্যান্য কল সেটিংস" - "%s এর মাধ্যমে কল করা হচ্ছে" - "%s এর মাধ্যমে ইনকামিং কল" - "পরিচিতির ফটো" - "ব্যক্তিগতভাবে কাজ করুন" - "পরিচিতি নির্বাচন করুন" - "আপনার নিজের পছন্দ মতো লিখুন…" - "বাতিল করুন" - "পাঠান" - "উত্তর" - "SMS পাঠান" - "অস্বীকার করুন" - "ভিডিও কল হিসেবে উত্তর দিন" - "অডিও কল হিসেবে উত্তর দিন" - "ভিডিওর অনুরোধ গ্রহণ করুন" - "ভিডিওর অনুরোধ প্রত্যাখ্যান করুন" - "ভিডিও প্রেরণ করার অনুরোধ স্বীকার করুন" - "ভিডিও প্রেরণ করার অনুরোধ প্রত্যাখ্যান করুন" - "ভিডিও গ্রহণ করার অনুরোধ স্বীকার করুন" - "ভিডিও গ্রহণ করার অনুরোধ প্রত্যাখ্যান করুন" - "%s এর জন্য উপরের দিকে স্লাইড করুন৷" - "%s এর জন্য বাঁ দিকে স্লাইড করুন৷" - "%s এর জন্য ডান দিকে স্লাইড করুন৷" - "%s এর জন্য নীচের দিকে স্লাইড করুন৷" - "কম্পন" - "কম্পন" - "শব্দ" - "ডিফল্ট শব্দ (%1$s)" - "ফোন রিংটোন" - "রিং হওয়ার সময় কম্পন হবে" - "রিংটোন ও কম্পন" - "কনফারেন্স কল পরিচালনা করুন" - "জরুরি নম্বর" - "প্রোফাইল ফটো" - "ক্যামেরা বন্ধ" - "%s এর মাধ্যমে" - "নোট পাঠানো হয়েছে" - "সাম্প্রতিক বার্তাগুলি" - "ব্যবসার তথ্য" - "%.1f মাইল দূরে" - "%.1f কিলোমিটার দূরে" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "আগামীকাল %s\'টায় খুলবে" - "আজ %s\'টায় খুলবে" - "%s\'টায় বন্ধ হয়" - "আজ %s\'টায় বন্ধ হয়েছে" - "এখন খোলা রয়েছে" - "এখন বন্ধ রয়েছে" - "সন্দেহভাজন স্প্যাম কলার" - "কল সমাপ্ত হয়েছে %1$s" - "এই প্রথমবার এই নম্বর থেকে আপনাকে কল করা হয়েছে৷" - "এটি কোনো স্প্যামারের কল হতে পারে বলে আমাদের মনে হচ্ছে৷" - "অবরুদ্ধ করুন/স্প্যাম হিসাবে অভিযোগ করুন" - "পরিচিতি যোগ করুন" - "স্প্যাম নয়" - diff --git a/InCallUI/res/values-bs/strings.xml b/InCallUI/res/values-bs/strings.xml deleted file mode 100644 index 9db727c98..000000000 --- a/InCallUI/res/values-bs/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefon" - "Na čekanju" - "Nepoznato" - "Privatni broj" - "Telefonska govornica" - "Konferencijski poziv" - "Poziv je prekinut" - "Zvučnik" - "Slušalice telefona" - "Žičane slušalice" - "Bluetooth" - "Poslati sljedeće tonove?\n" - "Slanje tonova\n" - "Pošalji" - "Da" - "Ne" - "Zamijeni zamjenski znak sa" - "Konferencijski poziv %s" - "Broj govorne pošte" - "Poziva se" - "Ponovno pozivanje" - "Konferencijski poziv" - "Dolazni poziv" - "Dolazni poslovni poziv" - "Poziv je završen" - "Na čekanju" - "Prekid veze" - "Poziv u toku" - "Moj broj je %s" - "Uspostavljanje videopoziva" - "Videopoziv" - "Zahtijevanje videopoziva" - "Nije moguće uspostaviti videopoziv" - "Zahtjev za videopoziv je odbijen" - "Vaš broj za povratni poziv\n %1$s" - "Vaš broj za hitni povratni poziv\n %1$s" - "Poziva se" - "Propušteni poziv" - "Propušteni pozivi" - "Propušteni pozivi: %s" - "Propušteni poziv od kontakta %s" - "Poziv u toku" - "Poslovni poziv u toku" - "Wi-Fi poziv u toku" - "Wi-Fi poslovni poziv u toku" - "Na čekanju" - "Dolazni poziv" - "Dolazni poslovni poziv" - "Dolazni Wi-Fi poziv" - "Dolazni Wi-Fi poslovni poziv" - "Dolazni videopoziv" - "Mogući neželjeni dolazni poziv" - "Zahtjev za dolazni videopoziv" - "Nova govorna pošta" - "Nova govorna pošta (%d)" - "Pozovi %s" - "Nepoznat broj govorne pošte" - "Nema mreže" - "Odabrana mreža (%s) je nedostupna" - "Odgovori" - "Prekini vezu" - "Videopoziv" - "Glasovni poziv" - "Prihvati" - "Odbaci" - "Povr. poziv" - "Poruka" - "Poziv u toku na drugom uređaju" - "Prenesi poziv" - "Da uputite poziv, isključite Način rada u avionu." - "Nije registrirano na mreži." - "Mobilna mreža nije dostupna." - "Da uputite poziv, upišite važeći broj." - "Nije moguće pozvati." - "Pokretanje MMI sekvence u toku…" - "Usluga nije podržana." - "Nije moguće prebacivanje poziva." - "Nije moguće odvojiti poziv." - "Prijenos nije moguć." - "Konferencijski poziv nije uspio." - "Nije moguće odbiti poziv." - "Nije moguće uputiti poziv(e)." - "SIP poziv" - "Hitni poziv" - "Uključivanje radija u toku…" - "Nema mreže. Ponovni pokušaj u toku…" - "Nije moguće pozvati. %s nije broj za htine slučajeve." - "Nije moguće pozvati. Pozovite broj za hitne slučajeve." - "Koristi tastaturu za biranje" - "Stavi poziv na čekanje" - "Nastavi poziv" - "Prekini poziv" - "Prikaži telefonsku tipkovnicu" - "Sakrij telefonsku tipkovnicu" - "Isključi zvuk" - "Uključi zvuk" - "Dodaj poziv" - "Spoji pozive" - "Zamijeni" - "Upravljaj pozivima" - "Upravljaj konf. pozivom" - "Konferencijski poziv" - "Upravljaj" - "Zvuk" - "Videopoziv" - "Promijeni na glasovni poziv" - "Promijeni kameru" - "Uključi kameru" - "Isključi kameru" - "Više opcija" - "Plejer je pokrenut" - "Plejer je zaustavljen" - "Kamera nije spremna" - "Kamera je spremna" - "Nepoznati događaj sesije poziva" - "Usluga" - "Postavljanje" - "<Nije postavljeno>" - "Ostale postavke poziva" - "Pozivanje putem %s" - "Dolazni poziv putem %s" - "fotografija kontakta" - "idi na privatno" - "odaberi kontakt" - "Napišite svoj..." - "Otkaži" - "Pošalji" - "Odgovori" - "Pošalji SMS" - "Odbij" - "Odgovori videopozivom" - "Prihvati kao audiopoziv" - "Prihvati zahtjev za videopoziv" - "Odbij zahtjev za videopoziv" - "Prihvati zahtjev za slanje videopoziva" - "Odbij zahtjev za slanje videopoziva" - "Prihvati zahtjev za primanje videopoziva" - "Odbij zahtjev za primanje videopoziva" - "Kliznite nagore za %s." - "Kliznite lijevo za %s." - "Kliznite nadesno za %s." - "Kliznite nadolje za %s." - "Vibracija" - "Vibracija" - "Zvuk" - "Zadani zvuk (%1$s)" - "Melodija zvona telefona" - "Vibriraj kada zvoni" - "Melodija zvona i vibracija" - "Upravljaj konferencijskim pozivom" - "Broj za hitne slučajeve" - "Fotografija profila" - "Kamera je isključena" - "putem %s" - "Bilješka je poslana" - "Nedavne poruke" - "Informacije o preduzeću" - "Udaljenost u miljama: %.1f" - "Udaljenost u km: %.1f" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "Otvara se sutra u %s" - "Otvara se danas u %s" - "Zatvara se u %s" - "Zatvoreno danas u %s" - "Otvori sad" - "Zatvoreno sada" - "Neželjeni pozivalac" - "Poziv je završen %1$s" - "Ovo je prvi poziv koji ste primili s ovog broja." - "Sumnjamo da je ovaj poziv neželjen sadržaj." - "Blokiraj/prijavi" - "Dodaj kontakt" - "Nije neželjeno" - diff --git a/InCallUI/res/values-ca/strings.xml b/InCallUI/res/values-ca/strings.xml deleted file mode 100644 index 103f15b63..000000000 --- a/InCallUI/res/values-ca/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telèfon" - "En espera" - "Desconegut" - "Número privat" - "Telèfon públic" - "Conferència" - "La trucada s\'ha interromput" - "Altaveu" - "Auricular" - "Auricular amb cable" - "Bluetooth" - "Vols enviar els tons següents?\n" - "S\'estan enviant els tons\n" - "Envia" - "Sí" - "No" - "Substitueix el caràcter comodí per" - "Conferència, %s" - "Número del missatge de veu" - "S\'està marcant" - "S\'està tornant a marcar" - "Conferència" - "Trucada entrant" - "Trucada de feina entrant" - "Ha finalitzat la trucada" - "En espera" - "S\'està penjant" - "En una trucada" - "El meu número és %s" - "S\'està connectant el vídeo" - "Videotrucada" - "S\'està sol·licitant el vídeo" - "No es pot connectar la videotrucada" - "S\'ha rebutjat la sol·licitud de vídeo" - "Número de devolució de trucada\n %1$s" - "Número de devolució de trucada d\'emergència\n %1$s" - "S\'està marcant" - "Trucada perduda" - "Trucades perdudes" - "%s trucades perdudes" - "Trucada perduda de %s" - "Trucada en curs" - "Trucada de feina en curs" - "Trucada per Wi-Fi en curs" - "Trucada de feina per Wi-Fi en curs" - "En espera" - "Trucada entrant" - "Trucada de feina entrant" - "Trucada per Wi-Fi entrant" - "Trucada de feina per Wi-Fi entrant" - "Videotrucada entrant" - "Presumpta trucada brossa entrant" - "Sol·licitud de vídeo entrant" - "Missatge de veu nou" - "Missatges de veu nous (%d)" - "Marca %s" - "Número del missatge de veu desconegut" - "Sense servei" - "La xarxa seleccionada (%s) no està disponible" - "Respon" - "Penja" - "Vídeo" - "Veu" - "Accepta" - "Ignora" - "Torna trucada" - "Missatge" - "Trucada en curs en un altre dispositiu" - "Transfereix la trucada" - "Per fer una trucada, primer desactiva el mode d\'avió." - "No s\'ha registrat a la xarxa." - "La xarxa mòbil no està disponible." - "Introdueix un número vàlid per fer una trucada." - "No es pot trucar." - "S\'està iniciant la seqüència MMI…" - "El servei no és compatible." - "No es pot canviar de trucada." - "No es pot separar la trucada." - "No es poden transferir trucades." - "No es pot establir la conferència." - "No es pot rebutjar la trucada." - "No es poden alliberar trucades." - "Trucada de SIP" - "Trucada d\'emergència" - "S\'està activant el senyal mòbil…" - "No hi ha servei. S\'està tornant a provar…" - "No es pot trucar. %s no és un número d\'emergència." - "No es pot trucar. Marca un número d\'emergència." - "Utilitza el teclat per marcar" - "Posa la trucada en espera" - "Reprèn la trucada" - "Finalitza la trucada" - "Mostra el teclat" - "Amaga el teclat" - "Silencia" - "Activa el so" - "Afegeix una trucada" - "Combina les trucades" - "Canvia" - "Gestiona les trucades" - "Gestiona la conferència" - "conferència" - "Gestiona" - "Àudio" - "Videotrucada" - "Canvia a trucada de veu" - "Canvia la càmera" - "Activa la càmera" - "Desactiva la càmera" - "Més opcions" - "S\'ha iniciat el reproductor" - "S\'ha aturat el reproductor" - "La càmera no està preparada" - "La càmera està preparada" - "Esdeveniment de sessió de trucada desconeguda" - "Servei" - "Configura" - "<No definit>" - "Altres opcions de configuració de les trucades" - "S\'està trucant amb %s" - "Trucada entrant mitjançant %s" - "foto de contacte" - "conferència privada" - "selecciona el contacte" - "Escriu la teva…" - "Cancel·la" - "Envia" - "Respon" - "Envia SMS" - "Rebutja" - "Respon amb una videotrucada" - "Respon amb una trucada d\'àudio" - "Accepta la sol·licitud de vídeo" - "Rebutja la sol·licitud de vídeo" - "Accepta la sol·licitud per transmetre vídeo" - "Rebutja la sol·licitud per transmetre vídeo" - "Accepta la sol·licitud per rebre vídeo" - "Rebutja la sol·licitud per rebre vídeo" - "Fes lliscar el dit cap amunt per %s." - "Fes lliscar el dit cap a l\'esquerra per %s." - "Fes lliscar el dit cap a la dreta per %s." - "Fes lliscar el dit cap avall per %s." - "Vibració" - "Vibració" - "So" - "So predeterminat (%1$s)" - "So de trucada" - "Vibrar en sonar" - "So de trucada i vibració" - "Gestiona la conferència" - "Número d\'emergència" - "Foto de perfil" - "La càmera està desactivada" - "mitjançant %s" - "S\'ha enviat la nota" - "Missatges recents" - "Informació de l\'empresa" - "A %.1f mi de distància" - "A %.1f km de distància" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "Obre demà a les %s" - "Obre avui a les %s" - "Tanca a les %s" - "Avui ha tancat a les %s" - "Obert ara" - "Ara és tancat" - "Possible trucada brossa" - "La trucada amb el número %1$s ha finalitzat" - "És la primera vegada que aquest número t\'ha trucat." - "Sospitem que aquesta trucada prové d\'un emissor de contingut brossa." - "Bloqueja/marca brossa" - "Afegeix un contacte" - "No és brossa" - diff --git a/InCallUI/res/values-cs/strings.xml b/InCallUI/res/values-cs/strings.xml deleted file mode 100644 index f3e4a5ed3..000000000 --- a/InCallUI/res/values-cs/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefon" - "Přidržený hovor" - "Neznámý volající" - "Soukromé číslo" - "Telefonní automat" - "Konferenční hovor" - "Volání zrušeno" - "Reproduktor" - "Sluchátko telefonu" - "Kabelová náhlavní soupr." - "Bluetooth" - "Odeslat následující tóny?\n" - "Odesílání tónů\n" - "Odeslat" - "Ano" - "Ne" - "Nahradit zástupné znaky jinými znaky" - "Konferenční hovor %s" - "Číslo hlasové schránky" - "Vytáčení" - "Opakované vytáčení" - "Konferenční hovor" - "Příchozí hovor" - "Příchozí pracovní hovor" - "Hovor ukončen" - "Přidržený hovor" - "Ukončování hovoru" - "Probíhá hovor" - "Moje číslo je %s" - "Navazování spojení pro video" - "Videohovor" - "Požadování videa" - "Videohovor nelze zahájit" - "Žádost o video byla zamítnuta" - "Vaše číslo pro zpětné volání\n%1$s" - "Vaše číslo pro tísňové zpětné volání\n%1$s" - "Vytáčení" - "Zmeškaný hovor" - "Zmeškané hovory" - "Zmeškané hovory: %s" - "Zmeškaný hovor od volajícího %s" - "Probíhající hovor" - "Probíhající pracovní hovor" - "Probíhající hovor přes Wi-Fi" - "Probíhající pracovní hovor přes Wi-Fi" - "Přidržený hovor" - "Příchozí hovor" - "Příchozí pracovní hovor" - "Příchozí hovor přes Wi-Fi" - "Příchozí pracovní hovor přes Wi-Fi" - "Příchozí videohovor" - "U příchozího hovoru máme podezření, že se jedná o spam" - "Příchozí žádost o videohovor" - "Nová hlasová zpráva" - "Nové hlasové zprávy (%d)" - "Volat hlasovou schránku %s" - "Číslo hlasové schránky není známé" - "Žádný signál" - "Vybraná síť (%s) není k dispozici" - "Přijmout" - "Zavěsit" - "Videohovor" - "Hlas. hovor" - "Přijmout" - "Odmítnout" - "Zavolat zpět" - "Posl. zprávu" - "Probíhá hovor na jiném zařízení" - "Převést hovor sem" - "Chcete-li telefonovat, nejprve vypněte režim Letadlo." - "Přihlášení k síti nebylo úspěšné." - "Mobilní síť je nedostupná." - "Chcete-li uskutečnit hovor, zadejte platné telefonní číslo." - "Hovor nelze uskutečnit." - "Spouštění sekvence MMI..." - "Služba není podporována." - "Hovory nelze přepnout." - "Hovor nelze rozdělit." - "Hovor nelze předat." - "Konferenční hovor nelze uskutečnit." - "Hovor nelze odmítnout." - "Hovor nelze ukončit." - "Volání SIP" - "Tísňové volání" - "Zapínání bezdrátového modulu..." - "Žádný signál. Probíhá další pokus…" - "Hovor nelze uskutečnit. %s není číslo tísňového volání." - "Hovor nelze uskutečnit. Vytočte číslo tísňového volání." - "Vytočte číslo pomocí klávesnice" - "Podržet hovor" - "Obnovit hovor" - "Ukončit hovor" - "Zobrazit číselník" - "Skrýt číselník" - "Vypnout zvuk" - "Zapnout zvuk" - "Přidat hovor" - "Spojit hovory" - "Zaměnit" - "Spravovat hovory" - "Spravovat konferenční hovor" - "Konferenční hovor" - "Spravovat" - "Zvuk" - "Videohovor" - "Změnit na hlasové volání" - "Přepnout kameru" - "Zapnout kameru" - "Vypnout kameru" - "Další možnosti" - "Přehrávač spuštěn" - "Přehrávač zastaven" - "Fotoaparát není připraven" - "Fotoaparát je připraven" - "Neznámá událost relace volání" - "Služba" - "Nastavení" - "<Nenastaveno>" - "Další nastavení hovorů" - "Volání prostřednictvím poskytovatele %s" - "Příchozí hovor přes poskytovatele %s" - "fotografie kontaktu" - "přepnout na soukromé" - "vybrat kontakt" - "Napsat vlastní odpověď..." - "Zrušit" - "Odeslat" - "Přijmout" - "Odeslat SMS" - "Odmítnout" - "Přijmout jako videohovor" - "Přijmout jako hlasový hovor" - "Přijmout žádost o videhovor" - "Odmítnout žádost o videohovor" - "Přijmout žádost o odesílání videa" - "Odmítnout žádost o odesílání videa" - "Přijmout žádost o příjem videa" - "Odmítnout žádost o příjem videa" - "%s – přejeďte prstem nahoru" - "%s – přejeďte prstem doleva" - "%s – přejeďte prstem doprava" - "%s – přejeďte prstem dolů" - "Vibrace" - "Vibrace" - "Zvuk" - "Výchozí zvuk (%1$s)" - "Vyzváněcí tón telefonu" - "Vibrace při vyzvánění" - "Vyzvánění a vibrace" - "Správa konferenčního hovoru" - "Číslo tísňové linky" - "Profilová fotka" - "Fotoaparát je vypnutý" - "pomocí čísla %s" - "Poznámka byla odeslána" - "Nejnovější zprávy" - "Informace o firmě" - "Vzdálenost: %.1f mi" - "Vzdálenost: %.1f km" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "Zítra otevírá v %s" - "Dnes otevírá v %s" - "Zavírá v %s" - "Dnes zavřeno od %s" - "Otevřeno" - "Nyní zavřeno" - "Podezření na spam" - "Hovor skončil v %1$s" - "Toto číslo vám volalo poprvé." - "Máme podezření, že tento hovor byl spam." - "Blok./nahlásit spam" - "Přidat kontakt" - "Nešlo o spam" - diff --git a/InCallUI/res/values-da/strings.xml b/InCallUI/res/values-da/strings.xml deleted file mode 100644 index 82d773c1c..000000000 --- a/InCallUI/res/values-da/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefon" - "Afventer" - "Ukendt" - "Privat nummer" - "Mønttelefon" - "Telefonmøde" - "Opkaldet blev afbrudt." - "Højttaler" - "Ørestykke til håndsæt" - "Headset med ledning" - "Bluetooth" - "Vil du sende følgende toner?\n" - "Sender toner\n" - "Send" - "Ja" - "Nej" - "Erstat jokertegnet med" - "Telefonmøde %s" - "Telefonsvarernummer" - "Ringer op" - "Ringer op igen" - "Telefonmøde" - "Indgående opkald" - "Indgående arbejdsopkald" - "Opkaldet er afsluttet" - "Afventer" - "Lægger på" - "Opkald i gang" - "Mit nummer er %s" - "Opretter videoforbindelse" - "Videoopkald" - "Anmoder om video" - "Kan ikke forbinde videoopkald" - "Videoanmodningen blev afvist" - "Dit tilbagekaldsnummer\n %1$s" - "Dit tilbagekaldsnummer til nødopkald\n %1$s" - "Ringer op" - "Ubesvaret opkald" - "Ubesvarede opkald" - "%s ubesvarede opkald" - "Ubesvaret opkald fra %s" - "Igangværende opkald" - "Igangværende opkald i forbindelse med arbejde" - "Igangværende opkald via Wi-Fi" - "Igangværende Wi-Fi-opkald i forbindelse med arbejde" - "Afventer" - "Indgående opkald" - "Indgående arbejdsopkald" - "Indgående Wi-Fi-opkald" - "Indgående Wi-Fi-opkald i forbindelse med arbejde" - "Indgående videoopkald" - "Indgående formodet spamopkald" - "Indgående videoanmodning" - "Ny telefonsvarerbesked" - "Nye telefonsvarerbeskeder (%d)" - "Ring til %s" - "Telefonsvarernummeret er ukendt" - "Ingen dækning" - "Det valgte netværk (%s) er ikke tilgængeligt" - "Besvar" - "Læg på" - "Video" - "Tale" - "Acceptér" - "Afvis" - "Ring tilbage" - "Besked" - "Igangværende opkald på en anden enhed" - "Overfør opkald" - "Slå Flytilstand fra først for at foretage et opkald." - "Ikke registreret på netværket." - "Mobilnetværket er ikke tilgængeligt." - "Indtast et gyldigt nummer for at foretage et opkald." - "Der kan ikke ringes op." - "Starter MMI-sekvens…" - "Tjenesten er ikke understøttet." - "Der kan ikke skiftes opkald." - "Opkaldet kan ikke adskilles." - "Der kan ikke viderestilles." - "Der kan ikke oprettes telefonmøde." - "Opkaldet kan ikke afvises." - "Et eller flere opkald kan ikke frigives." - "SIP-opkald" - "Nødopkald" - "Tænder for radio…" - "Ingen tjeneste. Prøver igen…" - "Der kan ikke ringes op. %s er ikke et alarmnummer." - "Der kan ikke ringes op. Ring til et alarmnummer." - "Brug tastaturet til at ringe op" - "Sæt opkald i venteposition" - "Genoptag opkald" - "Afslut opkald" - "Vis numerisk tastatur" - "Skjul numerisk tastatur" - "Slå lyden fra" - "Slå lyden til" - "Tilføj opkald" - "Slå opkald sammen" - "Skift" - "Administrer opkald" - "Administrer telefonmøde" - "Telefonmøde" - "Administrer" - "Lyd" - "Videoopkald" - "Skift til taleopkald" - "Skift kamera" - "Slå kameraet til" - "Slå kameraet fra" - "Flere valgmuligheder" - "Afspilleren er startet" - "Afspilleren er stoppet" - "Kameraet er ikke klar" - "Kameraet er klar" - "Ukendt opkaldsbegivenhed" - "Tjeneste" - "Konfiguration" - "<Ikke angivet>" - "Andre indstillinger for opkald" - "Opkald via %s" - "Indgående opkald via %s" - "billede af kontaktperson" - "gør privat" - "vælg kontaktperson" - "Skriv dit eget svar…" - "Annuller" - "Send" - "Besvar" - "Send sms" - "Afvis" - "Besvar som videoopkald" - "Besvar som taleopkald" - "Acceptér anmodning om video" - "Afvis videoanmodning" - "Acceptér anmodning om udgående video" - "Afvis anmodning om udgående video" - "Acceptér anmodning om indgående video" - "Afvis anmodning om indgående video" - "Skub op for at %s." - "Skub til venstre for at %s." - "Skub til højre for at %s." - "Skub ned for at %s." - "Vibration" - "Vibration" - "Lyd" - "Standardlyd (%1$s)" - "Ringetone ved opkald" - "Vibrer ved opringning" - "Ringetone og vibration" - "Administrer telefonmøde" - "Alarmnummer" - "Profilbillede" - "Kameraet er slukket" - "via %s" - "Noten er sendt" - "Seneste beskeder" - "Virksomhedsoplysninger" - "%.1f mil væk" - "%.1f km væk" - "%1$s, %2$s" - "%1$s-%2$s" - "%1$s, %2$s" - "Åbner i morgen kl. %s" - "Åbner i dag kl. %s" - "Lukker kl. %s" - "Lukkede i dag kl. %s" - "Åbent nu" - "Lukket for i dag" - "Formodet spammer" - "Opkaldet blev afsluttet %1$s" - "Dette er første gang, at dette nummer har ringet til dig." - "Vi har mistanke om, at dette er et spamopkald." - "Bloker/rap. spam" - "Tilføj kontaktperson" - "Ikke spam" - diff --git a/InCallUI/res/values-de/strings.xml b/InCallUI/res/values-de/strings.xml deleted file mode 100644 index b3ecffc4c..000000000 --- a/InCallUI/res/values-de/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefon" - "Gehaltener Anruf" - "Unbekannt" - "Private Nummer" - "Münztelefon" - "Telefonkonferenz" - "Verbindung unterbrochen" - "Lautsprecher" - "Mobilgerät-Kopfhörer" - "Kabelgebundenes Headset" - "Bluetooth" - "Folgende Töne senden?\n" - "Töne werden gesendet\n" - "Senden" - "Ja" - "Nein" - "Platzhalter ersetzen durch" - "Telefonkonferenz %s" - "Mailboxnummer" - "Rufaufbau" - "Wahlwiederholung" - "Telefonkonferenz" - "Eingehender Anruf" - "Eingeh. geschäftl. Anruf" - "Anruf beendet" - "Gehaltener Anruf" - "Auflegen" - "Im Gespräch" - "Meine Nummer lautet %s" - "Videoverbindung wird hergestellt" - "Videoanruf" - "Videoanfrage wird gesendet" - "Videoanruf kann nicht verbunden werden" - "Videoanfrage abgelehnt" - "Deine Rückrufnummer lautet:\n %1$s" - "Deine Notrufnummer lautet:\n %1$s" - "Rufaufbau" - "Verpasster Anruf" - "Entgangene Anrufe" - "%s entgangene Anrufe" - "Verpasster Anruf von %s" - "Aktiver Anruf" - "Aktiver geschäftlicher Anruf" - "Aktiver WLAN-Anruf" - "Aktiver geschäftlicher WLAN-Anruf" - "Gehaltener Anruf" - "Eingehender Anruf" - "Eingehender geschäftlicher Anruf" - "Eingehender WLAN-Anruf" - "Eingehender geschäftlicher WLAN-Anruf" - "Eingehender Videoanruf" - "Verdacht auf eingehenden Spam-Anruf" - "Eingehende Videoanfrage" - "Neue Mailbox-Nachricht" - "Neue Mailbox-Nachricht (%d)" - "%s wählen" - "Mailboxnummer unbekannt" - "Kein Service" - "Ausgewähltes Netzwerk (%s) nicht verfügbar" - "Annehmen" - "Beenden" - "Videoanruf" - "Sprachanruf" - "Akzeptieren" - "Ablehnen" - "Zurückrufen" - "Nachricht" - "Aktiver Anruf auf anderem Gerät" - "Anruf übertragen" - "Deaktiviere zunächst den Flugmodus, um einen Anruf zu tätigen." - "Nicht in Netzwerk registriert." - "Mobilfunknetz nicht verfügbar." - "Gib eine gültige Nummer ein, um einen Anruf zu tätigen." - "Anruf nicht möglich." - "MMI-Sequenz wird gestartet…" - "Dienst wird nicht unterstützt." - "Anruf kann nicht gewechselt werden." - "Anruf kann nicht getrennt werden." - "Anruf kann nicht übergeben werden." - "Konferenzschaltung nicht möglich." - "Anruf kann nicht abgelehnt werden." - "Anrufe können nicht freigegeben werden." - "SIP-Anruf" - "Notruf" - "Mobilfunkverbindung wird aktiviert…" - "Kein Service. Neuer Versuch…" - "Anruf nicht möglich. %s ist keine Notrufnummer." - "Anruf nicht möglich. Wähle eine Notrufnummer." - "Zum Wählen Tastatur verwenden" - "Anruf halten" - "Anruf fortsetzen" - "Anruf beenden" - "Wähltasten einblenden" - "Wähltasten ausblenden" - "Stummschalten" - "Stummschaltung aufheben" - "Anruf hinzufügen" - "Anrufe verbinden" - "Wechseln" - "Anrufe verwalten" - "Telefonkonferenz verwalten" - "Telefonkonferenz" - "Verwalten" - "Audio" - "Videoanruf" - "Zu Sprachanruf wechseln" - "Kamera wechseln" - "Kamera einschalten" - "Kamera ausschalten" - "Weitere Optionen" - "Videoübertragung gestartet" - "Videoübertragung gestoppt" - "Kamera nicht bereit" - "Kamera bereit" - "Unbekanntes Ereignis während eines Anrufs" - "Dienst" - "Einrichtung" - "<Nicht festgelegt>" - "Sonstige Anrufeinstellungen" - "Anruf über %s" - "Eingehender Anruf über %s" - "Kontaktbild" - "privat sprechen" - "Kontakt auswählen" - "Eigene Antwort schreiben…" - "Abbrechen" - "Senden" - "Annehmen" - "SMS senden" - "Ablehnen" - "Als Videoanruf annehmen" - "Als normalen Anruf annehmen" - "Videoanfrage akzeptieren" - "Videoanfrage ablehnen" - "Anfrage für ausgehenden Videoanruf akzeptieren" - "Anfrage für ausgehenden Videoanruf ablehnen" - "Anfrage für eingehenden Videoanruf akzeptieren" - "Anfrage für eingehenden Videoanruf ablehnen" - "Zum %s nach oben schieben." - "Zum %s nach links schieben." - "Zum %s nach rechts schieben." - "Zum %s nach unten schieben." - "Vibrieren" - "Vibrieren" - "Ton" - "Standardklingelton (%1$s)" - "Klingelton" - "Beim Klingeln vibrieren" - "Klingelton & Vibration" - "Telefonkonferenz verwalten" - "Notrufnummer" - "Profilbild" - "Kamera aus" - "über %s" - "Notiz gesendet" - "Zuletzt eingegangene Nachrichten" - "Geschäftsinformationen" - "%.1f Meilen entfernt" - "%.1f Kilometer entfernt" - "%1$s, %2$s" - "%1$s bis %2$s" - "%1$s, %2$s" - "Öffnet morgen um %s" - "Öffnet heute um %s" - "Schließt um %s" - "Hat heute um %s geschlossen" - "Jetzt geöffnet" - "Jetzt geschlossen" - "Verdacht auf Spam" - "Anruf beendet %1$s" - "Du wurdest das erste Mal von dieser Nummer angerufen." - "Dieser Anruf schien ein Spam-Anruf zu sein." - "Blockieren/Spam melden" - "Kontakt hinzufügen" - "Kein Spam" - diff --git a/InCallUI/res/values-el/strings.xml b/InCallUI/res/values-el/strings.xml deleted file mode 100644 index 993813290..000000000 --- a/InCallUI/res/values-el/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Τηλέφωνο" - "Σε αναμονή" - "Άγνωστος" - "Απόκρυψη αριθμού" - "Τηλέφωνο με χρέωση" - "Κλήση συνδιάσκεψης" - "Η κλήση απορρίφθηκε" - "Ηχείο" - "Ακουστικό" - "Ενσύρματο ακουστικό" - "Bluetooth" - "Αποστολή των παρακάτω ήχων;\n" - "Ήχοι αποστολής\n" - "Αποστολή" - "Ναι" - "Όχι" - "Αντικατάσταση του χαρακτήρα μπαλαντέρ με" - "Κλήση συνδιάσκεψης %s" - "Αριθμός αυτόματου τηλεφωνητή" - "Κλήση" - "Επανάκληση" - "Κλήση συνδιάσκεψης" - "Εισερχόμενη κλήση" - "Εισερχόμ. κλήση εργασίας" - "Η κλήση τερματίστηκε" - "Σε αναμονή" - "Κλείσιμο γραμμής" - "Σε κλήση" - "Ο αριθμός μου είναι %s" - "Σύνδεση βίντεο" - "Βιντεοκλήση" - "Αίτημα βίντεο" - "Δεν είναι δυνατή η σύνδεση βιντεοκλήσης" - "Το αίτημα βίντεο απορρίφθηκε" - "Αριθμός επανάκλησης\n %1$s" - "Αριθμός επανάκλησης έκτακτης ανάγκης\n %1$s" - "Κλήση" - "Αναπάντητη κλήση" - "Αναπάντητες κλήσεις" - "%s αναπάντητες κλήσεις" - "Αναπάντητη κλήση από %s" - "Κλήση σε εξέλιξη" - "Κλήση εργασίας σε εξέλιξη" - "Κλήση Wi-Fi σε εξέλιξη" - "Κλήση εργασίας μέσω Wi-Fi σε εξέλιξη" - "Σε αναμονή" - "Εισερχόμενη κλήση" - "Εισερχόμενη κλήση εργασίας" - "Εισερχόμενη κλήση μέσω Wi-Fi" - "Εισερχόμενη κλήση εργασίας μέσω Wi-Fi" - "Εισερχόμενη βιντεοκλήση" - "Πιθανώς ανεπιθύμητη εισερχόμενη κλήση" - "Αίτημα εισερχόμενου βίντεο" - "Νέο μήνυμα στον αυτόματο τηλεφωνητή" - "Νέο μήνυμα στον αυτόματο τηλεφωνητή (%d)" - "Καλέστε στο %s" - "Άγνωστος αριθμός αυτόματου τηλεφωνητή" - "Δίκτυο μη διαθέσιμο" - "Το επιλεγμένο δίκτυο (%s) δεν είναι διαθέσιμο" - "Απάντηση" - "Τερμ. κλήσης" - "Βίντεο" - "Φωνή" - "Αποδοχή" - "Παράβλεψη" - "Επανάκληση" - "Μήνυμα" - "Κλήση σε εξέλιξη σε άλλη συσκευή" - "Μεταφορά κλήσης" - "Για να πραγματοποιήσετε μια κλήση, απενεργοποιήστε πρώτα τη λειτουργία πτήσης." - "Δεν έχετε εγγραφεί στο δίκτυο." - "Το δίκτυο κινητής τηλεφωνίας δεν είναι διαθέσιμο." - "Για να πραγματοποιήσετε κλήση, εισαγάγετε έναν έγκυρο αριθμό." - "Δεν είναι δυνατή η κλήση." - "Έναρξη ακολουθίας MMI…" - "Η υπηρεσία δεν υποστηρίζεται." - "Δεν είναι δυνατή η εναλλαγή κλήσεων." - "Δεν είναι δυνατός ο διαχωρισμός της κλήσης." - "Δεν είναι δυνατή η μεταφορά." - "Δεν είναι δυνατή η συνδιάσκεψη." - "Δεν είναι δυνατή η απόρριψη της κλήσης." - "Δεν είναι δυνατή η πραγματοποίηση κλήσεων." - "Κλήση SIP" - "Κλήση έκτακτης ανάγκης" - "Ενεργοποίηση πομπού…" - "Δεν υπάρχει υπηρεσία. Νέα προσπάθεια…" - "Δεν είναι δυνατή η κλήση. Το %s δεν είναι αριθμός έκτακτης ανάγκης." - "Δεν είναι δυνατή η κλήση. Πληκτρολογήστε έναν αριθμό έκτακτης ανάγκης." - "Χρησιμοποιήστε το πληκτρολόγιο για να πραγματοποιήσετε μια κλήση" - "Αναμονή κλήσης" - "Συνέχιση κλήσης" - "Τερματισμός κλήσης" - "Εμφάνιση πληκτρολογίου κλήσης" - "Απόκρυψη πληκτρολογίου κλήσης" - "Σίγαση" - "Κατάργηση σίγασης" - "Προσθήκη κλήσης" - "Συγχώνευση κλήσεων" - "Ανταλλαγή" - "Διαχείριση κλήσεων" - "Διαχείριση κλήσης συνδιάσκεψης" - "Κλήση διάσκεψης" - "Διαχείριση" - "Ήχος" - "Βιντεοκλ." - "Αλλαγή σε φωνητική κλήση" - "Αλλαγή κάμερας" - "Ενεργοποίηση κάμερας" - "Απενεργοποίηση κάμερας" - "Περισσότερες επιλογές" - "Το πρόγραμμα αναπαραγωγής βίντεο ξεκίνησε" - "Το πρόγραμμα αναπαραγωγής βίντεο διακόπηκε" - "Η κάμερα δεν είναι έτοιμη" - "Η κάμερα είναι έτοιμη" - "Άγνωστο συμβάν περιόδου σύνδεσης κλήσης" - "Υπηρεσία" - "Ρύθμιση" - "<Δεν έχει οριστεί>" - "Άλλες ρυθμίσεις κλήσης" - "Κλήση μέσω %s" - "Εισερχόμενη κλήση μέσω %s" - "φωτογραφία επαφής" - "ιδιωτική χρήση" - "επιλογή επαφής" - "Συντάξτε τη δική σας…" - "Ακύρωση" - "Αποστολή" - "Απάντηση" - "Αποστολή SMS" - "Απόρριψη" - "Απάντηση ως βιντεοκλήση" - "Απάντηση ως κλήση ήχου" - "Αποδοχή αιτήματος βίντεο" - "Απόρριψη αιτήματος βίντεο" - "Αποδοχή αιτήματος μετάδοσης βίντεο" - "Απόρριψη αιτήματος μετάδοσης βίντεο" - "Αποδοχή αιτήματος λήψης βίντεο" - "Απόρριψη αιτήματος λήψης βίντεο" - "Κύλιση προς τα επάνω για %s." - "Κύλιση προς τα αριστερά για %s." - "Κύλιση προς τα δεξιά για %s." - "Κύλιση προς τα κάτω για %s." - "Δόνηση" - "Δόνηση" - "Ήχος" - "Προεπιλεγμένος ήχος (%1$s)" - "Ήχος κλήσης τηλεφώνου" - "Δόνηση κατά το κουδούνισμα" - "Ήχος κλήσης και δόνηση" - "Διαχείριση κλήσης συνδιάσκεψης" - "Αριθμός έκτακτης ανάγκης" - "Φωτογραφία προφίλ" - "Απενεργοποίηση κάμερας" - "μέσω %s" - "Η σημείωση εστάλη" - "Πρόσφατα μηνύματα" - "Πληροφορίες επιχείρησης" - "%.1f μίλια μακριά" - "%.1f χιλιόμετρα μακριά" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "Ανοίγει αύριο στις %s" - "Ανοίγει σήμερα στις %s" - "Κλείνει στις %s" - "Έκλεισε σήμερα στις %s" - "Ανοιχτό τώρα" - "Κλειστό τώρα" - "Πιθανώς ανεπιθύμητος" - "Η κλήση τερματίστηκε %1$s" - "Αυτή είναι η πρώτη φορά που σας καλεί αυτός ο αριθμός." - "Έχουμε υποψίες ότι αυτή κλήση είναι ανεπιθύμητη." - "Αποκλ./αναφ. ανεπιθ." - "Προσθήκη επαφής" - "Μη ανεπιθύμητος" - diff --git a/InCallUI/res/values-en-rAU/strings.xml b/InCallUI/res/values-en-rAU/strings.xml deleted file mode 100644 index 013aa9685..000000000 --- a/InCallUI/res/values-en-rAU/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Phone" - "On hold" - "unknown" - "Private number" - "Payphone" - "Conference call" - "Call cut off" - "Speaker" - "Handset earpiece" - "Wired headset" - "Bluetooth" - "Send the following tones?\n" - "Sending tones\n" - "Send" - "yes" - "no" - "Replace wild character with" - "Conference call %s" - "Voicemail number" - "Dialling" - "Redialling" - "Conference call" - "Incoming call" - "Incoming work call" - "Call ended" - "On hold" - "Hanging up" - "In call" - "My number is %s" - "Connecting video" - "Video call" - "Requesting video" - "Can\'t connect video call" - "Video request rejected" - "Your callback number\n %1$s" - "Your emergency callback number\n %1$s" - "Dialling" - "Missed call" - "Missed calls" - "%s missed calls" - "Missed call from %s" - "On-going call" - "Ongoing work call" - "Ongoing Wi-Fi call" - "Ongoing Wi-Fi work call" - "On hold" - "Incoming call" - "Incoming work call" - "Incoming Wi-Fi call" - "Incoming Wi-Fi work call" - "Incoming video call" - "Incoming suspected spam call" - "Incoming video request" - "New voicemail" - "New voicemail (%d)" - "Dial %s" - "Voicemail number unknown" - "No service" - "Selected network (%s) unavailable" - "Answer" - "Hang up" - "In-stream video" - "Voice" - "Accept" - "Dismiss" - "Call back" - "Message" - "Ongoing call on another device" - "Transfer call" - "To place a call, first turn off Aeroplane mode." - "Not registered on network." - "Mobile network not available." - "To place a call, enter a valid number." - "Can\'t call." - "Starting MMI sequence…" - "Service not supported." - "Can\'t switch calls." - "Can\'t separate call." - "Can\'t transfer." - "Can\'t conference." - "Can\'t reject call." - "Can\'t release call(s)." - "SIP call" - "Emergency call" - "Turning on radio…" - "No network. Trying again…" - "Can\'t call. %s is not an emergency number." - "Can\'t call. Dial an emergency number." - "Use keyboard to dial" - "Hold Call" - "Resume Call" - "End Call" - "Show dial pad" - "Hide dial pad" - "Mute" - "Unmute" - "Add call" - "Merge calls" - "Swap" - "Manage calls" - "Manage conference call" - "Conference call" - "Manage" - "Audio" - "Video call" - "Change to voice call" - "Switch camera" - "Turn on camera" - "Turn off camera" - "More options" - "Player Started" - "Player Stopped" - "Camera not ready" - "Camera ready" - "Unknown call session event" - "Service" - "Set up" - "<Not set>" - "Other call settings" - "Calling via %s" - "Incoming via %s" - "contact photo" - "go private" - "select contact" - "Write your own..." - "cancel" - "Send" - "Answer" - "Send SMS" - "Decline" - "Answer as video call" - "Answer as audio call" - "Accept video request" - "Decline video request" - "Accept video transmit request" - "Decline video transmit request" - "Accept video receive request" - "Decline video receive request" - "Slide up for %s." - "Slide left for %s." - "Slide right for %s." - "Slide down for %s." - "Vibrate" - "Vibrate" - "Sound" - "Default sound (%1$s)" - "Phone ringtone" - "Vibrate when ringing" - "Ringtone & Vibrate" - "Manage conference call" - "Emergency number" - "Profile photo" - "Camera off" - "via %s" - "Note sent" - "Recent messages" - "Business info" - "%.1f mi away" - "%.1f km away" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "Opens tomorrow at %s" - "Opens today at %s" - "Closes at %s" - "Closed today at %s" - "Open now" - "Closed now" - "Suspected spam caller" - "Call ended %1$s" - "This is the first time that this number has called you." - "We suspected that this call was from a spammer." - "Block/report spam" - "Add contact" - "Not spam" - diff --git a/InCallUI/res/values-en-rGB/strings.xml b/InCallUI/res/values-en-rGB/strings.xml deleted file mode 100644 index 013aa9685..000000000 --- a/InCallUI/res/values-en-rGB/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Phone" - "On hold" - "unknown" - "Private number" - "Payphone" - "Conference call" - "Call cut off" - "Speaker" - "Handset earpiece" - "Wired headset" - "Bluetooth" - "Send the following tones?\n" - "Sending tones\n" - "Send" - "yes" - "no" - "Replace wild character with" - "Conference call %s" - "Voicemail number" - "Dialling" - "Redialling" - "Conference call" - "Incoming call" - "Incoming work call" - "Call ended" - "On hold" - "Hanging up" - "In call" - "My number is %s" - "Connecting video" - "Video call" - "Requesting video" - "Can\'t connect video call" - "Video request rejected" - "Your callback number\n %1$s" - "Your emergency callback number\n %1$s" - "Dialling" - "Missed call" - "Missed calls" - "%s missed calls" - "Missed call from %s" - "On-going call" - "Ongoing work call" - "Ongoing Wi-Fi call" - "Ongoing Wi-Fi work call" - "On hold" - "Incoming call" - "Incoming work call" - "Incoming Wi-Fi call" - "Incoming Wi-Fi work call" - "Incoming video call" - "Incoming suspected spam call" - "Incoming video request" - "New voicemail" - "New voicemail (%d)" - "Dial %s" - "Voicemail number unknown" - "No service" - "Selected network (%s) unavailable" - "Answer" - "Hang up" - "In-stream video" - "Voice" - "Accept" - "Dismiss" - "Call back" - "Message" - "Ongoing call on another device" - "Transfer call" - "To place a call, first turn off Aeroplane mode." - "Not registered on network." - "Mobile network not available." - "To place a call, enter a valid number." - "Can\'t call." - "Starting MMI sequence…" - "Service not supported." - "Can\'t switch calls." - "Can\'t separate call." - "Can\'t transfer." - "Can\'t conference." - "Can\'t reject call." - "Can\'t release call(s)." - "SIP call" - "Emergency call" - "Turning on radio…" - "No network. Trying again…" - "Can\'t call. %s is not an emergency number." - "Can\'t call. Dial an emergency number." - "Use keyboard to dial" - "Hold Call" - "Resume Call" - "End Call" - "Show dial pad" - "Hide dial pad" - "Mute" - "Unmute" - "Add call" - "Merge calls" - "Swap" - "Manage calls" - "Manage conference call" - "Conference call" - "Manage" - "Audio" - "Video call" - "Change to voice call" - "Switch camera" - "Turn on camera" - "Turn off camera" - "More options" - "Player Started" - "Player Stopped" - "Camera not ready" - "Camera ready" - "Unknown call session event" - "Service" - "Set up" - "<Not set>" - "Other call settings" - "Calling via %s" - "Incoming via %s" - "contact photo" - "go private" - "select contact" - "Write your own..." - "cancel" - "Send" - "Answer" - "Send SMS" - "Decline" - "Answer as video call" - "Answer as audio call" - "Accept video request" - "Decline video request" - "Accept video transmit request" - "Decline video transmit request" - "Accept video receive request" - "Decline video receive request" - "Slide up for %s." - "Slide left for %s." - "Slide right for %s." - "Slide down for %s." - "Vibrate" - "Vibrate" - "Sound" - "Default sound (%1$s)" - "Phone ringtone" - "Vibrate when ringing" - "Ringtone & Vibrate" - "Manage conference call" - "Emergency number" - "Profile photo" - "Camera off" - "via %s" - "Note sent" - "Recent messages" - "Business info" - "%.1f mi away" - "%.1f km away" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "Opens tomorrow at %s" - "Opens today at %s" - "Closes at %s" - "Closed today at %s" - "Open now" - "Closed now" - "Suspected spam caller" - "Call ended %1$s" - "This is the first time that this number has called you." - "We suspected that this call was from a spammer." - "Block/report spam" - "Add contact" - "Not spam" - diff --git a/InCallUI/res/values-en-rIN/strings.xml b/InCallUI/res/values-en-rIN/strings.xml deleted file mode 100644 index 013aa9685..000000000 --- a/InCallUI/res/values-en-rIN/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Phone" - "On hold" - "unknown" - "Private number" - "Payphone" - "Conference call" - "Call cut off" - "Speaker" - "Handset earpiece" - "Wired headset" - "Bluetooth" - "Send the following tones?\n" - "Sending tones\n" - "Send" - "yes" - "no" - "Replace wild character with" - "Conference call %s" - "Voicemail number" - "Dialling" - "Redialling" - "Conference call" - "Incoming call" - "Incoming work call" - "Call ended" - "On hold" - "Hanging up" - "In call" - "My number is %s" - "Connecting video" - "Video call" - "Requesting video" - "Can\'t connect video call" - "Video request rejected" - "Your callback number\n %1$s" - "Your emergency callback number\n %1$s" - "Dialling" - "Missed call" - "Missed calls" - "%s missed calls" - "Missed call from %s" - "On-going call" - "Ongoing work call" - "Ongoing Wi-Fi call" - "Ongoing Wi-Fi work call" - "On hold" - "Incoming call" - "Incoming work call" - "Incoming Wi-Fi call" - "Incoming Wi-Fi work call" - "Incoming video call" - "Incoming suspected spam call" - "Incoming video request" - "New voicemail" - "New voicemail (%d)" - "Dial %s" - "Voicemail number unknown" - "No service" - "Selected network (%s) unavailable" - "Answer" - "Hang up" - "In-stream video" - "Voice" - "Accept" - "Dismiss" - "Call back" - "Message" - "Ongoing call on another device" - "Transfer call" - "To place a call, first turn off Aeroplane mode." - "Not registered on network." - "Mobile network not available." - "To place a call, enter a valid number." - "Can\'t call." - "Starting MMI sequence…" - "Service not supported." - "Can\'t switch calls." - "Can\'t separate call." - "Can\'t transfer." - "Can\'t conference." - "Can\'t reject call." - "Can\'t release call(s)." - "SIP call" - "Emergency call" - "Turning on radio…" - "No network. Trying again…" - "Can\'t call. %s is not an emergency number." - "Can\'t call. Dial an emergency number." - "Use keyboard to dial" - "Hold Call" - "Resume Call" - "End Call" - "Show dial pad" - "Hide dial pad" - "Mute" - "Unmute" - "Add call" - "Merge calls" - "Swap" - "Manage calls" - "Manage conference call" - "Conference call" - "Manage" - "Audio" - "Video call" - "Change to voice call" - "Switch camera" - "Turn on camera" - "Turn off camera" - "More options" - "Player Started" - "Player Stopped" - "Camera not ready" - "Camera ready" - "Unknown call session event" - "Service" - "Set up" - "<Not set>" - "Other call settings" - "Calling via %s" - "Incoming via %s" - "contact photo" - "go private" - "select contact" - "Write your own..." - "cancel" - "Send" - "Answer" - "Send SMS" - "Decline" - "Answer as video call" - "Answer as audio call" - "Accept video request" - "Decline video request" - "Accept video transmit request" - "Decline video transmit request" - "Accept video receive request" - "Decline video receive request" - "Slide up for %s." - "Slide left for %s." - "Slide right for %s." - "Slide down for %s." - "Vibrate" - "Vibrate" - "Sound" - "Default sound (%1$s)" - "Phone ringtone" - "Vibrate when ringing" - "Ringtone & Vibrate" - "Manage conference call" - "Emergency number" - "Profile photo" - "Camera off" - "via %s" - "Note sent" - "Recent messages" - "Business info" - "%.1f mi away" - "%.1f km away" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "Opens tomorrow at %s" - "Opens today at %s" - "Closes at %s" - "Closed today at %s" - "Open now" - "Closed now" - "Suspected spam caller" - "Call ended %1$s" - "This is the first time that this number has called you." - "We suspected that this call was from a spammer." - "Block/report spam" - "Add contact" - "Not spam" - diff --git a/InCallUI/res/values-es-rUS/strings.xml b/InCallUI/res/values-es-rUS/strings.xml deleted file mode 100644 index 915c90779..000000000 --- a/InCallUI/res/values-es-rUS/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Teléfono" - "En espera" - "Desconocido" - "Número privado" - "Teléfono pago" - "Llamada en conferencia" - "Se interrumpió la llamada" - "Altavoz" - "Auricular del dispositivo" - "Auriculares con cable" - "Bluetooth" - "¿Deseas enviar los siguientes tonos?\n" - "Enviando tonos\n" - "Enviar" - "Sí" - "No" - "Reemplazar el carácter comodín con" - "Llamada en conferencia: %s" - "Número de buzón de voz" - "Marcando" - "Volviendo a marcar" - "Llamada en conferencia" - "Llamada entrante" - "Llamada entrante: trabajo" - "Llamada finalizada" - "En espera" - "Colgando" - "En llamada" - "Mi número es %s" - "Conectando video" - "Videollamada" - "Solicitando video" - "No se puede conectar la videollamada" - "Se rechazó la solicitud de videollamada" - "Número de devolución de llamada\n %1$s" - "Número de devolución de llamada de emergencia\n %1$s" - "Marcando" - "Llamada perdida" - "Llamadas perdidas" - "%s llamadas perdidas" - "Llamada perdida de %s" - "Llamada en curso" - "Llamada en curso: trabajo" - "Llamada Wi-Fi en curso" - "Llamada Wi-Fi en curso: trabajo" - "En espera" - "Llamada entrante" - "Llamada entrante: trabajo" - "Llamada Wi-Fi entrante" - "Llamada Wi-Fi entrante: trabajo" - "Videollamada entrante" - "Posible llamada entrante de spam" - "Solicitud de videollamada entrante" - "Nuevo mensaje de buzón de voz" - "Buzón de voz nuevo (%d)" - "Marcar %s" - "Número de buzón de voz desconocido" - "Sin servicio" - "La red seleccionada (%s) no está disponible" - "Responder" - "Colgar" - "Video" - "Voz" - "Aceptar" - "Descartar" - "Llamar" - "Mensaje" - "Llamada en curso en otro dispositivo" - "Transferir llamada" - "Para realizar una llamada, primero debes desactivar el modo de avión." - "No está registrado en la red." - "La red móvil no está disponible." - "Para realizar una llamada, ingresa un número válido." - "No se puede realizar la llamada." - "Iniciando la secuencia de MMI…" - "El servicio no es compatible." - "No se pueden cambiar las llamadas." - "No se puede desviar la llamada." - "No se puede transferir." - "No se puede realizar la conferencia." - "No se puede rechazar la llamada." - "No se pueden liberar las llamadas." - "Llamada SIP" - "Llamada de emergencia" - "Encendiendo radio…" - "No hay servicio. Vuelve a intentarlo…" - "No se puede realizar la llamada. %s no es un número de emergencia." - "No se puede realizar la llamada. Marca un número de emergencia." - "Usar teclado para marcar" - "Retener llamada" - "Reanudar llamada" - "Finalizar llamada" - "Mostrar teclado" - "Ocultar teclado" - "Silenciar" - "Dejar de silenciar" - "Agregar llamada" - "Combinar llamadas" - "Cambiar" - "Administrar llamadas" - "Administrar conferencia" - "Llamada en conferencia" - "Administrar" - "Audio" - "Video" - "Cambiar a llamada de voz" - "Cambiar cámara" - "Activar la cámara" - "Desactivar la cámara" - "Más opciones" - "Se inició el reproductor" - "Se detuvo el reproductor" - "La cámara no está lista" - "Cámara lista" - "Evento de sesión de llamada desconocido" - "Servicio" - "Configuración" - "<Sin configurar>" - "Otras opciones de llamada" - "Llamada por medio de %s" - "Entrantes por medio de %s" - "foto de contacto" - "pasar a modo privado" - "seleccionar contacto" - "Escribe tu propia respuesta…" - "Cancelar" - "Enviar" - "Responder" - "Enviar SMS" - "Rechazar" - "Responder como videollamada" - "Responder como llamada de audio" - "Aceptar solicitud de videollamada" - "Rechazar solicitud de videollamada" - "Aceptar solicitud de transmisión de videollamada" - "Rechazar solicitud de transmisión de videollamada" - "Aceptar solicitud de recepción de videollamada" - "Rechazar solicitud de recepción de videollamada" - "Desliza el dedo hacia arriba para %s." - "Desliza el dedo hacia la izquierda para %s." - "Desliza el dedo hacia la derecha para %s." - "Desliza el dedo hacia abajo para %s." - "Vibrar" - "Vibrar" - "Sonido" - "Sonido predeterminado (%1$s)" - "Tono del teléfono" - "Vibrar al sonar" - "Tono y vibración" - "Administrar llamada en conferencia" - "Número de emergencia" - "Foto de perfil" - "Cámara desactivada" - "del %s" - "Se envió la nota" - "Mensajes recientes" - "Información de la empresa" - "A %.1f mi" - "A %.1f km" - "%1$s, %2$s" - "De %1$s a %2$s" - "%1$s y %2$s" - "Abre mañana a la hora %s" - "Abre hoy a la hora %s" - "Cierra a la hora %s" - "Cerró hoy a la hora %s" - "Abierto ahora" - "Cerrado ahora" - "Posible spam" - "Llamada finalizada %1$s" - "Es la primera vez que te llaman desde este número." - "Sospechamos que esta llamada era spam." - "Bloquear/denunciar" - "Agregar contacto" - "No es spam" - diff --git a/InCallUI/res/values-es/strings.xml b/InCallUI/res/values-es/strings.xml deleted file mode 100644 index ab570d593..000000000 --- a/InCallUI/res/values-es/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Teléfono" - "En espera" - "Desconocida" - "Número privado" - "Teléfono público" - "Conferencia" - "Llamada perdida" - "Altavoz" - "Auricular" - "Auriculares con cable" - "Bluetooth" - "¿Quieres enviar los siguientes tonos?\n" - "Enviando tonos\n" - "Enviar" - "Sí" - "No" - "Sustituir el carácter comodín por" - "Conferencia %s" - "Número del mensaje de voz" - "Llamando" - "Llamando otra vez" - "Conferencia" - "Llamada entrante" - "Llamada trabajo entrante" - "Llamada finalizada" - "En espera" - "Colgando" - "Llamada entrante" - "Mi número es el %s" - "Conectando videollamada" - "Videollamada" - "Solicitando videollamada" - "No se puede establecer la videollamada" - "Solicitud de videollamada rechazada" - "Tu número de devolución de llamada\n %1$s" - "Tu número de devolución de llamada de emergencia\n %1$s" - "Llamando" - "Llamada perdida" - "Llamadas perdidas" - "%s llamadas perdidas" - "Llamada perdida de %s" - "Llamada en curso" - "Llamada de trabajo en curso" - "Llamada Wi-Fi en curso" - "Llamada Wi-Fi de trabajo en curso" - "En espera" - "Llamada entrante" - "Llamada de trabajo entrante" - "Llamada Wi-Fi entrante" - "Llamada Wi-Fi de trabajo entrante" - "Videollamada entrante" - "Llamada entrante sospechosa de spam" - "Solicitud de videollamada entrante" - "Nuevo mensaje de voz" - "Nuevo mensaje de voz (%d)" - "Marcar %s" - "Número del mensaje de voz desconocido" - "Sin servicio" - "La red seleccionada (%s) no está disponible" - "Responder" - "Colgar" - "Videollamada" - "Voz" - "Aceptar" - "Rechazar" - "Llamar" - "Mensaje" - "Llamada activa en otro dispositivo" - "Transferir llamada" - "Para realizar una llamada, primero debes desactivar el modo avión." - "No estás registrado en la red." - "La red móvil no está disponible." - "Para realizar una llamada, introduce un número válido." - "No se puede establecer la llamada." - "Iniciando secuencia MMI..." - "Servicio no admitido." - "No se pueden intercambiar llamadas." - "No se pueden separar llamadas." - "No se puede transferir." - "No se puede establecer la conferencia." - "No se puede rechazar la llamada." - "No se pueden hacer llamadas." - "Llamada SIP" - "Llamada de emergencia" - "Activando señal móvil…" - "Sin servicio. Reintentado…" - "No se puede establecer la llamada. %s no es un número de emergencia." - "No se puede establecer la llamada. Marca un número de emergencia." - "Usa el teclado para marcar" - "Retener llamada" - "Seguir con la llamada" - "Finalizar llamada" - "Mostrar teclado" - "Ocultar teclado" - "Silenciar" - "Activar sonido" - "Añadir llamada" - "Llamada a tres" - "Cambiar" - "Administrar llamadas" - "Administrar conferencia" - "Teleconferencia" - "Gestionar" - "Audio" - "Videollamada" - "Cambiar a llamada de voz" - "Cambiar cámara" - "Activar cámara" - "Desactivar cámara" - "Más opciones" - "Reproductor iniciado" - "Reproductor detenido" - "Cámara no preparada" - "Cámara preparada" - "Evento de sesión de llamada desconocido" - "Servicio" - "Configuración" - "<No definido>" - "Otra configuración de llamada" - "Llamada a través de %s" - "Recibidas a través de %s" - "foto de contacto" - "llamada privada" - "seleccionar contacto" - "Escribe tu propia respuesta..." - "Cancelar" - "Enviar" - "Responder" - "Enviar SMS" - "Rechazar" - "Responder como videollamada" - "Responder como llamada de audio" - "Aceptar solicitud de videollamada" - "Rechazar solicitud de videollamada" - "Aceptar solicitud de transmisión de videollamada" - "Rechazar solicitud de transmisión de videollamada" - "Aceptar solicitud de recepción de videollamada" - "Rechazar solicitud de recepción de videollamada" - "Desliza el dedo hacia arriba para %s." - "Desliza el dedo hacia la izquierda para %s." - "Desliza el dedo hacia la derecha para %s." - "Desliza el dedo hacia abajo para %s." - "Vibrar" - "Vibrar" - "Sonido" - "Sonido predeterminado (%1$s)" - "Tono de llamada del teléfono" - "Vibrar al sonar" - "Tono de llamada y vibración" - "Administrar videollamada" - "Número de emergencia" - "Foto de perfil" - "Cámara apagada" - "a través de %s" - "Nota enviada" - "Mensajes recientes" - "Información de la empresa" - "A %.1f mi" - "A %.1f km" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "Abre mañana a las %s" - "Abre hoy a las %s" - "Cierra a las %s" - "Cerrado hoy a las %s" - "Abierto ahora" - "Cerrado ahora" - "Sospechoso de spam" - "Llamada de %1$s terminada" - "Es la primera vez que recibes una llamada de este número." - "Sospechábamos que esta llamada era de spam." - "Bloquear / Marcar como spam" - "Añadir contacto" - "No es spam" - diff --git a/InCallUI/res/values-et/strings.xml b/InCallUI/res/values-et/strings.xml deleted file mode 100644 index 5d2a0d8b0..000000000 --- a/InCallUI/res/values-et/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefon" - "Ootel" - "Tundmatu" - "Eranumber" - "Telefoniautomaat" - "Konverentskõne" - "Kõne katkes" - "Kõlar" - "Käsitelefoni kuular" - "Juhtmega peakomplekt" - "Bluetooth" - "Kas saata järgmised toonid?\n" - "Toonide saatmine\n" - "Saada" - "Jah" - "Ei" - "Asenda metamärk üksusega" - "Konverentskõne %s" - "Kõneposti number" - "Valimine" - "Uuesti valimine" - "Konverentskõne" - "Sissetulev kõne" - "Sissetulev töökõne" - "Kõne lõpetati" - "Ootel" - "Lõpetamine" - "Kõne on pooleli" - "Minu number on %s" - "Video ühendamine" - "Videokõne" - "Video taotlemine" - "Videokõnet ei õnnestu ühendada" - "Videokõne taotlus lükati tagasi" - "Teie tagasihelistamise number\n%1$s" - "Teie hädaabikõne tagasihelistamise number\n%1$s" - "Valimine" - "Vastamata kõne" - "Vastamata kõned" - "%s vastamata kõnet" - "Vastamata kõne helistajalt %s" - "Käimasolev kõne" - "Käimasolev töökõne" - "Käimasolev WiFi-kõne" - "Käimasolev töökõne WiFi kaudu" - "Ootel" - "Sissetulev kõne" - "Sissetulev töökõne" - "Sissetulev WiFi-kõne" - "Sissetulev töökõne WiFi kaudu" - "Sissetulev videokõne" - "Arvatav sissetulev rämpskõne" - "Sissetulev videokõne taotlus" - "Uus kõnepostisõnum" - "Uus kõnepostisõnum (%d)" - "Valige %s" - "Kõnepostinumber on tundmatu" - "Levi puudub" - "Valitud võrk (%s) pole saadaval" - "Vasta" - "Lõpeta kõne" - "Videokõne" - "Häälkõne" - "Nõustu" - "Loobu" - "Helista tagasi" - "Saada sõnum" - "Pooleliolev kõne teise seadmes" - "Kõne ülekandmine" - "Helistamiseks lülitage esmalt lennukirežiim välja." - "Ei ole võrgus registreeritud." - "Mobiilsidevõrk pole saadaval." - "Helistamiseks sisestage kehtiv number." - "Ei saa helistada." - "MMI-jada alustamine …" - "Teenust ei toetata." - "Kõnesid ei saa vahetada." - "Kõnet ei saa eraldada." - "Ei saa üle kanda." - "Konverentskõnet ei saa pidada." - "Kõnet ei saa tagasi lükata." - "Kõnesid ei saa vabastada." - "SIP-kõne" - "Hädaabikõne" - "Raadioside sisselülitamine …" - "Levi puudub. Uuesti proovimine …" - "Ei saa helistada. %s ei ole hädaabinumber." - "Ei saa helistada. Valige hädaabinumber." - "Kasutage valimiseks klaviatuuri" - "Kõne ootele" - "Jätka kõnet" - "Lõpeta kõne" - "Kuva valimisklahvistik" - "Peida valimisklahvistik" - "Vaigista" - "Tühista vaigistus" - "Lisa kõne" - "Ühenda kõned" - "Vaheta" - "Halda kõnesid" - "Halda konverentskõnet" - "Konverentskõne" - "Halda" - "Heli" - "Videokõne" - "Mine üle häälkõnele" - "Vaheta kaamerat" - "Lülita kaamera sisse" - "Lülita kaamera välja" - "Rohkem valikuid" - "Pleier käivitati" - "Pleier peatati" - "Kaamera pole valmis" - "Kaamera on valmis" - "Tundmatu kõneseansisündmus" - "Teenus" - "Seadistamine" - "<Määramata>" - "Muud kõneseaded" - "Kõne edastab %s" - "Sissetulev kõne teenusepakkuja %s kaudu" - "kontakti foto" - "aktiveeri privaatne kõne" - "vali kontakt" - "Kirjutage ise …" - "Tühista" - "Saada" - "Vastamine" - "Saada SMS" - "Tagasilükkamine" - "Vastamine videokõnena" - "Vastamine helikõnena" - "Video taotluse aktsepteerimine" - "Video taotluse tagasilükkamine" - "Video edastamise taotluse aktsepteerimine" - "Video edastamise taotluse tagasilükkamine" - "Video vastuvõtmise taotluse aktsepteerimine" - "Video vastuvõtmise taotluse tagasilükkamine" - "Lohistage üles: %s." - "Lohistage vasakule: %s." - "Lohistage paremale: %s." - "Lohistage alla: %s." - "Vibreerimine" - "Vibreerimine" - "Heli" - "Vaikeheli (%1$s)" - "Telefonihelin" - "Vibreerimine helina ajal" - "Helin ja vibratsioon" - "Konverentskõne haldamine" - "Hädaabinumber" - "Profiilifoto" - "Kaamera on välja lülitatud" - "numbri %s kaudu" - "Märkus on saadetud" - "Hiljutised sõnumid" - "Ettevõtte teave" - "%.1f miili kaugusel" - "%.1f km kaugusel" - "%1$s, %2$s" - "%1$s kuni %2$s" - "%1$s, %2$s" - "Avatakse homme kell %s" - "Avatakse täna kell %s" - "Suletakse kell %s" - "Suleti täna kell %s" - "Praegu avatud" - "Praegu suletud" - "Arvatav rämpskõne" - "Kõne lõppes: %1$s" - "Teile helistati sellelt numbrilt esimest korda." - "Kahtlustasime, et see võis olla rämpskõne." - "Blokeeri / teavita rämpskõnest" - "Lisa kontakt" - "Pole rämpskõne" - diff --git a/InCallUI/res/values-eu/strings.xml b/InCallUI/res/values-eu/strings.xml deleted file mode 100644 index c300c47cd..000000000 --- a/InCallUI/res/values-eu/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefonoa" - "Zain" - "Ezezaguna" - "Zenbaki pribatua" - "Telefono publikoa" - "Konferentzia-deia" - "Eten egin da deia" - "Bozgorailua" - "Aurikularrak" - "Kabledun entzungailua" - "Bluetooth konexioa" - "Tonu hauek bidali nahi dituzu?\n" - "Tonuak bidaltzen\n" - "Bidali" - "Bai" - "Ez" - "Ordeztu komodina honekin:" - "Konferentzia-deiaren ordua: %s" - "Erantzungailuaren zenbakia" - "Deitzen" - "Berriro markatzen" - "Konferentzia-deia" - "Dei bat jaso duzu" - "Laneko dei bat jaso duzu" - "Amaitu da deia" - "Zain" - "Deia amaitzen" - "Deia abian" - "Nire zenbakia %s da" - "Bideoa konektatzen" - "Bideo-deia" - "Bideo-deia eskatzen" - "Ezin da konektatu bideo-deia" - "Baztertu egin da bideo-deia egiteko eskaera" - "Dei-erantzunetarako zenbakia:\n %1$s" - "Larrialdi-dei bidez erantzuteko zenbakia:\n %1$s" - "Deitzen" - "Dei bat galdu duzu" - "Dei batzuk galdu dituzu" - "%s dei galdu dituzu" - "Deitzaile honen dei bat galdu duzu: %s" - "Deia abian da" - "Laneko dei bat abian da" - "Wi-Fi bidezko deia abian da" - "Wi-Fi bidezko laneko dei bat abian da" - "Zain" - "Dei bat jaso duzu" - "Laneko dei bat jaso duzu" - "Wi-Fi bidezko dei bat jaso duzu" - "Wi-Fi bidezko laneko dei bat jaso duzu" - "Bideo-dei bat jaso duzu" - "Ustezko spam-deia jaso duzu" - "Bideo-dei bat egiteko eskaera bat jaso duzu" - "Ahots-mezu berria" - "Ahots-mezu berriak (%d)" - "Markatu %s" - "Erantzungailuaren zenbakia ezezaguna da" - "Ez dago zerbitzurik" - "Hautatutako sarea (%s) ez dago erabilgarri" - "Erantzun" - "Amaitu deia" - "Bideo-deia" - "Ahots-deia" - "Onartu" - "Baztertu" - "Itzuli deia" - "Bidali SMSa" - "Dei bat abian da beste gailu batean" - "Transferitu deia" - "Deitzeko, desaktibatu hegaldi modua." - "Ez dago sarean erregistratuta." - "Sare mugikorra ez dago erabilgarri." - "Deitzeko, idatzi balio duen zenbaki bat." - "Ezin da deitu." - "MMI sekuentzia hasten…" - "Ez da onartzen zerbitzua." - "Ezin aldatu beste dei batera." - "Ezin da bereizi deia." - "Ezin da transferitu." - "Ezin da egin konferentzia-deia." - "Ezin da baztertu deia." - "Ezin dira amaitu deiak." - "SIP deia" - "Larrialdi-deia" - "Irratia pizten…" - "Ez dago zerbitzurik. Berriro saiatzen…" - "Ezin da deitu. %s ez da larrialdietarako zenbakia." - "Ezin da deitu. Markatu larrialdietarako zenbakia." - "Erabili teklatua markatzeko" - "Utzi deia zain" - "Berrekin deiari" - "Amaitu deia" - "Erakutsi markagailua" - "Ezkutatu markagailua" - "Desaktibatu audioa" - "Aktibatu audioa" - "Gehitu deia" - "Bateratu deiak" - "Aldatu" - "Kudeatu deiak" - "Kudeatu konferentzia-deia" - "Konferentzia-deia" - "Kudeatu" - "Audioa" - "Bideo-deia" - "Aldatu ahots-deira" - "Aldatu kamera" - "Aktibatu kamera" - "Desaktibatu kamera" - "Aukera gehiago" - "Abian da erreproduzigailua" - "Gelditu da erreproduzigailua" - "Ez dago prest kamera" - "Prest dago kamera" - "Dei-saioko gertaera ezezaguna" - "Zerbitzua" - "Konfigurazioa" - "<Ezarri gabe>" - "Deien beste ezarpen batzuk" - "%s bidez deitzen" - "%s bidez jasotzen" - "kontaktuaren argazkia" - "bihurtu pribatu" - "hautatu kontaktua" - "Idatzi zeure erantzuna…" - "Utzi" - "Bidali" - "Erantzun" - "Bidali SMS mezua" - "Baztertu" - "Erantzun bideo-dei moduan" - "Erantzun audio-dei moduan" - "Onartu bideo-deia egiteko eskaera" - "Baztertu bideo-deia egiteko eskaera" - "Onartu bideoa transmititzeko eskaera" - "Baztertu bideoa transmititzeko eskaera" - "Onartu bideo-deia jasotzeko eskaera" - "Baztertu bideo-deia jasotzeko eskaera" - "Lerratu gora hau egiteko: %s." - "Lerratu ezkerrera hau egiteko: %s." - "Lerratu eskuinera hau egiteko: %s." - "Lerratu behera hau egiteko: %s." - "Dardara" - "Dardara" - "Soinua" - "Soinu lehenetsia (%1$s)" - "Telefonoaren tonua" - "Egin dar-dar tonuak jotzean" - "Tonua eta dardara" - "Kudeatu konferentzia-deia" - "Larrialdietarako zenbakia" - "Profileko argazkia" - "Desaktibatuta dago kamera" - "%s zenbakitik" - "Bidali da oharra" - "Azken mezuak" - "Enpresaren informazioa" - "Hemendik %.1f miliara" - "Hemendik %.1f km-ra" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "%s da biharko irekitze-ordua" - "%s da gaurko irekitze-ordua" - "%s da ixte-ordua" - "%s da gaurko itxiera-ordua" - "Irekita dago" - "Itxita dago" - "Ustezko spam-deitzailea" - "Deiaren amaiera: %1$s" - "Zenbaki honek deitu dizun lehen aldia izan da." - "Spam-igorle baten deia izan dela susmatu dugu." - "Salatu spama dela" - "Gehitu kontaktua" - "Ez da spama" - diff --git a/InCallUI/res/values-fa/strings.xml b/InCallUI/res/values-fa/strings.xml deleted file mode 100644 index f96b8956a..000000000 --- a/InCallUI/res/values-fa/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "تلفن" - "در انتظار" - "نامشخص" - "شماره خصوصی" - "تلفن عمومی" - "تماس کنفرانسی" - "تماس قطع شد" - "بلندگو" - "گوشی" - "هدست سیمی" - "بلوتوث" - "شماره‌های بعدی ارسال شود؟\n" - "تون‌های ارسالی\n" - "ارسال" - "بله" - "نه" - "جایگزینی نویسه عمومی با" - "تماس کنفرانسی %s" - "شماره پست صوتی" - "شماره‌گیری" - "درحال شماره‌گیری مجدد" - "تماس کنفرانسی" - "تماس ورودی" - "تماس کاری ورودی" - "تماس پایان یافت" - "در انتظار" - "قطع تماس" - "درحال تماس" - "شماره من %s است" - "درحال برقراری تماس ویدئویی" - "تماس ویدئویی" - "درحال درخواست تماس ویدئویی" - "برقراری تماس ویدئویی ممکن نیست" - "درخواست تماس ویدئویی رد شد" - "شماره پاسخ تماس شما\n %1$s" - "شماره پاسخ تماس اضطراری شما\n %1$s" - "شماره‌گیری" - "تماس بی‌پاسخ" - "تماس بی‌پاسخ" - "%s تماس بی‌پاسخ" - "تماس بی‌پاسخ از %s" - "تماس درحال انجام" - "تماس کاری درحال انجام" - "‏تماس درحال انجام ازطریق Wi-Fi" - "‏تماس کاری Wi-Fi درحال انجام" - "در انتظار" - "تماس ورودی" - "تماس کاری ورودی" - "‏تماس Wi-Fi ورودی" - "‏تماس کاری Wi-Fi ورودی" - "تماس ویدئویی ورودی" - "تماس هرزنامه احتمالی ورودی" - "درخواست تماس ویدئویی ورودی" - "پست صوتی جدید" - "پست صوتی جدید (%d)" - "شماره‌گیری %s" - "شماره پست صوتی ناشناس" - "بدون سرویس" - "شبکه انتخابی (%s) قابل دسترس نیست" - "پاسخ" - "پایان تماس" - "ویدئو" - "صدا" - "پذیرفتن" - "نپذیرفتن" - "پاسخ تماس" - "پیام" - "تماس در حال انجام در دستگاهی دیگر" - "انتقال تماس" - "برای برقراری تماس، ابتدا حالت هواپیما را خاموش کنید." - "در شبکه ثبت نشده است." - "شبکه تلفن همراه در دسترس نیست." - "برای برقراری تماس، شماره معتبری وارد کنید." - "تماس ممکن نیست." - "‏شروع ترتیب MMI…" - "سرویس پشتیبانی نمی‌شود." - "جابه‌جایی بین تماس‌ها ممکن نیست." - "جدا کردن تماس ممکن نیست." - "انتقال ممکن نیست." - "تماس کنفرانسی ممکن نیست." - "رد کردن تماس ممکن نیست." - "آزاد کردن تماس(ها) ممکن نیست." - "‏تماس SIP" - "تماس اضطراری" - "درحال روشن کردن رادیو…‏" - "سرویسی در دسترس نیست. درحال تلاش مجدد…‏" - "تماس ممکن نیست. %s شماره اضطراری نیست." - "تماس ممکن نیست. فقط شماره اضطراری." - "استفاده از صفحه‌کلید برای شماره‌گیری" - "در انتظار گذاشتن تماس" - "ازسرگیری تماس" - "پایان تماس" - "نمایش صفحه شماره‌گیری" - "پنهان کردن صفحه شماره‌گیری" - "بی‌صدا کردن" - "لغو نادیده گرفتن" - "افزودن تماس" - "ادغام تماس‌ها" - "تعویض" - "مدیریت تماس‌ها" - "مدیریت تماس کنفرانسی" - "تماس کنفرانسی" - "مدیریت" - "صوتی" - "تماس ویدئویی" - "تغییر به تماس صوتی" - "تعویض دوربین" - "روشن کردن دوربین" - "خاموش کردن دوربین" - "گزینه‌های بیشتر" - "پخش‌کننده راه‌اندازی شد" - "پخش‌کننده متوقف شد" - "دوربین آماده نیست" - "دوربین آماده است" - "رویداد جلسه تماس ناشناس" - "سرویس" - "راه‌اندازی" - "‏<تنظیم نشده>" - "سایر تنظیمات تماس" - "تماس با %s" - "تماس‌های ورودی ازطریق %s" - "عکس مخاطب" - "رفتن به حالت خصوصی" - "انتخاب مخاطب" - "پیام خودتان را بنویسید..." - "لغو" - "ارسال" - "پاسخ" - "ارسال پیامک" - "رد کردن" - "پاسخ به‌صورت تماس ویدئویی" - "پاسخ به‌صورت تماس صوتی" - "پذیرفتن درخواست تماس ویدئویی" - "نپذیرفتن درخواست تماس ویدئویی" - "پذیرفتن درخواست مخابره ویدئویی" - "نپذیرفتن درخواست مخابره ویدئویی" - "پذیرفتن درخواست دریافت ویدئویی" - "نپذیرفتن درخواست دریافت ویدئویی" - "برای %s به بالا بلغزانید." - "برای %s به چپ بلغزانید." - "برای %s به راست بلغزانید." - "برای %s به پایین بلغزانید." - "لرزش" - "لرزش" - "صدا" - "صدای پیش‌فرض (%1$s)" - "آهنگ زنگ تلفن" - "لرزش هنگام زنگ زدن" - "آهنگ‌ زنگ و لرزش" - "مدیریت تماس کنفرانسی" - "شماره اضطراری" - "عکس نمایه" - "دوربین خاموش" - "ازطریق %s" - "یادداشت ارسال شد" - "پیام‌های جدید" - "اطلاعات کسب و کار" - "%.1f مایل فاصله" - "%.1f کیلومتر فاصله" - "%1$s،‏ %2$s" - "%1$s تا %2$s" - "%1$s،‏ %2$s" - "فردا ساعت %s باز می‌شود" - "امروز ساعت %s باز می‌شود" - "ساعت %s بسته می‌شود" - "امروز ساعت %s بسته شد" - "اکنون باز است" - "اکنون بسته است" - "تماس‌گیرنده هرزنامه احتمالی" - "‏تماس به پایان رسید %1$s" - "این اولین بار است که این شماره با شما تماس گرفته است." - "ما به این تماس شک کردیم و احساس کردیم که ممکن است کلاهبردار باشد." - "مسدود کردن/گزارش دادن هرزنامه" - "افزودن مخاطب" - "هرزنامه نیست" - diff --git a/InCallUI/res/values-fi/strings.xml b/InCallUI/res/values-fi/strings.xml deleted file mode 100644 index 7553e7c25..000000000 --- a/InCallUI/res/values-fi/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Puhelin" - "Pidossa" - "Tuntematon" - "Yksityinen numero" - "Maksupuhelin" - "Puhelinneuvottelu" - "Puhelu katkaistiin." - "Kaiutin" - "Puhelimen kaiutin" - "Kuulokemikrofoni" - "Bluetooth" - "Lähetetäänkö seuraavat äänet?\n" - "Lähetetään ääniä\n" - "Lähetä" - "Kyllä" - "Ei" - "Muuta jokerimerkiksi" - "Puhelinneuvottelu %s" - "Puhelinvastaajan numero" - "Soitetaan" - "Soitetaan uudelleen" - "Puhelinneuvottelu" - "Saapuva puhelu" - "Saapuva työpuhelu" - "Puhelu päättyi" - "Pidossa" - "Katkaistaan" - "Puhelu käynnissä" - "Numeroni on %s" - "Avataan videoyhteys" - "Videopuhelu" - "Videota pyydetään" - "Videopuhelua ei voi soittaa" - "Videopyyntö hylättiin" - "Takaisinsoittonumero:\n %1$s" - "Hätäpuhelujen takaisinsoittonumero:\n %1$s" - "Soitetaan" - "Vastaamaton puhelu" - "Vastaamattomia puheluita" - "%s vastaamatonta puhelua" - "Vastaamaton puhelu: %s" - "Käynnissä oleva puhelu" - "Käynnissä oleva työpuhelu" - "Käynnissä oleva Wi-Fi-puhelu" - "Käynnissä oleva Wi-Fi-työpuhelu" - "Pidossa" - "Saapuva puhelu" - "Saapuva työpuhelu" - "Saapuva Wi-Fi-puhelu" - "Saapuva Wi-Fi-työpuhelu" - "Saapuva videopuhelu" - "Tämä puhelu saattaa olla häirikköpuhelu." - "Saapuva videopyyntö" - "Uusi vastaajaviesti" - "Uusia vastaajaviestejä (%d)" - "Soita: %s" - "Puhelinvastaajan numero on tuntematon." - "Ei yhteyttä" - "Valittu verkko (%s) ei ole käytettävissä." - "Vastaa" - "Katkaise" - "Videopuhelu" - "Äänipuhelu" - "Hyväksy" - "Hylkää" - "Soita" - "Viesti" - "Puhelu on kesken toisella laitteella." - "Siirrä puhelu" - "Poista lentokonetila käytöstä ennen puhelun soittamista." - "Ei rekisteröity verkkoon" - "Matkapuhelinverkko ei ole käytettävissä." - "Soita antamalla kelvollinen numero." - "Puhelua ei voi soittaa." - "Aloitetaan MMI-koodisekvenssiä…" - "Yhteyttä ei tueta." - "Puhelua ei voi vaihtaa." - "Puhelua ei voi erottaa." - "Puhelua ei voi siirtää." - "Puheluja ei voi yhdistää." - "Puhelua ei voi hylätä." - "Puheluja ei voi katkaista." - "SIP-puhelu" - "Hätäpuhelu" - "Käynnistetään radiota…" - "Ei yhteyttä. Yritetään uudelleen…" - "Puhelua ei voi soittaa. %s ei ole hätänumero." - "Puhelua ei voi soittaa. Valitse hätänumero." - "Valitse numero näppäimistöllä." - "Aseta puhelu pitoon" - "Jatka puhelua" - "Lopeta puhelu" - "Avaa näppäimistö" - "Piilota näppäimistö" - "Mykistä" - "Poista mykistys" - "Lisää puhelu" - "Yhdistä puhelut" - "Vaihda" - "Hallinnoi puheluja" - "Hallinnoi puhelinneuvottelua" - "Puhelinneuvottelu" - "Hallinnoi" - "Ääni" - "Video" - "Muuta äänipuheluksi" - "Vaihda kameraa" - "Käynnistä kamera" - "Sammuta kamera" - "Lisäasetukset" - "Soitin käynnistettiin." - "Soitin pysäytettiin." - "Kamera ei ole valmis." - "Kamera on valmis." - "Tuntematon puheluistunnon tapahtuma" - "Palveluntarjoaja" - "Määritys" - "<Ei määritetty>" - "Muut puheluasetukset" - "Käytetään operaattoria %s" - "Saapuva puhelu (%s)" - "Yhteystiedon kuva" - "Muuta yksityiseksi." - "Valitse yhteystieto." - "Kirjoita oma…" - "Peruuta" - "Lähetä" - "Vastaa." - "Lähetä tekstiviesti." - "Hylkää." - "Vastaa ja aloita videopuhelu." - "Vastaa ja aloita äänipuhelu." - "Hyväksy videopyyntö." - "Hylkää videopyyntö." - "Hyväksy videon lähetyspyyntö." - "Hylkää videon lähetyspyyntö." - "Hyväksy videon vastaanottopyyntö." - "Hylkää videon vastaanottopyyntö." - "Valitse %s liu\'uttamalla ylös." - "Valitse %s liu\'uttamalla vasemmalle." - "Valitse %s liu\'uttamalla oikealle." - "Valitse %s liu\'uttamalla alas." - "Värinä" - "Värinä" - "Ääni" - "Oletusääni (%1$s)" - "Puhelimen soittoääni" - "Käytä värinää, kun puhelin soi" - "Soittoääni ja värinä" - "Hallinnoi puhelinneuvottelua" - "Hätänumero" - "Profiilikuva" - "Kamera on pois käytöstä." - "nron %s kautta" - "Muistiinpano lähetettiin." - "Viimeisimmät viestit" - "Yrityksen tiedot" - "Etäisyys: %.1f mailia" - "Etäisyys: %.1f kilometriä" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "Avataan huomenna kello %s" - "Avataan tänään kello %s" - "Suljetaan tänään kello %s" - "Suljettiin tänään kello %s" - "Avoinna nyt" - "Suljettu nyt" - "Häirikkösoittaja" - "Puhelu loppui %1$s" - "Tämä oli ensimmäinen kerta, kun tästä numerosta soitettiin sinulle." - "Epäilemme, että tämä puhelu tuli häirikkösoittajalta." - "Estä / ilmoita" - "Lisää yhteystieto" - "Ei häirikkösoittaja" - diff --git a/InCallUI/res/values-fr-rCA/strings.xml b/InCallUI/res/values-fr-rCA/strings.xml deleted file mode 100644 index 2980646e8..000000000 --- a/InCallUI/res/values-fr-rCA/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Téléphone" - "En attente" - "Inconnue" - "Numéro privé" - "Cabine téléphonique" - "Conférence téléphonique" - "L\'appel a été interrompu" - "Haut-parleur" - "Écouteur du combiné" - "Écouteurs à fil" - "Bluetooth" - "Envoyer les tonalités suivantes?\n" - "Envoi des tonalités\n" - "Envoyer" - "Oui" - "Non" - "Remplacer le caractère générique par" - "Conférence téléphonique %s" - "Numéro de messagerie vocale" - "Composition..." - "Recomposition en cours..." - "Conférence téléphonique" - "Appel entrant" - "Appel entrant - travail" - "Appel terminé" - "En attente" - "Fin de l\'appel" - "En cours d\'appel" - "Mon numéro est le %s" - "Connexion de la vidéo en cours…" - "Appel vidéo" - "Demande de vidéo en cours" - "Impossible de connecter l\'appel vidéo" - "Demande vidéo refusée" - "Votre numéro de rappel :\n %1$s" - "Votre numéro de rappel d\'urgence :\n %1$s" - "Composition en cours..." - "Appel manqué" - "Appels manqués" - "%s appels manqués" - "Appel manqué de %s" - "Appel en cours" - "Appel en cours - travail" - "Appel Wi-Fi en cours" - "Appel Wi-Fi en cours - travail" - "En attente" - "Appel entrant" - "Appel entrant - travail" - "Appel Wi-Fi entrant" - "Appel Wi-Fi entrant - travail" - "Appel vidéo entrant" - "L\'appel entrant est suspect" - "Demande de vidéo reçue" - "Nouveau message vocal" - "Nouveaux messages vocaux (%d)" - "Composer le %s" - "Numéro de messagerie vocale inconnu" - "Aucun service" - "Réseau sélectionné (%s) non disponible" - "Répondre" - "Raccrocher" - "Vidéo" - "Voix" - "Accepter" - "Fermer" - "Rappeler" - "Message" - "Appel en cours sur un autre appareil" - "Transférer l\'appel" - "Pour faire un appel, d\'abord désactiver le mode Avion." - "Non enregistré sur le réseau." - "Réseau cellulaire non disponible." - "Pour faire un appel, entrez un numéro valide." - "Impossible d\'appeler." - "Lancement de la séquence IHM en cours…" - "Service non pris en charge." - "Impossible de faire des appels." - "Impossible de séparer les appels." - "Impossible de transférer." - "Impossible de créer la conférence." - "Impossible de refuser l\'appel." - "Impossible de libérer l\'appel ou les appels." - "Appel SIP" - "Appel d\'urgence" - "Activation du signal radio…" - "Aucun service. Nouvel essai en cours..." - "Appel impossible. %s n\'est pas un numéro d\'urgence." - "Appel impossible. Composez un numéro d\'urgence." - "Utilisez le clavier pour composer un numéro" - "Mettre l\'appel en attente" - "Reprendre l\'appel" - "Mettre fin à l\'appel" - "Afficher le clavier numérique" - "Masquer le clavier numérique" - "Désactiver le son" - "Réactiver le son" - "Ajouter un appel" - "Fusionner les appels" - "Permuter" - "Gérer les appels" - "Gérer la conférence" - "Conférence téléphonique" - "Gérer" - "Audio" - "Appel vidéo" - "Passer à un appel vocal" - "Changer d\'appareil photo" - "Activer la caméra" - "Désactiver la caméra" - "Plus d\'options" - "Le lecteur a démarré" - "Le lecteur a arrêté" - "L\'appareil photo n\'est pas prêt" - "L\'appareil photo est prêt" - "Événement inconnu de séance d\'appel" - "Service" - "Configuration" - "<Non défini>" - "Autres paramètres d\'appel" - "Appel par %s" - "Appel entrant par %s" - "photo du contact" - "mode privé" - "sélectionner un contact" - "Réponse personnalisée..." - "Annuler" - "Envoyer" - "Répondre" - "Envoyer un texto" - "Refuser" - "Répondre comme appel vidéo" - "Répondre comme appel audio" - "Accepter la demande vidéo" - "Refuser la demande vidéo" - "Accepter la demande de transmission vidéo" - "Refuser la demande de transmission vidéo" - "Accepter la demande de réception vidéo" - "Refuser la demande de réception vidéo" - "Faites glisser votre doigt vers le haut pour %s." - "Faites glisser votre doigt vers la gauche pour %s." - "Faites glisser votre doigt vers la droite pour %s." - "Faire glisser le doigt vers le bas : %s" - "Vibration" - "Vibration" - "Son" - "Son par défaut (%1$s)" - "Sonnerie du téléphone" - "Vibrer lorsque téléphone sonne" - "Sonnerie et vibreur" - "Gérer la conférence" - "Numéro d\'urgence" - "Photo de profil" - "Appareil photo désactivé" - "au moyen du %s" - "Note envoyée" - "Messages récents" - "Renseignements sur l\'entreprise" - %.1f mi" - %.1f km" - "%1$s, %2$s" - "De %1$s à %2$s" - "%1$s, %2$s" - "Ouvre demain à %s" - "Ouvre aujourd\'hui à %s" - "Ferme à %s" - "A fermé aujourd\'hui à %s" - "Ouvert" - "Fermé" - "Appel suspect" - "Appel terminé %1$s" - "C\'est la première fois que ce numéro vous appelle." - "Cet appel nous semblait suspect." - "Sign. appel suspect" - "Ajouter un contact" - "N\'est pas suspect" - diff --git a/InCallUI/res/values-fr/strings.xml b/InCallUI/res/values-fr/strings.xml deleted file mode 100644 index 521fedb64..000000000 --- a/InCallUI/res/values-fr/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Téléphone" - "En attente" - "Inconnu" - "Numéro privé" - "Cabine téléphonique" - "Conférence téléphonique" - "Appel interrompu" - "Haut-parleur" - "Écouteur du combiné" - "Casque filaire" - "Bluetooth" - "Envoyer les tonalités suivantes ?\n" - "Envoi des tonalités…\n" - "Envoyer" - "Oui" - "Non" - "Remplacer le caractère générique par" - "Conférence téléphonique à %s" - "N° de la messagerie vocale" - "Appel…" - "Rappel…" - "Conférence téléphonique" - "Appel entrant" - "Appel profession. entrant" - "Appel terminé" - "En attente" - "Fin de l\'appel…" - "Appel en cours" - "Mon numéro est le %s" - "Connexion de la vidéo…" - "Appel vidéo" - "Demande de vidéo…" - "Impossible d\'établir la connexion de l\'appel vidéo." - "Demande d\'appel vidéo refusée" - "Votre numéro de rappel\n %1$s" - "Votre numéro de rappel d\'urgence\n %1$s" - "Appel…" - "Appel manqué" - "Appels manqués" - "%s appels manqués" - "Appel manqué de %s" - "Appel en cours" - "Appel professionnel en cours" - "Appel Wi-Fi en cours" - "Appel Wi-Fi professionnel en cours" - "En attente" - "Appel entrant" - "Appel professionnel entrant" - "Appel Wi-Fi entrant" - "Appel Wi-Fi professionnel entrant" - "Appel vidéo entrant" - "Appel entrant indésirable suspecté" - "Demande de vidéo reçue" - "Nouveau message vocal" - "Nouveaux messages vocaux (%d)" - "Composer le %s" - "Numéro de messagerie vocale inconnu" - "Aucun service" - "Réseau sélectionné (%s) non disponible" - "Répondre" - "Raccrocher" - "Vidéo" - "Appel vocal" - "Accepter" - "Fermer" - "Rappeler" - "Envoyer SMS" - "Appel en cours sur un autre appareil" - "Transférer l\'appel" - "Veuillez désactiver le mode Avion avant de passer un appel." - "Non enregistré sur le réseau." - "Réseau mobile indisponible." - "Pour émettre un appel, veuillez saisir un numéro valide." - "Impossible d\'émettre l\'appel." - "Lancement de la séquence IHM…" - "Service non compatible." - "Impossible de changer d\'appel." - "Impossible d\'isoler l\'appel." - "Transfert impossible." - "Impossible de lancer une conférence téléphonique." - "Impossible de refuser l\'appel." - "Impossible de lancer les appels." - "Appel SIP" - "Appel d\'urgence" - "Activation du signal radio…" - "Aucun service disponible. Nouvelle tentative…" - "Impossible d\'émettre l\'appel. %s n\'est pas un numéro d\'urgence." - "Impossible d\'émettre l\'appel. Veuillez composer un numéro d\'urgence." - "Utilisez le clavier pour composer un numéro." - "Mettre l\'appel en attente" - "Reprendre l\'appel" - "Mettre fin à l\'appel" - "Afficher le clavier" - "Masquer le clavier" - "Couper le son" - "Réactiver le son" - "Ajouter un appel" - "Fusionner les appels" - "Permuter" - "Gérer les appels" - "Gérer conférence téléphonique" - "Conférence téléphonique" - "Gérer" - "Audio" - "Appel vidéo" - "Passer à un appel vocal" - "Changer de caméra" - "Activer la caméra" - "Désactiver la caméra" - "Plus d\'options" - "Le lecteur a démarré." - "Le lecteur s\'est arrêté." - "La caméra n\'est pas prête" - "La caméra est prête" - "Événement de session d\'appel inconnu" - "Service" - "Configuration" - "<Non défini>" - "Autres paramètres d\'appel" - "Appel via %s" - "Appel entrant via %s" - "photo du contact" - "mode privé" - "sélectionner un contact" - "Réponse personnalisée…" - "Annuler" - "Envoyer" - "Répondre" - "Envoyer un SMS" - "Refuser" - "Répondre via un appel vidéo" - "Répondre via un appel audio" - "Accepter la demande d\'appel vidéo" - "Refuser la demande d\'appel vidéo" - "Accepter la demande de transmission d\'appel vidéo" - "Refuser la demande de transmission d\'appel vidéo" - "Accepter la demande de réception d\'appel vidéo" - "Refuser la demande de réception d\'appel vidéo" - "Faites glisser vers le haut pour %s" - "Faites glisser vers la gauche pour %s." - "Faites glisser vers la droite pour %s." - "Faites glisser vers le bas pour %s." - "Vibreur" - "Vibreur" - "Sonnerie" - "Sonnerie par défaut (%1$s)" - "Sonnerie du téléphone" - "Vibreur lorsque le tél. sonne" - "Sonnerie et vibreur" - "Gérer la conférence téléphonique" - "Numéro d\'urgence" - "Photo du profil" - "Caméra désactivée" - "via le %s" - "La note a bien été envoyée." - "Messages récents" - "Informations sur l\'établissement" - %.1f mi" - %.1f km" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "Ouvre demain à %s" - "Ouvre aujourd\'hui à %s" - "Ferme à %s" - "Fermé aujourd\'hui à %s" - "Ouvert" - "Fermé" - "Appel indésirable suspecté" - "Appel terminé %1$s" - "C\'est la première fois que vous recevez un appel de ce numéro." - "Nous suspectons cet appel de provenir d\'un spammeur." - "Bloquer/Signaler spam" - "Ajouter un contact" - "Numéro fiable" - diff --git a/InCallUI/res/values-gl/strings.xml b/InCallUI/res/values-gl/strings.xml deleted file mode 100644 index 8968946e6..000000000 --- a/InCallUI/res/values-gl/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Teléfono" - "En espera" - "Descoñecido" - "Número privado" - "Teléfono público" - "Conferencia telefónica" - "Chamada interrompida" - "Altofalante" - "Auricular do teléfono" - "Auriculares con cable" - "Bluetooth" - "Queres enviar os seguintes tons?\n" - "Enviando tons\n" - "Enviar" - "Si" - "Non" - "Substituír carácter comodín por" - "Conferencia telefónica ás %s" - "Número de correo de voz" - "Marcando" - "Marcando de novo" - "Conferencia telefónica" - "Chamada entrante" - "Chamada traballo entrante" - "Chamada finalizada" - "En espera" - "Desconectando" - "Chamada entrante" - "O meu número é o %s" - "Conectando vídeo" - "Videochamada" - "Solicitando vídeo" - "Non se pode conectar a videochamada" - "Rexeitouse a solicitude de vídeo" - "O teu número de devolución de chamada\n %1$s" - "O teu número de devolución de chamada de emerxencia\n %1$s" - "Marcando" - "Chamada perdida" - "Chamadas perdidas" - "%s chamadas perdidas" - "Chamada perdida de %s" - "Chamada en curso" - "Chamada de traballo saínte" - "Chamada por wifi saínte" - "Chamada por wifi de traballo saínte" - "En espera" - "Chamada entrante" - "Chamada de traballo entrante" - "Chamada por wifi entrante" - "Chamada wifi de traballo entrante" - "Videochamada entrante" - "Chamada entrante sospeitosa de spam" - "Solicitude de vídeo entrante" - "Correo de voz novo" - "Correo de voz novo (%d)" - "Marcar o %s" - "Número de correo de voz descoñecido" - "Sen servizo" - "A rede seleccionada (%s) non está dispoñible" - "Responder" - "Colgar" - "Vídeo" - "Voz" - "Aceptar" - "Ignorar" - "Dev. chamada" - "Mensaxe" - "Chamada en curso noutro dispositivo" - "Transferir chamada" - "Para realizar unha chamada, primeiro desactiva o modo avión." - "Sen rexistro na rede." - "Rede móbil non dispoñible." - "Para realizar unha chamada, introduce un número válido." - "Non se pode realizar a chamada." - "Iniciando secuencia MMI..." - "Servizo non compatible." - "Non se poden cambiar as chamadas." - "Non se pode separar a chamada." - "Non se pode transferir." - "Non se pode realizar a conferencia." - "Non se pode rexeitar a chamada." - "Non se poden desconectar as chamadas." - "Chamada SIP" - "Chamada de emerxencia" - "Activando radio..." - "Sen servizo. Tentándoo de novo…" - "Non se pode realizar a chamada. %s non é un número de emerxencia." - "Non se pode realizar a chamada. Marca un número de emerxencia." - "Utiliza o teclado para marcar" - "Poñer a chamada en espera" - "Retomar chamada" - "Finalizar chamada" - "Mostrar teclado de marcación" - "Ocultar teclado de marcación" - "Silenciar" - "Activar o son" - "Engadir chamada" - "Combinar chamadas" - "Cambiar" - "Xestionar chamadas" - "Xestionar confer. telefónica" - "Conferencia telefónica" - "Xestionar" - "Audio" - "Videocham." - "Cambiar para chamada de voz" - "Cambiar cámara" - "Acender cámara" - "Apagar cámara" - "Máis opcións" - "Iniciouse o reprodutor" - "Detívose o reprodutor" - "A cámara non está preparada" - "A cámara está preparada" - "Evento de sesión de chamada descoñecido" - "Servizo" - "Configuración" - "<Sen configurar>" - "Outras configuracións de chamada" - "Chamando a través de %s" - "Chamadas entrantes a través de %s" - "foto do contacto" - "activar o modo privado" - "seleccionar contacto" - "Escribe a túa propia..." - "Cancelar" - "Enviar" - "Responder" - "Enviar SMS" - "Rexeitar" - "Responde como videochamada" - "Responde como chamada de audio" - "Acepta a solicitude de vídeo" - "Rexeita a solicitude de vídeo" - "Acepta a solicitude de transmisión de vídeo" - "Rexeita a solicitude de transmisión de vídeo" - "Acepta a solicitude de recepción de vídeo" - "Rexeita a solicitude de recepción de vídeo" - "Pasa o dedo cara a arriba para %s." - "Pasa o dedo cara a esquerda para %s." - "Pasa o dedo cara a dereita para %s." - "Pasa o dedo cara a abaixo para %s." - "Vibrar" - "Vibrar" - "Son" - "Son predeterminado (%1$s)" - "Ton de chamada do teléfono" - "Vibrar ao soar" - "Ton de chamada e vibración" - "Xestionar conferencia telefónica" - "Número de emerxencia" - "Foto do perfil" - "A cámara está desactivada" - "a través do %s" - "Enviouse a nota" - "Mensaxes recentes" - "Información da empresa" - "A %.1f mi de distancia" - "A %.1f km de distancia" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "Abre mañá ás %s" - "Abre hoxe ás %s" - "Pecha ás %s" - "Pechou hoxe ás %s" - "Aberto agora" - "Pechado agora" - "Chamada sospeitosa" - "Finalizouse a chamada %1$s" - "É a primeira vez que te chama este número." - "Sospeitamos que esta chamada era un xerador de spam." - "Bloquear/marcar spam" - "Engadir contacto" - "Non é spam" - diff --git a/InCallUI/res/values-gu/strings.xml b/InCallUI/res/values-gu/strings.xml deleted file mode 100644 index 017ccb691..000000000 --- a/InCallUI/res/values-gu/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "ફોન" - "હોલ્ડ પર" - "અજાણ્યો" - "ખાનગી નંબર" - "પેફોન" - "કોન્ફરન્સ કૉલ" - "કૉલ કપાઇ ગયો" - "સ્પીકર" - "હેન્ડસેટ ઇયરપીસ" - "વાયર્ડ હેડસેટ" - "Bluetooth" - "નીચે આપેલ ટોન્સ મોકલીએ?\n" - "ટોન્સ મોકલી રહ્યાં છે\n" - "મોકલો" - "હા" - "નહીં" - "વાઇલ્ડ અક્ષરને આની સાથે બદલો" - "કોન્ફરન્સ કૉલ %s" - "વૉઇસમેઇલ નંબર" - "ડાયલ કરી રહ્યાં છે" - "ફરી ડાયલ કરી રહ્યાં છે" - "કોન્ફરન્સ કૉલ" - "ઇનકમિંગ કૉલ" - "ઇનકમિંગ કાર્ય કૉલ" - "કૉલ સમાપ્ત થયો" - "હોલ્ડ પર" - "સમાપ્ત કરી રહ્યાં છે" - "કૉલમાં" - "મારો નંબર %s છે" - "વિડિઓ કનેક્ટ કરી રહ્યાં છે" - "વિડિઓ કૉલ" - "વિડિઓની વિનંતી કરી રહ્યાં છે" - "વિડિઓ કૉલ કનેક્ટ કરી શકાતો નથી" - "વિડિઓ વિનંતી નકારી" - "તમારો કૉલબેક નંબર\n %1$s" - "તમારો કટોકટીનો કૉલબેક નંબર\n %1$s" - "ડાયલ કરી રહ્યાં છે" - "છૂટેલો કૉલ" - "છૂટેલા કૉલ્સ" - "%s છૂટેલા કૉલ" - "%s નો કૉલ ચૂકી ગયાં" - "ચાલી રહેલ કૉલ" - "ચાલી રહેલ કાર્ય કૉલ" - "ચાલી રહેલ Wi-Fi કૉલ" - "ચાલી રહેલ Wi-Fi કાર્ય કૉલ" - "હોલ્ડ પર" - "ઇનકમિંગ કૉલ" - "ઇનકમિંગ કાર્ય કૉલ" - "ઇનકમિંગ Wi-Fi કૉલ" - "ઇનકમિંગ Wi-Fi કાર્ય કૉલ" - "ઇનકમિંગ વિડિઓ કૉલ" - "ઇનકમિંગ શંકાસ્પદ સ્પામ કૉલ" - "ઇનકમિંગ વિડિઓ વિનંતી" - "નવો વૉઇસમેઇલ" - "નવો વૉઇસમેઇલ (%d)" - "%s ડાયલ કરો" - "વૉઇસમેઇલ નંબર અજાણ" - "કોઈ સેવા નથી" - "પસંદ કરેલ નેટવર્ક (%s) અનુપલબ્ધ" - "જવાબ" - "સમાપ્ત કરો" - "વિડિઓ" - "વૉઇસ" - "સ્વીકારો" - "છોડી દો" - "કૉલ બૅક કરો" - "સંદેશ" - "અન્ય ઉપકરણ પર ચાલી રહેલ કૉલ" - "કૉલ સ્થાનાંતરિત કરો" - "કૉલ કરવા માટે, પહેલા એરપ્લેન મોડને બંધ કરો." - "નેટવર્ક પર નોંધણી કરાયેલ નથી." - "સેલ્યુલર નેટવર્ક ઉપલબ્ધ નથી." - "કૉલ કરવા માટે, માન્ય નંબર દાખલ કરો." - "કૉલ કરી શકાતો નથી." - "MMI અનુક્રમ પ્રારંભ કરી રહ્યાં છે…" - "સેવા સમર્થિત નથી." - "કૉલ્સ સ્વિચ કરી શકાતાં નથી." - "અલગ કૉલ કરી શકાતો નથી." - "ટ્રાંસ્ફર કરી શકાતો નથી." - "કોન્ફરન્સ કરી શકાતી નથી." - "કૉલ નકારી શકાતો નથી." - "કૉલ(કૉલ્સ) રિલીઝ કરી શકતાં નથી." - "SIP કૉલ" - "કટોકટીનો કૉલ" - "રેડિઓ ચાલુ કરી રહ્યાં છે…" - "કોઈ સેવા નથી. ફરી પ્રયાસ કરી રહ્યાં છે…" - "કૉલ કરી શકાતો નથી. %s એ કટોકટીનો નંબર નથી." - "કૉલ કરી શકાતો નથી. કટોકટીનો નંબર ડાયલ કરો." - "ડાયલ કરવા માટે કીબોર્ડનો ઉપયોગ કરો" - "કૉલ હોલ્ડ કરો" - "કૉલ ફરી શરૂ કરો" - "કૉલ સમાપ્ત કરો" - "ડાયલપેડ બતાવો" - "ડાયલપેડ છુપાવો" - "મ્યૂટ કરો" - "અનમ્યૂટ કરો" - "કૉલ ઉમેરો" - "કૉલ્સ મર્જ કરો" - "સ્વેપ કરો" - "કૉલ્સ સંચાલિત કરો" - "કોન્ફરન્સ કૉલ સંચાલિત કરો" - "કોન્ફરન્સ કૉલ" - "સંચાલિત કરો" - "ઑડિઓ" - "વિડિઓ કૉલ" - "વૉઇસ કૉલ પર બદલો" - "કૅમેરા પર સ્વિચ કરો" - "કૅમેરો ચાલુ કરો" - "કૅમેરો બંધ કરો" - "વધુ વિકલ્પો" - "પ્લેયર પ્રારંભ કર્યું" - "પ્લેયર બંધ કર્યું" - "કૅમેરો તૈયાર નથી" - "કૅમેરો તૈયાર" - "અજાણી કૉલ સત્ર ઇવેન્ટ" - "સેવા" - "સેટઅપ" - "<સેટ કરેલ નથી>" - "અન્ય કૉલ સેટિંગ્સ" - "%s મારફતે કૉલ કરે છે" - "%s મારફતે ઇનકમિંગ" - "સંપર્ક ફોટો" - "ખાનગી થાઓ" - "સંપર્ક પસંદ કરો" - "તમારો પોતાનો લખો…" - "રદ કરો" - "મોકલો" - "જવાબ" - "SMS મોકલો" - "નકારો" - "વિડિઓ કૉલ તરીકે જવાબ આપો" - "ઑડિઓ કૉલ તરીકે જવાબ આપો" - "વિડિઓ વિનંતી સ્વીકારો" - "વિડિઓ વિનંતી નકારો" - "વિડિઓ ટ્રાંસ્મિટ વિનંતી સ્વીકારો" - "વિડિઓ ટ્રાંસ્મિટ વિનંતી નકારો" - "વિડિઓ પ્રાપ્તિ વિનંતી સ્વીકારો" - "વિડિઓ પ્રાપ્તિ વિનંતી નકારો" - "%s માટે ઉપર સ્લાઇડ કરો." - "%s માટે ડાબે સ્લાઇડ કરો." - "%s માટે જમણે સ્લાઇડ કરો." - "%s માટે નીચે સ્લાઇડ કરો." - "વાઇબ્રેટ" - "વાઇબ્રેટ" - "ધ્વનિ" - "ડિફોલ્ટ ધ્વનિ (%1$s)" - "ફોન રિંગટોન" - "જ્યારે રિંગ કરે ત્યારે વાઇબ્રેટ કરો" - "રિંગટોન અને વાઇબ્રેટ" - "કોન્ફરન્સ કૉલ સંચાલિત કરો" - "કટોકટીનો નંબર" - "પ્રોફાઇલ ફોટો" - "કૅમેરો બંધ" - "%s મારફતે" - "નોંધ મોકલી" - "તાજેતરનાં સંદેશા" - "વ્યવસાયની માહિતી" - "%.1f માઇલ દૂર" - "%.1f કિમી દૂર" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "આવતીકાલે %s વાગ્યે ખુલશે" - "આજે %s વાગ્યે ખુલશે" - "%s વાગ્યે બંધ થશે" - "આજે %s વાગ્યે બંધ થયેલું" - "હમણાં ખુલ્લું" - "હમણાં બંધ છે" - "શંકાસ્પદ સ્પામ કૉલર" - "%1$s નો કૉલ સમાપ્ત થયો" - "આ નંબરથી તમને પહેલી વાર કૉલ કરવામાં આવ્યો છે." - "આ કૉલ કોઈ સ્પામર હોવાની અમને શંકા છે." - "સ્પામની જાણ/અવરોધિત કરો" - "સંપર્ક ઉમેરો" - "સ્પામ નથી" - diff --git a/InCallUI/res/values-h400dp/dimens.xml b/InCallUI/res/values-h400dp/dimens.xml deleted file mode 100644 index dda755a3e..000000000 --- a/InCallUI/res/values-h400dp/dimens.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - true - - 90dp - - 15dp - - -24dp - - 2dp - - 20dp - diff --git a/InCallUI/res/values-hi/strings.xml b/InCallUI/res/values-hi/strings.xml deleted file mode 100644 index dee8f464d..000000000 --- a/InCallUI/res/values-hi/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "फ़ोन" - "होल्ड पर" - "अज्ञात" - "निजी नंबर" - "पे-फ़ोन" - "कॉन्फ़्रेंस कॉल" - "कॉल कट गया" - "स्पीकर" - "हैंडसेट इयरपीस" - "वायर वाला हैडसेट" - "ब्लूटूथ" - "निम्न टोन भेजें?\n" - "भेजने वाली टोन\n" - "भेजें" - "हां" - "नहीं" - "वाइल्ड वर्ण को इससे बदलें:" - "कॉन्फ़्रेंस कॉल %s" - "वॉइसमेल नबंर" - "डायल किया जा रहा है" - "पुन: डायल हो रहा है" - "कॉन्फ़्रेंस कॉल" - "इनकमिंग कॉल" - "कार्यस्थल का इनकमिंग कॉल" - "कॉल समाप्त" - "होल्ड पर" - "कॉल समाप्त हो रहा है" - "कॉल में" - "मेरा नंबर %s है" - "वीडियो कनेक्ट हो रहा है" - "वीडियो कॉल" - "वीडियो का अनुरोध किया जा रहा है" - "वीडियो कॉल कनेक्ट नहीं किया जा सकता" - "वीडियो अनुरोध अस्वीकार किया गया" - "आपका कॉलबैक नंबर\n %1$s" - "आपका आपातकालीन कॉलबैक नंबर\n %1$s" - "डायल किया जा रहा है" - "छूटा कॉल" - "छूटे कॉल" - "%s छूटे कॉल" - "%s के छूटे कॉल" - "चल रहा कॉल" - "कार्यस्थल का जारी कॉल" - "चल रहा वाई-फ़ाई कॉल" - "कार्यस्थल का जारी वाई-फ़ाई कॉल" - "होल्ड पर" - "इनकमिंग कॉल" - "कार्यस्थल का इनकमिंग कॉल" - "इनकमिंग वाई-फ़ाई कॉल" - "कार्यस्थल का वाई-फ़ाई इनकमिंग कॉल" - "इनकमिंग वीडियो कॉल" - "संदिग्ध आवक स्पैम कॉल" - "इनकमिंग वीडियो अनुरोध" - "नया वॉइसमेल" - "नया वॉइसमेल (%d)" - "%s डायल करें" - "वॉइसमेल नंबर अज्ञात" - "कोई सेवा नहीं" - "चयनित नेटवर्क (%s) अनुपलब्ध" - "उत्तर दें" - "समाप्त करें" - "वीडियो" - "ध्वनि" - "स्वीकार करें" - "ख़ारिज करें" - "कॉल बैक करें" - "संदेश" - "दूसरे डिवाइस पर चल रहा कॉल" - "कॉल स्थानान्तरित करें" - "कॉल करने के लिए, पहले हवाई जहाज़ मोड बंद करें." - "नेटवर्क पर पंजीकृत नहीं." - "सेल्युलर नेटवर्क उपलब्ध नहीं." - "कॉल करने के लिए, मान्य नंबर डालें." - "कॉल नहीं किया जा सकता." - "MMI अनुक्रम प्रारंभ हो रहा है…" - "सेवा समर्थित नहीं है." - "कॉल स्विच नहीं किए जा सकते." - "कॉल अलग नहीं किया जा सकता." - "स्थानान्तरित नहीं किया जा सकता." - "कॉन्फ़्रेंस नहीं की जा सकती." - "कॉल अस्वीकार नहीं किया जा सकता." - "कॉल रिलीज़ नहीं किया जा सकता (किए जा सकते)." - "SIP कॉल" - "आपातकालीन कॉल" - "रेडियो चालू कर रहा है..." - "कोई सेवा नहीं. पुन: प्रयास किया जा रहा है…" - "कॉल नहीं किया जा सकता. %s एक आपातकालीन नंबर नहीं है." - "कॉल नहीं किया जा सकता. आपातकालीन नबर डायल करें." - "डायल करने के लिए कीबोर्ड का उपयोग करें" - "कॉल होल्ड करें" - "कॉल फिर से शुरू करें" - "कॉल समाप्त करें" - "डायलपैड दिखाएं" - "डायलपैड छिपाएं" - "म्यूट करें" - "अनम्यूट करें" - "कॉल जोड़ें" - "कॉल मर्ज करें" - "स्वैप करें" - "कॉल प्रबंधित करें" - "कॉन्फ़्रेंस कॉल प्रबंधित करें" - "कॉन्फ़्रेंस कॉल" - "प्रबंधित करें" - "ऑडियो" - "वीडियो कॉल" - "वॉइस कॉल में बदलें" - "कैमरा स्विच करें" - "कैमरा चालू करें" - "कैमरा बंद करें" - "अधिक विकल्प" - "प्लेयर प्रारंभ हो गया" - "प्लेयर रुक गया" - "कैमरा तैयार नहीं है" - "कैमरा तैयार है" - "अज्ञात कॉल सत्र इवेंट" - "सेवा" - "सेटअप" - "<सेट नहीं है>" - "अन्य कॉल सेटिंग" - "%s के माध्यम से कॉल किया जा रहा है" - "%s की ओर से इनकमिंग" - "संपर्क का फ़ोटो" - "निजी हो जाएं" - "संपर्क को चुनें" - "अपना स्वयं का लिखें..." - "अभी नहीं" - "भेजें" - "उत्तर दें" - "SMS भेजें" - "अस्वीकार करें" - "वीडियो कॉल के रूप में उत्तर दें" - "ऑडियो कॉल के रूप में उत्तर दें" - "वीडियो अनुरोध स्वीकार करें" - "वीडियो अनुरोध अस्वीकार करें" - "वीडियो प्रसारण अनुरोध स्वीकार करें" - "वीडियो प्रसारण अनुरोध अस्वीकार करें" - "वीडियो प्राप्ति अनुरोध स्वीकार करें" - "वीडियो प्राप्ति अनुरोध अस्वीकार करें" - "%s करने के लिए ऊपर स्लाइड करें." - "%s करने के लिए बाएं स्लाइड करें." - "%s करने के लिए दाएं स्लाइड करें." - "%s करने के लिए नीचे स्लाइड करें." - "कंपन" - "कंपन" - "ध्वनि" - "डिफ़ॉल्ट ध्वनि (%1$s)" - "फ़ोन रिंगटोन" - "रिंग आने पर कंपन करें" - "रिंगटोन और कंपन" - "कॉन्फ़्रेंस कॉल प्रबंधित करें" - "आपातकालीन नंबर" - "प्रोफ़ाइल फ़ोटो" - "कैमरा बंद है" - "%s के द्वारा" - "नोट भेज दिया गया है" - "हाल ही के संदेश" - "व्यवसाय जानकारी" - "%.1f मील दूर" - "%.1f किमी दूर" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "कल %s बजे खुलेगा" - "आज %s बजे खुलता है" - "%s बजे बंद होता है" - "आज %s बजे बंद हो गया" - "अभी खुला है" - "अभी बंद है" - "संदिग्ध स्पैम कॉलर" - "%1$s का कॉल समाप्त हो गया" - "इस नंबर से आपको पहली बार कॉल किया गया है." - "हमें आशंका है कि कॉल स्पैमकर्ता का हो सकता है." - "अवरुद्ध करें/स्पैम रिपोर्ट करें" - "संपर्क जोड़ें" - "स्पैम नहीं है" - diff --git a/InCallUI/res/values-hr/strings.xml b/InCallUI/res/values-hr/strings.xml deleted file mode 100644 index 9c11644c4..000000000 --- a/InCallUI/res/values-hr/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefon" - "Na čekanju" - "Nepoznato" - "Privatni broj" - "Javna telefonska govornica" - "Konferencijski poziv" - "Poziv je prekinut" - "Zvučnik" - "Slušalice" - "Žičane slušalice" - "Bluetooth" - "Poslati sljedeće tonove?\n" - "Slanje tonova\n" - "Pošalji" - "Da" - "Ne" - "Zamijeni zamjenski znak znakom" - "Konferencijski poziv %s" - "Broj govorne pošte" - "Biranje broja" - "Ponovno biranje" - "Konferencijski poziv" - "Dolazni poziv" - "Dolazni poslovni poziv" - "Poziv je završio" - "Na čekanju" - "Prekidanje veze" - "Poziv u tijeku" - "Moj je broj %s" - "Uspostavljanje videopoziva" - "Videopoziv" - "Zahtijevanje videopoziva" - "Videopoziv nije uspostavljen" - "Zahtjev za videopoziv odbijen" - "Vaš broj za povratni poziv\n %1$s" - "Vaš broj za povratni poziv za hitne službe\n %1$s" - "Biranje broja" - "Propušteni poziv" - "Propušteni pozivi" - "Propušteni pozivi (%s)" - "Propušten poziv kontakta %s" - "Poziv u tijeku" - "Poslovni poziv u tijeku" - "Wi-Fi poziv u tijeku" - "Poslovni Wi-Fi poziv u tijeku" - "Na čekanju" - "Dolazni poziv" - "Dolazni poslovni poziv" - "Dolazni Wi-Fi poziv" - "Dolazni poslovni Wi-Fi poziv" - "Dolazni videopoziv" - "Mogući neželjeni dolazni poziv" - "Dolazni zahtjev za videopoziv" - "Nova govorna pošta" - "Nova govorna pošta (%d)" - "Biraj %s" - "Nepoznat broj govorne pošte" - "Nema usluge" - "Odabrana mreža (%s) nije dostupna" - "Odgovori" - "Prekini vezu" - "Videopoziv" - "Glasovni poziv" - "Prihvati" - "Odbaci" - "Uzvrati" - "Poruka" - "Poziv u tijeku na drugom uređaju" - "Prijenos poziva" - "Da biste uspostavili poziv, isključite način rada u zrakoplovu." - "Nije registrirano na mreži." - "Mobilna mreža nije dostupna." - "Unesite važeći broj da biste uspostavili poziv." - "Pozivanje nije moguće." - "Pokretanje MMI sekvence…" - "Usluga nije podržana." - "Prebacivanje poziva nije moguće." - "Odvajanje poziva nije moguće." - "Prijenos nije moguć." - "Konferencijski poziv nije moguć." - "Odbijanje poziva nije moguće." - "Prekidanje poziva nije moguće." - "SIP poziv" - "Hitni poziv" - "Uključivanje radija…" - "Nema usluge. Pokušavamo ponovo…" - "Pozivanje nije moguće. %s nije broj hitne službe." - "Pozivanje nije moguće. Nazovite broj hitne službe." - "Upotrijebite tipkovnicu" - "Stavi poziv na čekanje" - "Nastavi poziv" - "Završi poziv" - "Prikaži površinu za biranje brojeva" - "Sakrij površinu za biranje brojeva" - "Zanemari" - "Prestani zanemarivati" - "Dodaj poziv" - "Spoji pozive" - "Zamijeni" - "Upravljaj pozivima" - "Upravljaj konf. pozivom" - "Konferencijski poziv" - "Upravljanje" - "Audio" - "Videopoziv" - "Prijeđi na glasovni poziv" - "Promijeni kameru" - "Uključivanje kamere" - "Isključivanje kamere" - "Više opcija" - "Player je pokrenut" - "Player je prekinut" - "Fotoaparat nije spreman" - "Fotoaparat je spreman" - "Nepoznati događaj sesije poziva" - "Usluga" - "Postavljanje" - "<Nije postavljeno>" - "Ostale postavke poziva" - "Pozivanje putem operatera %s" - "Dolazni pozivi putem usluge %s" - "fotografija kontakta" - "uputi na privatno" - "odabir kontakta" - "Napišite odgovor..." - "Odustani" - "Pošalji" - "Odgovori" - "Pošalji SMS" - "Odbij" - "Prihvati kao videopoziv" - "Prihvati kao audiopoziv" - "Prihvati zahtjev za videopoziv" - "Odbij zahtjev za videopoziv" - "Prihvati zahtjev za slanje videopoziva" - "Odbij zahtjev za slanje videopoziva" - "Prihvati zahtjev za primanje videopoziva" - "Odbij zahtjev za primanje videopoziva" - "Kliznite prema gore za %s." - "Kliznite lijevo za %s." - "Kliznite desno za %s." - "Kliznite prema dolje za %s." - "Vibriranje" - "Vibriranje" - "Zvuk" - "Zadani zvuk (%1$s)" - "Melodija zvona telefona" - "Vibracija tijekom zvonjenja" - "Melodija zvona i vibracija" - "Upravljaj konferencijskim pozivom" - "Broj hitne službe" - "Fotografija profila" - "Fotoaparat je isključen" - "putem broja %s" - "Bilješka je poslana" - "Nedavne poruke" - "Informacije o tvrtki" - "%.1f mi udaljenosti" - "%.1f km udaljenosti" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "Otvara se sutra u %s" - "Otvara se danas u %s" - "Zatvara se u %s" - "Zatvoreno danas u %s" - "Trenutačno otvoreno" - "Trenutačno zatvoreno" - "Neželjeni pozivatelj" - "Poziv je završio %1$s" - "Prvi ste put primili poziv s tog broja." - "Sumnjamo da vas zove pošiljatelj neželjenih sadržaja." - "Blokiranje/prijava neželjenog broja" - "Dodavanje kontakta" - "Nije neželjeni broj" - diff --git a/InCallUI/res/values-hu/strings.xml b/InCallUI/res/values-hu/strings.xml deleted file mode 100644 index 816942601..000000000 --- a/InCallUI/res/values-hu/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefon" - "Tartásban" - "Ismeretlen" - "Magántelefonszám" - "Nyilvános telefon" - "Konferenciahívás" - "A hívás megszakadt" - "Hangszóró" - "Kézibeszélő fülhallgatója" - "Vezetékes fülhallgató" - "Bluetooth" - "Elküldi a következő hangjelzéseket?\n" - "Hangjelzések küldése\n" - "Küldés" - "Igen" - "Nem" - "Helyettesítő karakter lecserélése a következőre:" - "Konferenciahívás – %s" - "Hangposta száma" - "Tárcsázás" - "Újratárcsázás" - "Konferenciahívás" - "Bejövő hívás" - "Bejövő munkahelyi hívás" - "A hívás befejeződött" - "Tartásban" - "Megszakítás" - "Hívásban" - "A számom: %s" - "Videókapcsolat létrehozása" - "Videóhívás" - "Videóhívást kér" - "A videóhívás létesítése sikertelen" - "Videókérelem elutasítva" - "Visszahívható telefonszáma:\n %1$s" - "Vészhelyzet esetén visszahívható telefonszáma:\n %1$s" - "Tárcsázás" - "Nem fogadott hívás" - "Nem fogadott hívások" - "%s nem fogadott hívás" - "Nem fogadott hívás: %s" - "Hívás folyamatban" - "Folyamatban lévő munkahelyi hívás" - "Folyamatban lévő Wi-Fi-hívás" - "Folyamatban lévő munkahelyi hívás Wi-Fi-hálózaton keresztül" - "Tartásban" - "Bejövő hívás" - "Bejövő munkahelyi hívás" - "Bejövő Wi-Fi-hívás" - "Bejövő munkahelyi hívás Wi-Fi-hálózaton keresztül" - "Bejövő videóhívás" - "Bejövő gyanús spamhívás" - "Bejövő videókérés" - "Új hangpostaüzenet" - "Új hangpostaüzenet (%d)" - "%s tárcsázása" - "A hangposta száma ismeretlen" - "Nincs szolgáltatás" - "A kiválasztott hálózat (%s) nem áll rendelkezésre" - "Fogadás" - "Hívás bontása" - "Videó" - "Hang" - "Elfogadás" - "Elvetés" - "Visszahívás" - "Üzenet" - "Folyamatban lévő hívás egy másik eszközön" - "Hívásátirányítás" - "Hívásindításhoz kapcsolja ki a Repülős üzemmódot." - "Nincs regisztrálva a hálózaton." - "A mobilhálózat nem áll rendelkezésre." - "Hívásindításhoz adjon meg egy érvényes számot." - "A hívás sikertelen." - "MMI-sorozat indítása…" - "A szolgáltatás nem támogatott." - "A hívások közötti váltás sikertelen." - "A híváselkülönítés sikertelen." - "Az átirányítás sikertelen." - "A konferenciahívás sikertelen." - "A híváselutasítás sikertelen." - "A tartásban lévő hívás(ok) folytatása sikertelen." - "SIP-hívás" - "Segélyhívás" - "Rádió bekapcsolása…" - "Nincs szolgáltatás. Újrapróbálkozás folyamatban…" - "A hívás sikertelen. A(z) %s szám nem segélyhívószám." - "A hívás sikertelen. Tárcsázzon segélyhívószámot." - "A tárcsázáshoz használja a billentyűzetet" - "Hívás tartása" - "Hívás folytatása" - "Hívás befejezése" - "Tárcsázó megjelenítése" - "Tárcsázó elrejtése" - "Némítás" - "Némítás feloldása" - "Hívás hozzáadása" - "Hívások egyesítése" - "Csere" - "Hívások kezelése" - "Konferenciahívás kezelése" - "Konferenciahívás" - "Kezelés" - "Hang" - "Videóhívás" - "Váltás hanghívásra" - "Váltás a kamerák között" - "Kamera bekapcsolása" - "Kamera kikapcsolása" - "További lehetőségek" - "A lejátszó elindult" - "A lejátszó leállt" - "A kamera nem áll készen" - "A kamera készen áll" - "Ismeretlen hívási esemény" - "Szolgáltatás" - "Beállítás" - "<Nincs megadva>" - "Egyéb hívásbeállítások" - "Hívás a(z) %s szolgáltatón keresztül" - "Bejövő hívás a következőn keresztül: %s" - "fotó a névjegyhez" - "magánbeszélgetés" - "névjegy kiválasztása" - "Saját válasz írása…" - "Mégse" - "Küldés" - "Fogadás" - "SMS küldése" - "Elutasítás" - "Fogadás videóhívásként" - "Fogadás hanghívásként" - "Videó kérésének elfogadása" - "Videó kérésének elutasítása" - "Videóküldési kérés elfogadása" - "Videóküldési kérés elutasítása" - "Videófogadási kérés elfogadása" - "Videófogadási kérés elutasítása" - "A(z) %s művelethez csúsztassa felfelé." - "A(z) %s művelethez csúsztassa balra." - "A(z) %s művelethez csúsztassa jobbra." - "A(z) %s művelethez csúsztassa lefelé." - "Rezgés" - "Rezgés" - "Hang" - "Alapértelmezett hang (%1$s)" - "Telefon csengőhangja" - "Csörgéskor rezegjen" - "Csengőhang és rezgés" - "Konferenciahívás kezelése" - "Segélyhívó szám" - "Profilfotó" - "Kamera ki" - "a következő számon keresztül: %s" - "Üzenet elküldve" - "Legutóbbi üzenetek" - "Cég adatai" - "%.1f mérföldre" - "%.1f kilométerre" - "%2$s, %1$s" - "%1$s%2$s" - "%1$s, %2$s" - "Holnap ekkor nyit: %s" - "Ma ekkor nyit: %s" - "Ekkor zár: %s" - "Ma ekkor zárt: %s" - "Jelenleg nyitva van" - "Jelenleg zárva van" - "Gyanús spamhívó" - "Hívás befejezve: %1$s" - "Ez az első alkalom, hogy erről a számról hívása érkezett." - "Azt gyanítjuk, hogy ez egy spamhívás." - "Letiltás/spam bejel." - "Névjegy hozzáadása" - "Nem spam" - diff --git a/InCallUI/res/values-hy/strings.xml b/InCallUI/res/values-hy/strings.xml deleted file mode 100644 index 899262432..000000000 --- a/InCallUI/res/values-hy/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Հեռախոս" - "Սպասում" - "Անհայտ" - "Գաղտնի համար" - "Հանրային հեռախոս" - "Կոնֆերանս զանգ" - "Զանգն ընդհատվեց" - "Բարձրախոս" - "Հեռախոսի ականջակալ" - "Լարային ականջակալ" - "Bluetooth" - "Ուղարկե՞լ հետևյալ ձայներանգները:\n" - "Ձայներանգների ուղարկում\n" - "Ուղարկել" - "Այո" - "Ոչ" - "Փոխարինել կոպիտ գրանշանը" - "Կոնֆերանս զանգ %s" - "Ձայնային փոստի համարը" - "Համարը հավաքվում է" - "Վերահամարարկում" - "Կոնֆերանս զանգ" - "Մուտքային զանգ" - "Մուտքային աշխատանքային զանգ" - "Զանգն ավարտվեց" - "Սպասում" - "Անջատում" - "Զանգը միացված է" - "Իմ համարը՝ %s" - "Տեսակապը միանում է" - "Տեսազանգ" - "Տեսակապի հայցում" - "Հնարավոր չէ միացնել տեսազանգը" - "Տեսազանգի հարցումը մերժվել է" - "Հետադարձ զանգի համարը՝\n%1$s" - "Արտակարգ իրավիճակների հետադարձ զանգի համարը՝\n%1$s" - "Համարը հավաքվում է" - "Բաց թողնված զանգ" - "Բաց թողնված զանգեր" - "%s բաց թողնված զանգ" - "Բաց թողնված զանգ %s-ից" - "Ընթացիկ զանգ" - "Ընթացիկ աշխատանքային զանգ" - "Ընթացիկ Wi-Fi զանգ" - "Ընթացիկ աշխատանքային Wi-Fi զանգ" - "Սպասում" - "Մուտքային զանգ" - "Մուտքային աշխատանքային զանգ" - "Մուտքային Wi-Fi զանգ" - "Մուտքային աշխատանքային Wi-Fi զանգ" - "Մուտքային տեսազանգ" - "Մուտքային զանգը հավանաբար լցոն է" - "Մուտքային տեսազանգի հայցում" - "Նոր ձայնային հաղորդագրություն" - "Նոր ձայնային հաղորդագրություն (%d)" - "Զանգել %s համարին" - "Ձայնային փոստի համարն անհայտ է" - "Ծառայություն չկա" - "Ընտրված ցանցը (%s) անհասանելի է" - "Պատասխանել" - "Անջատել" - "Տեսազանգ" - "Ձայնային" - "Ընդունել" - "Մերժել" - "Հետ զանգել" - "Հաղորդագրություն" - "Ընթացիկ զանգ այլ սարքում" - "Փոխանցել զանգը" - "Զանգ կատարելու համար նախ անջատեք Ինքնաթիռի ռեժիմը:" - "Ցանցում գրանցված չէ:" - "Բջջային ցանցն անհասանելի է:" - "Զանգ կատարելու համար մուտքագրեք ճիշտ համար:" - "Հնարավոր չէ զանգել:" - "Մեկնարկում է MMI հաջորդականությունը…" - "Ծառայությունը չի աջակցվում:" - "Հնարավոր չէ փոխարկել զանգերը:" - "Հնարավոր չէ առանձնացնել զանգը:" - "Հնարավոր չէ փոխանցել:" - "Հնարավոր չէ կոնֆերանս զանգ կատարել:" - "Հնարավոր չէ մերժել զանգը:" - "Հնարավոր չէ անջատել զանգ(եր)ը:" - "SIP զանգ" - "Շտապ կանչ" - "Ռադիոն միացվում է…" - "Ծառայությունը մատչելի չէ: Փորձը կրկնվում է…" - "Հնարավոր չէ զանգել: %s համարը արտակարգ իրավիճակի համար չէ:" - "Հնարավոր չէ զանգել: Հավաքեք արտակարգ իրավիճակի որևէ համար:" - "Օգտագործել ստեղնաշարը համար հավաքելու համար" - "Հետաձգել զանգը" - "Վերսկսել զանգը" - "Ավարտել զանգը" - "Ցուցադրել թվաշարը" - "Թաքցնել թվաշարը" - "Անջատել ձայնը" - "Չանտեսել" - "Ավելացնել զանգ" - "Միացնել զանգերը" - "Փոխանակել" - "Կառավարել զանգերը" - "Կառավարել կոնֆերանս զանգը" - "Կոնֆերանս զանգ" - "Կառավարել" - "Ձայնային" - "Տեսազանգ" - "Փոխարկել ձայնային կանչի" - "Փոխարկել խցիկը" - "Միացնել տեսախցիկը" - "Անջատել տեսախցիկը" - "Այլ ընտրանքներ" - "Նվագարկիչը մեկնարկել է" - "Նվագարկիչը դադարեցվել է" - "Տեսախցիկը պատրաստ չէ" - "Տեսախցիկը պատրաստ է" - "Զանգի աշխատաշրջանի անհայտ իրադարձություն" - "Ծառայություն" - "Կարգավորում" - "<Կարգավորված չէ>" - "Զանգերի այլ կարգավորումներ" - "Զանգում է %s-ի միջոցով" - "Մուտքային զանգ %s-ի միջոցով" - "կոնտակտի լուսանկարը" - "անցնել անձնականի" - "ընտրել կոնտակտ" - "Գրեք ձեր սեփականը…" - "Չեղարկել" - "Ուղարկել" - "Պատասխանել" - "Ուղարկել SMS" - "Մերժել" - "Պատասխանել տեսազանգով" - "Պատասխանել ձայնային զանգով" - "Ընդունել տեսազանգի հարցումը" - "Մերժել տեսազանգի հարցումը" - "Ընդունել տեսափոխանցման հարցումը" - "Մերժել տեսափոխանցման հարցումը" - "Ընդունել տեսազանգ ստանալու հարցումը" - "Մերժել տեսազանգ ստանալու հարցումը" - "Սահեցրեք վերև` %s գործառույթի համար:" - "Սահեցրեք ձախ` %s գործառույթի համար:" - "Սահեցրեք աջ` %s գործառույթի համար:" - "Սահեցրեք ցած՝ %s գործառույթի համար:" - "Թրթռոց" - "Թրթռոց" - "Ձայն" - "Կանխադրված ձայնը (%1$s)" - "Հեռախոսի զանգերանգ" - "Թրթռալ զանգի ժամանակ" - "Ձայներանգ և թրթռոց" - "Կառավարել կոնֆերանս զանգը" - "Արտակարգ իրավիճակի համար" - "Պրոֆիլի լուսանկար" - "Տեսախցիկն անջատված" - "%s-ի միջոցով" - "Գրառումն ուղարկվեց" - "Վերջին հաղորդագրությունները" - "Բիզնես տեղեկատվություն" - "%.1f մղոն հեռու" - "%.1f կմ հեռու" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "Բացվում է վաղը ժամը %s-ին" - "Բացվում է այսօր ժամը %s-ին" - "Փակվում է ժամը %s-ին" - "Փակվել է այսօր ժամը %s-ին" - "Հիմա բաց է" - "Հիմա փակ է" - "Հավանաբար լցոն է" - "Զանգն ավարտվեց %1$s" - "Այս համարից առաջին անգամ եք զանգ ստանում:" - "Կասկածներ կային, որ այս զանգը լցոնողից է:" - "Արգելափակել/Նշել լցոն" - "Ավելացնել կոնտակտ" - "Լցոն չէ" - diff --git a/InCallUI/res/values-in/strings.xml b/InCallUI/res/values-in/strings.xml deleted file mode 100644 index a2cd31dc7..000000000 --- a/InCallUI/res/values-in/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telepon" - "Ditahan" - "Tidak dikenal" - "Nomor pribadi" - "Telepon Umum" - "Telewicara" - "Panggilan terputus" - "Pengeras suara" - "Earpiece handset" - "Headset berkabel" - "Bluetooth" - "Kirim nada berikut?\n" - "Mengirim nada\n" - "Kirim" - "Ya" - "Tidak" - "Ganti karakter acak dengan" - "Telewicara %s" - "Nomor pesan suara" - "Memanggil" - "Memanggil ulang" - "Telewicara" - "Panggilan masuk" - "Panggilan masuk di telepon kerja" - "Panggilan diakhiri" - "Ditahan" - "Menutup panggilan" - "Sedang dalam panggilan" - "Nomor saya %s" - "Menyambungkan video" - "Video call" - "Meminta video" - "Tidak dapat menyambungkan video call" - "Permintaan video ditolak" - "Nomor panggilan balik Anda\n %1$s" - "Nomor panggilan balik darurat Anda\n %1$s" - "Memanggil" - "Panggilan tak terjawab" - "Panggilan tak terjawab" - "%s panggilan tak terjawab" - "Panggilan tak terjawab dari %s" - "Panggilan yang berlangsung" - "Panggilan telepon kerja yang sedang berlangsung" - "Panggilan Wi-Fi keluar" - "Panggilan Wi-Fi kerja yang sedang berlangsung" - "Ditahan" - "Panggilan masuk" - "Panggilan masuk di telepon kerja" - "Panggilan Wi-Fi masuk" - "Panggilan Wi-Fi masuk di telepon kerja" - "Video call masuk" - "Panggilan masuk yang diduga spam" - "Permintaan video masuk" - "Pesan suara baru" - "Pesan suara baru (%d)" - "Telepon %s" - "Nomor pesan suara tidak dikenal" - "Tidak ada layanan" - "Jaringan yang dipilih (%s) tidak tersedia" - "Jawab" - "Akhiri" - "Video" - "Suara" - "Terima" - "Tutup" - "Telepon balik" - "Pesan" - "Panggilan yang berlangsung di perangkat lain" - "Transfer Panggilan" - "Untuk melakukan panggilan, terlebih dahulu nonaktifkan mode Pesawat." - "Tidak terdaftar pada jaringan." - "Jaringan seluler tidak tersedia." - "Untuk melakukan panggilan telepon, masukkan nomor yang valid." - "Tidak dapat menelepon." - "Memulai urutan MMI..." - "Layanan tidak didukung." - "Tidak dapat beralih panggilan." - "Tidak dapat memisahkan panggilan." - "Tidak dapat mentransfer." - "Tidak dapat melakukan telewicara." - "Tidak dapat menolak panggilan." - "Tidak dapat melepas panggilan." - "Panggilan SIP" - "Panggilan darurat" - "Menghidupkan radio..." - "Tidak ada layanan. Mencoba lagi…" - "Tidak dapat menelepon. %s bukan nomor darurat." - "Tidak dapat menelepon. Panggil nomor darurat." - "Gunakan keyboard untuk memanggil" - "Tahan Panggilan" - "Mulai Kembali Panggilan" - "Akhiri Panggilan" - "Tampilkan tombol nomor" - "Sembunyikan tombol nomor" - "Bisukan" - "Suarakan" - "Tambahkan panggilan" - "Gabungkan panggilan" - "Tukar" - "Kelola panggilan" - "Kelola telewicara" - "Konferensi telepon" - "Kelola" - "Audio" - "Video call" - "Ubah ke panggilan suara" - "Beralih kamera" - "Nyalakan kamera" - "Matikan kamera" - "Opsi lainnya" - "Pemutar Dimulai" - "Pemutar Dihentikan" - "Kamera tidak siap" - "Kamera siap" - "Sesi panggilan tidak dikenal" - "Layanan" - "Siapkan" - "<Tidak disetel>" - "Setelan panggilan lainnya" - "Memanggil via %s" - "Masuk melalui %s" - "foto kontak" - "aktifkan pribadi" - "pilih kontak" - "Tulis respons Anda sendiri…" - "Batal" - "Kirim" - "Jawab" - "Kirim SMS" - "Tolak" - "Jawab sebagai video call" - "Jawab sebagai panggilan audio" - "Terima permintaan video" - "Tolak permintaan video" - "Terima permintaan transmisi video" - "Tolak permintaan transmisi video" - "Terima permintaan menerima video" - "Tolak permintaan menerima video" - "Geser ke atas untuk %s." - "Geser ke kiri untuk %s." - "Geser ke kanan untuk %s." - "Geser ke bawah untuk %s." - "Getar" - "Getar" - "Suara" - "Suara default (%1$s)" - "Nada dering ponsel" - "Bergetar saat berdering" - "Nada dering & Getar" - "Kelola telewicara" - "Nomor darurat" - "Foto profil" - "Kamera tidak aktif" - "melalui %s" - "Catatan telah dikirim" - "Pesan terbaru" - "Info bisnis" - "%.1f mil" - "%.1f km" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "Buka jam %s" - "Hari ini buka jam %s" - "Tutup pukul %s" - "Hari ini tutup pukul %s" - "Buka sekarang" - "Sekarang tutup" - "Diduga telepon spam" - "Panggilan diakhiri %1$s" - "Nomor ini baru pertama kali menghubungi Anda." - "Kami menduga panggilan ini dari pelaku spam." - "Blokir/laporkan spam" - "Tambahkan kontak" - "Bukan spam" - diff --git a/InCallUI/res/values-is/strings.xml b/InCallUI/res/values-is/strings.xml deleted file mode 100644 index 480fd6eaa..000000000 --- a/InCallUI/res/values-is/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Sími" - "Í bið" - "Óþekkt" - "Óþekkt númer" - "Símasjálfsali" - "Símafundur" - "Símtali slitið" - "Hátalari" - "Símahátalari" - "Höfuðtól með snúru" - "Bluetooth" - "Senda eftirfarandi tóna?\n" - "Sendir tóna\n" - "Senda" - "Já" - "Nei" - "Skipta algildisstaf út fyrir" - "Símafundur %s" - "Talhólfsnúmer" - "Hringir" - "Hringir aftur" - "Símafundur" - "Móttekið símtal" - "Vinnusímtal berst" - "Lagt á" - "Í bið" - "Leggur" - "Í símtali" - "Númerið mitt er %s" - "Tengir myndskeið" - "Myndsímtal" - "Biður um myndskeið" - "Ekki tókst að tengja myndsímtal" - "Myndsímtalsbeiðni hafnað" - "Svarhringingarnúmer þitt\n %1$s" - "Svarhringingarnúmer þitt í neyðartilvikum\n %1$s" - "Hringir" - "Ósvarað símtal" - "Ósvöruð símtöl" - "%s ósvöruð símtöl" - "Ósvarað símtal frá %s" - "Samtal í gangi" - "Vinnusímtal í gangi" - "Wi-Fi símtal stendur yfir" - "Vinnusímtal í gangi um Wi-Fi" - "Í bið" - "Móttekið símtal" - "Vinnusímtal berst" - "Wi-Fi símtal berst" - "Vinnusímtal berst um Wi-Fi" - "Myndsímtal berst" - "Símtal sem berst er hugsanlega úr ruslnúmeri" - "Myndbeiðni berst" - "Ný skilaboð í talhólfinu" - "Ný skilaboð í talhólfinu (%d)" - "Hringja í %s" - "Talhólfsnúmer ekki þekkt" - "Ekkert símasamband" - "Valið símkerfi (%s) er ekki tiltækt" - "Svara" - "Leggja á" - "Myndskeið" - "Tal" - "Samþykkja" - "Hunsa" - "Hringja til baka" - "Skilaboð" - "Símtal í gangi í öðru tæki" - "Flytja símtal" - "Til að hringja símtal þarftu fyrst að slökkva á flugstillingu." - "Ekki skráð á símkerfi." - "Farsímakerfi ekki til staðar." - "Sláðu inn gilt númer til að hringja símtal." - "Ekki hægt að hringja." - "Ræsir MMI-runu…" - "Þjónustan er ekki studd." - "Ekki hægt að skipta milli símtala." - "Ekki hægt að aðskilja símtal." - "Ekki hægt að flytja." - "Ekki hægt að halda símafund." - "Ekki hægt að hafna símtali." - "Ekki hægt að leggja á." - "SIP-símtal" - "Neyðarsímtal" - "Kveikir á loftneti…" - "Ekkert samband. Reynir aftur…" - "Getur ekki hringt. %s er ekki neyðarsímanúmer." - "Ekki hægt að hringja. Hringdu í neyðarnúmer" - "Notaðu lyklaborðið til að hringja" - "Setja símtal í bið" - "Halda símtali áfram" - "Leggja á" - "Sýna símatakkaborð" - "Fela símatakkaborð" - "Slökkva á hljóði" - "Kveikja á hljóði" - "Bæta við símtali" - "Sameina símtöl" - "Skipta milli" - "Stjórna símtölum" - "Stjórna símafundi" - "Símafundur" - "Stjórna" - "Hljóð" - "Myndsímtal" - "Breyta í símtal" - "Skipta um myndavél" - "Kveikja á myndavél" - "Slökkva á myndavél" - "Fleiri valkostir" - "Spilari ræstur" - "Spilari stöðvaður" - "Myndavél ekki tilbúin" - "Myndavél tilbúin" - "Óþekkt atvik símtalslotu" - "Þjónusta" - "Uppsetning" - "<Ekki valið>" - "Aðrar símtalsstillingar" - "Hringt í gegnum %s" - "Berst í gegnum %s" - "mynd tengiliðar" - "tala í einrúmi" - "velja tengilið" - "Skrifaðu eigið svar…" - "Hætta við" - "Senda" - "Svara" - "Senda SMS-skilaboð" - "Hafna" - "Svara sem myndsímtali" - "Svara sem símtali" - "Samþykkja beiðni um myndsímtal" - "Hafna beiðni um myndsímtal" - "Samþykkja beiðni um sendingu myndsímtals" - "Hafna beiðni um sendingu myndsímtals" - "Samþykkja beiðni um móttöku myndsímtals" - "Hafna beiðni um móttöku myndsímtals" - "Strjúktu upp til að %s." - "Strjúktu til vinstri til að %s." - "Strjúktu til hægri til að %s." - "Strjúktu niður til að %s." - "Titra" - "Titra" - "Hljóð" - "Sjálfgefið hljóð (%1$s)" - "Hringitónn síma" - "Titra við hringingu" - "Hringitónn og titringur" - "Stjórna símafundi" - "Neyðarnúmer" - "Prófílmynd" - "Slökkt á myndavél" - "úr %s" - "Glósa send" - "Nýleg skilaboð" - "Fyrirtækjaupplýsingar" - %.1f míl. fjarlægð" - %.1f km fjarlægð" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "Opið á morgun frá kl. %s" - "Opið í dag frá kl. %s" - "Lokað kl. %s" - "Var lokað í dag kl. %s" - "Opið núna" - "Lokað núna" - "Grunur um svikasímtal" - "Símtali lokið %1$s" - "Þetta er í fyrsta sinn sem hringt er í þig úr þessu númeri." - "Okkur grunaði að þetta símtal væri úr ruslnúmeri." - "Bannlisti/tilkynna" - "Bæta tengilið við" - "Ekki ruslnúmer" - diff --git a/InCallUI/res/values-it/strings.xml b/InCallUI/res/values-it/strings.xml deleted file mode 100644 index c9b49501b..000000000 --- a/InCallUI/res/values-it/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefono" - "In attesa" - "Sconosciuto" - "Numero privato" - "Telefono pubblico" - "Audioconferenza" - "Chiamata persa" - "Altoparlante" - "Auricolare telefono" - "Auricolare con cavo" - "Bluetooth" - "Inviare i numeri successivi?\n" - "Invio toni\n" - "Invia" - "Sì" - "No" - "Sostituisci carattere jolly con" - "Audioconferenza: %s" - "Numero segreteria" - "Chiamata in corso" - "Ricomposizione" - "Audioconferenza" - "Chiamata in arrivo" - "Chiamata lavoro in arrivo" - "Chiamata terminata" - "In attesa" - "In fase di chiusura" - "Chiamata in corso" - "Il mio numero è: %s" - "Collegamento video" - "Videochiamata" - "Richiesta video in corso" - "Impossibile effettuare una videochiamata" - "Richiesta video rifiutata" - "Numero da richiamare:\n %1$s" - "Numero da richiamare in caso di emergenza:\n %1$s" - "Composizione in corso" - "Chiamata persa" - "Chiamate perse" - "%s chiamate perse" - "Chiamata senza risposta da %s" - "Chiamata in corso" - "Chiamata di lavoro in corso" - "Chiamata Wi-Fi in corso" - "Chiamata di lavoro tramite Wi-Fi in corso" - "In attesa" - "Chiamata in arrivo" - "Chiamata di lavoro in arrivo" - "Chiamata Wi-Fi in arrivo" - "Chiamata di lavoro in arrivo tramite Wi-Fi" - "Videochiamata in arrivo" - "Chiamata di presunto spam in arrivo" - "Richiesta video in arrivo" - "Nuovo messaggio vocale" - "Nuovo messaggio vocale (%d)" - "Componi %s" - "Numero segreteria sconosciuto" - "Nessun servizio" - "Rete selezionata (%s) non disponibile" - "Rispondi" - "Riaggancia" - "Video" - "Voce" - "Accetta" - "Ignora" - "Richiama" - "Messaggio" - "Chiamata in corso su un altro dispositivo" - "Trasferisci chiamata" - "Per fare una chiamata, disattiva la modalità aereo." - "Non registrato sulla rete." - "Rete dati non disponibile." - "Per fare una chiamata, inserisci un numero valido." - "Impossibile chiamare." - "Avvio sequenza MMI..." - "Servizio non supportato." - "Impossibile cambiare chiamata." - "Impossibile separare la chiamata." - "Impossibile trasferire." - "Impossibile fare una chiamata in conferenza." - "Impossibile rifiutare la chiamata." - "Impossibile riagganciare." - "Chiamata SIP" - "Chiamata di emergenza" - "Attivazione segnale cellulare..." - "Nessun servizio. Nuovo tentativo…" - "Impossibile chiamare. %s non è un numero di emergenza." - "Impossibile chiamare. Componi un numero di emergenza." - "Usa tastiera" - "Metti in attesa la chiamata" - "Riprendi chiamata" - "Termina chiamata" - "Mostra tastierino" - "Nascondi tastierino" - "Disattiva audio" - "Riattiva audio" - "Aggiungi chiamata" - "Unisci chiamate" - "Scambia" - "Gestisci chiamate" - "Gestisci audioconferenza" - "Audioconferenza" - "Gestisci" - "Audio" - "Videochiam" - "Passa a chiamata vocale" - "Cambia fotocamera" - "Attiva fotocamera" - "Disattiva fotocamera" - "Altre opzioni" - "Player avviato" - "Player interrotto" - "La fotocamera non è pronta" - "Fotocamera pronta" - "Evento sessione chiamata sconosciuto" - "Servizio" - "Configura" - "<Non impostato>" - "Altre impostazioni di chiamata" - "Chiamate tramite %s" - "In arrivo tramite %s" - "foto contatto" - "Privato" - "seleziona contatto" - "Scrivi risposta personale..." - "Annulla" - "Invia" - "Rispondi" - "Invia SMS" - "Rifiuta" - "Rispondi con videochiamata" - "Rispondi con chiamata audio" - "Accetta richiesta video" - "Rifiuta richiesta video" - "Accetta richiesta di trasmissione video" - "Rifiuta richiesta di trasmissione video" - "Accetta richiesta di ricevimento video" - "Rifiuta richiesta di ricevimento video" - "Scorri verso l\'alto per %s." - "Scorri verso sinistra per %s." - "Scorri verso destra per %s." - "Scorri verso il basso per %s." - "Vibrazione" - "Vibrazione" - "Suono" - "Suono predefinito (%1$s)" - "Suoneria telefono" - "Vibrazione quando squilla" - "Suoneria e vibrazione" - "Gestisci audioconferenza" - "Numero di emergenza" - "Foto del profilo" - "Fotocamera disattivata" - "tramite %s" - "Nota inviata" - "Messaggi recenti" - "Informazioni sull\'attività" - "Distante %.1f mi" - "Distante %.1f km" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "Apre domani alle ore %s" - "Apre oggi alle ore %s" - "Chiude alle ore %s" - "Ha chiuso oggi alle ore %s" - "Aperto ora" - "Ora chiuso" - "Presunto spammer" - "Chiamata terminata %1$s" - "È la prima volta che questo numero ti chiama." - "Sospettavamo che questa chiamata provenisse da uno spammer." - "Blocca/Segnala spam" - "Aggiungi contatto" - "Non spam" - diff --git a/InCallUI/res/values-iw/strings.xml b/InCallUI/res/values-iw/strings.xml deleted file mode 100644 index c75c8da0d..000000000 --- a/InCallUI/res/values-iw/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "טלפון" - "בהמתנה" - "לא ידוע" - "מספר פרטי" - "טלפון ציבורי" - "שיחת ועידה" - "השיחה נותקה" - "רמקול" - "אוזנייה" - "אוזניות עם חיבור חוטי" - "Bluetooth" - "האם לשלוח את הצלילים הבאים?\n" - "שולח צלילים\n" - "שלח" - "כן" - "לא" - "החלף את התו הכללי ב" - "שיחת ועידה %s" - "המספר של הדואר הקולי" - "מחייג" - "מחייג שוב" - "שיחת ועידה" - "שיחה נכנסת" - "שיחת עבודה נכנסת" - "השיחה הסתיימה" - "בהמתנה" - "מנתק" - "בשיחה" - "המספר שלי הוא %s" - "מחבר וידאו" - "שיחת וידאו" - "מבקש וידאו" - "לא ניתן לחבר שיחת וידאו" - "בקשת וידאו נדחתה" - "המספר שלך להתקשרות חזרה\n %1$s" - "המספר שלך להתקשרות חזרה במצב חירום\n %1$s" - "מחייג" - "שיחה שלא נענתה" - "שיחות שלא נענו" - "%s שיחות שלא נענו" - "שיחה שלא נענתה מאת %s" - "שיחה פעילה" - "שיחת עבודה פעילה" - "‏שיחת Wi-Fi פעילה" - "‏שיחת עבודה פעילה ברשת WiFi" - "בהמתנה" - "שיחה נכנסת" - "שיחת עבודה נכנסת" - "‏שיחת Wi-Fi נכנסת" - "‏שיחת עבודה נכנסת ברשת WiFi" - "שיחת וידאו נכנסת" - "השיחה הנכנסת חשודה כספאם" - "בקשת וידאו נכנסת" - "דואר קולי חדש" - "דואר קולי חדש (%d)" - "‏חייג ‎%s‎" - "המספר של הדואר הקולי אינו ידוע" - "אין שירות" - "הרשת שנבחרה (%s) לא זמינה" - "ענה" - "נתק" - "וידאו" - "קול" - "אשר" - "בטל" - "התקשר חזרה" - "שלח הודעה" - "באחד מהמכשירים האחרים מתבצעת שיחה" - "העבר את השיחה" - "כדי להתקשר, כבה תחילה את מצב טיסה." - "לא רשום ברשת." - "רשת סלולרית אינה זמינה." - "כדי להתקשר, הזן מספר טלפון חוקי." - "לא ניתן להתקשר." - "‏מתחיל רצף MMI…" - "שירות לא נתמך." - "לא ניתן לעבור בין שיחות." - "לא ניתן להפריד שיחה." - "לא ניתן להעביר." - "לא ניתן לבצע שיחת ועידה." - "לא ניתן לדחות שיחה." - "לא ניתן להתקשר." - "‏שיחת SIP" - "שיחת חירום" - "מפעיל את הרדיו…" - "אין שירות. מנסה שוב..." - "לא ניתן להתקשר. %s אינו מספר חירום." - "לא ניתן להתקשר. חייג למספר חירום." - "השתמש במקלדת כדי לחייג" - "החזק שיחה" - "המשך בשיחה" - "סיים שיחה" - "הצגת לוח החיוג" - "הסתרת לוח החיוג" - "השתקה" - "ביטול ההשתקה" - "הוסף שיחה" - "מזג שיחות" - "החלף" - "נהל שיחות" - "נהל שיחת ועידה" - "שיחת ועידה" - "ניהול" - "אודיו" - "שיחת וידאו" - "שנה לשיחה קולית" - "החלף מצלמה" - "הפעל את המצלמה" - "כבה את המצלמה" - "אפשרויות נוספות" - "הנגן הופעל" - "הנגן הפסיק" - "המצלמה לא מוכנה" - "המצלמה מוכנה" - "אירוע הפעלת שיחה לא ידוע" - "שירות" - "הגדרות" - "<לא הוגדר>" - "הגדרות אחרות של שיחה" - "שיחה באמצעות %s" - "שיחה נכנסת באמצעות %s" - "תמונה של איש קשר" - "עבור לשיחה פרטית" - "בחר איש קשר" - "כתוב תגובה משלך..." - "בטל" - "שלח" - "ענה" - "‏שלח SMS" - "דחה" - "ענה כשיחת וידאו" - "ענה כשיחת אודיו" - "קבל בקשת וידאו" - "דחה בקשת וידאו" - "אשר את הבקשה לשידור וידאו" - "דחה את הבקשה לשידור וידאו" - "אשר את הבקשה לקבלת וידאו" - "דחה את הבקשה לקבלת וידאו" - "הסט למעלה כדי %s." - "הסט שמאלה כדי %s." - "הסט ימינה כדי %s." - "הסט למטה כדי %s." - "רטט" - "רטט" - "צליל" - "צליל ברירת מחדל (%1$s)" - "רינגטון של טלפון" - "רטט בעת צלצול" - "רינגטון ורטט" - "נהל שיחת ועידה" - "מספר חירום" - "תמונת פרופיל" - "המצלמה כבויה" - "באמצעות %s" - "ההערה נשלחה" - "הודעות אחרונות" - "פרטי עסק" - "במרחק %.1f מייל" - "במרחק %.1f ק\"מ" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "ייפתח מחר ב-%s" - "נפתח היום ב-%s" - "נסגר ב-%s" - "נסגר היום ב-%s" - "פתוח עכשיו" - "סגור עכשיו" - "חשד לשיחת ספאם" - "‏השיחה הסתיימה %1$s" - "זוהי הפעם הראשונה שמתקשרים אליך מהמספר הזה." - "אנו חושדים שהמספר הזה הוא של שולח ספאם." - "חסימה/דיווח על ספאם" - "הוספת איש קשר" - "לא ספאם" - diff --git a/InCallUI/res/values-ja/strings.xml b/InCallUI/res/values-ja/strings.xml deleted file mode 100644 index 551d896b5..000000000 --- a/InCallUI/res/values-ja/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "電話" - "保留中" - "不明" - "非通知" - "公衆電話" - "グループ通話" - "通話が遮断されました" - "スピーカー" - "モバイル端末のイヤホン" - "有線ヘッドセット" - "Bluetooth" - "次の番号を送信しますか?\n" - "番号を送信中\n" - "送信" - "はい" - "いいえ" - "ワイルド文字を置換:" - "グループ通話 %s" - "ボイスメールの番号" - "発信中" - "リダイヤル中" - "グループ通話" - "着信中" - "仕事の通話が着信中" - "通話終了" - "保留中" - "通話終了" - "通話中" - "この電話の番号: %s" - "ビデオハングアウトに接続中" - "ビデオハングアウト" - "ビデオハングアウトをリクエスト中" - "ビデオハングアウトの接続エラー" - "ビデオハングアウトのリクエスト不承認" - "コールバック先\n %1$s" - "緊急通報コールバック先\n %1$s" - "発信中" - "不在着信" - "不在着信" - "不在着信 %s 件" - "%s さんからの不在着信" - "通話中" - "仕事の通話中" - "Wi-Fi 通話中" - "仕事の Wi-Fi 通話中" - "保留中" - "着信中" - "仕事の通話が着信中" - "Wi-Fi 通話が着信中" - "仕事の Wi-Fi 通話が着信中" - "ビデオハングアウトが着信中" - "迷惑電話の疑いがある通話を着信しています" - "ビデオハングアウト リクエストが着信中" - "新着のボイスメール" - "新着のボイスメール(%d 件)" - "%s に発信" - "ボイスメールの番号が不明です" - "通信サービスはありません" - "選択したネットワーク(%s)が利用できません" - "電話に出る" - "通話終了" - "ビデオ" - "音声" - "受ける" - "拒否する" - "コールバック" - "メッセージ" - "別の端末で通話中" - "通話を転送" - "機内モードを OFF にしてから発信してください。" - "ご加入の通信サービスがありません。" - "モバイル ネットワークが利用できません。" - "発信するには、有効な番号を入力してください。" - "発信できません。" - "MMI シーケンスを開始しています..." - "サービスはサポートされていません。" - "通話を切り替えられません。" - "通話を分割できません。" - "転送できません。" - "グループ通話できません。" - "着信を拒否できません。" - "通話を解放できません。" - "SIP 通話" - "緊急通報" - "無線通信を ON にしています..." - "通信サービスはありません。もう一度お試しください…" - "発信できません。%s は緊急通報番号ではありません。" - "発信できません。緊急通報番号におかけください。" - "キーボードで番号を入力してください" - "通話を保留" - "通話を再開" - "通話を終了" - "ダイヤルパッドを表示" - "ダイヤルパッドを非表示" - "ミュート" - "ミュートを解除" - "通話を追加" - "グループ通話" - "切り替え" - "通話を管理" - "グループ通話オプション" - "グループ通話" - "管理" - "音声" - "ビデオ" - "音声通話に変更" - "カメラを切り替え" - "カメラを ON にする" - "カメラを OFF にする" - "その他のオプション" - "プレーヤーを開始しました" - "プレーヤーを停止しました" - "カメラが準備できていません" - "カメラが準備できました" - "不明な通話セッション イベントです" - "サービス" - "セットアップ" - "<未設定>" - "その他の通話設定" - "%s で発信中" - "%s で着信中" - "連絡先の写真" - "個別通話に切り替え" - "連絡先を選択" - "カスタム返信を作成..." - "キャンセル" - "送信" - "電話に出る" - "SMS を送信する" - "拒否" - "ビデオハングアウトで電話に出る" - "音声通話で電話に出る" - "ビデオハングアウト リクエストを承認する" - "ビデオハングアウト リクエストを拒否する" - "ビデオハングアウト送信リクエストを承認する" - "ビデオハングアウト送信リクエストを拒否する" - "ビデオハングアウト受信リクエストを承認する" - "ビデオハングアウト受信リクエストを拒否する" - "上にスライドして%sを行います。" - "左にスライドして%sを行います。" - "右にスライドして%sを行います。" - "下にスライドして%sを行います。" - "バイブレーション" - "バイブレーション" - "着信音" - "デフォルトの通知音(%1$s)" - "着信音" - "着信時のバイブレーション" - "着信音とバイブレーション" - "グループ通話オプション" - "緊急通報番号" - "プロフィール写真" - "カメラ OFF" - "%s に着信" - "メモを送信しました" - "最近のメッセージ" - "ビジネス情報" - "%.1f マイル内" - "%.1f km 内" - "%1$s%2$s" - "%1$s%2$s" - "%1$s%2$s" - "明日 %sに営業開始" - "本日 %sに営業開始" - "%sに営業終了" - "本日 %sに営業終了" - "現在営業中" - "営業終了" - "迷惑電話の疑いあり" - "通話が終了しました %1$s" - "この番号からの通話を受信したのはこれが初めてです。" - "この通話は迷惑電話の可能性があります。" - "ブロック / 報告" - "連絡先に追加" - "迷惑電話ではない" - diff --git a/InCallUI/res/values-ka/strings.xml b/InCallUI/res/values-ka/strings.xml deleted file mode 100644 index b01006561..000000000 --- a/InCallUI/res/values-ka/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "ტელეფონი" - "მოცდის რეჟიმში" - "უცნობი" - "დაფარული ნომერი" - "ტელეფონ-ავტომატი" - "საკონფერენციო ზარი" - "ზარი შეწყდა" - "დინამიკი" - "ყურსაცვამის საყურისი" - "სადენიანი ყურსაცვამი" - "Bluetooth" - "გსურთ შემდეგი ტონების გაგზავნა?\n" - "ტონების გაგზავნა\n" - "გაგზავნა" - "დიახ" - "არა" - "ჩანაცვლების სიმბოლო ჩანაცვლდეს შემდეგით:" - "საკონფერენციო ზარი: %s" - "ხმოვანი ფოსტის ნომერი" - "მიმდინარეობს აკრეფა" - "იკრიფება ხელახლა" - "საკონფერენციო ზარი" - "შემომავალი ზარი" - "შემომავალი ზარი (სამსახური)" - "ზარი დასრულდა" - "მოცდის რეჟიმში" - "მიმდინარეობს გათიშვა" - "საუბრის რეჟიმში" - "ჩემი ნომერია %s" - "მიმდინარეობს ვიდეოს დაკავშირება" - "ვიდეო ზარი" - "მიმდინარეობს ვიდეოს მოთხოვნა" - "ვიდეო ზარის დაკავშირება ვერ მოხერხდა" - "ვიდეოს მოთხოვნა უარყოფილია" - "თქვენი ნომერი გადმორეკვისთვის\n %1$s" - "თქვენი ნომერი გადაუდებელი გადმორეკვისთვის\n %1$s" - "მიმდინარეობს აკრეფა" - "გამოტოვებული ზარი" - "გამოტოვებული ზარები" - "%s გამოტოვებული ზარი" - "გამოტოვებული ზარი %s-ისგან" - "მიმდინარე ზარი" - "მიმდინარე ზარი (სამსახური)" - "მიმდინარე Wi-Fi ზარი" - "მიმდინარე Wi-Fi ზარი (სამსახური)" - "მოცდის რეჟიმში" - "შემომავალი ზარი" - "შემომავალი ზარი (სამსახური)" - "შემომავალი Wi-Fi ზარი" - "შემომავალი Wi-Fi ზარი (სამსახური)" - "შემომავალი ვიდეო ზარი" - "შემომავალი ზარი - სავარაუდოდ სპამი" - "შემომავალი ვიდეოს მოთხოვნა" - "ახალი ხმოვანი შეტყობინება" - "ახალი ხმოვანი შეტყობინება (%d)" - "%s-ზე დარეკვა" - "ხმოვანი ფოსტის ნომერი უცნობია" - "სერვისი არ არის" - "არჩეული ქსელი (%s) მიუწვდომელია" - "პასუხი" - "გათიშვა" - "ვიდეო" - "ხმოვანი" - "მიღება" - "დახურვა" - "გადარეკვა" - "შეტყობინება" - "სხვა მოწყობილობაზე მიმდინარე ზარი" - "ზარის ტრანსფერი" - "ზარის განსახორციელებლად, ჯერ გამორთეთ თვითმფრინავის რეჟიმი." - "არ არის რეგისტრირებული ქსელში." - "ფიჭური ქსელი მიუწვდომელია." - "ზარის განსახორციელებლად, შეიყვანეთ სწორი ნომერი." - "დარეკვა ვერ ხერხდება." - "MMI თანმიმდევრობის დაწყება…" - "სერვისი არ არის მხარდაჭერილი." - "ზარების გადართვა ვერ ხერხდება." - "ზარის განცალკევება ვერ ხერხდება." - "გადამისამართება ვერ ხერხდება." - "საკონფერენციო ზარის განხორციელება ვერ ხერხდება." - "ზარის უარყოფა ვერ ხერხდება." - "ზარ(ებ)ის გათიშვა ვერ ხერხდება." - "SIP ზარი" - "გადაუდებელი ზარი" - "მიმდინარეობს რადიოს ჩართვა…" - "სერვისი არ არის. მიმდინარეობს ხელახლა ცდა…" - "დარეკვა ვერ ხერხდება. %s არ არის გადაუდებელი დახმარების ნომერი." - "დარეკვა ვერ ხერხდება. აკრიფეთ გადაუდებელი დახმარების ნომერი." - "ნომრის ასაკრეფად გამოიყენეთ კლავიატურა" - "მოცდის რეჟიმზე გადაყვანა" - "ზარის განახლება" - "ზარის დასრულება" - "ციფერბლატის ჩვენება" - "ციფერბლატის დამალვა" - "დადუმება" - "დადუმების გაუქმება" - "ზარის დამატება" - "ზარების გაერთიანება" - "ჩანაცვლება" - "ზარების მართვა" - "საკონფერენციო ზარის მართვა" - "საკონფერენციო ზარი" - "მართვა" - "აუდიო" - "ვიდეო ზარი" - "ხმოვან ზარზე გადართვა" - "კამერის გადართვა" - "კამერის ჩართვა" - "კამერის გამორთვა" - "სხვა ვარიანტები" - "დამკვრელი ჩაირთო" - "დამკვრელი გამოირთო" - "კამერა არ არის მზად" - "კამერა მზადაა" - "ზარის სესიის უცნობი მოვლენა" - "სერვისი" - "დაყენება" - "<არ არის დაყენებული>" - "ზარის სხვა პარამეტრები" - "მიმდინარეობს დარეკვა %s-ის მეშვეობით" - "შემომავალი ზარი %s-დან" - "კონტაქტის ფოტო" - "პირადი რეჟიმი" - "კონტაქტის არჩევა" - "საკუთარის შექმნა..." - "გაუქმება" - "გაგზავნა" - "პასუხი" - "SMS-ის გაგზავნა" - "უარყოფა" - "პასუხი ვიდეო ზარის სახით" - "პასუხი ხმოვანი ზარის სახით" - "ვიდეოს მოთხოვნის მიღება" - "ვიდეოს მოთხოვნის უარყოფა" - "ვიდეოს გადაცემის მოთხოვნის მიღება" - "ვიდეოს გადაცემის მოთხოვნის უარყოფა" - "ვიდეოს მიღების მოთხოვნაზე დათანხმება" - "ვიდეოს მიღების მოთხოვნის უარყოფა" - "გაასრიალეთ ზემოთ, რათა შესრულდეს %s." - "გაასრიალეთ მარცხნივ, რათა შესრულდეს %s." - "გაასრიალეთ მარჯვნივ, რათა შესრულდეს %s." - "გაასრიალეთ ქვემოთ, რათა შესრულდეს %s." - "ვიბრაცია" - "ვიბრაცია" - "ხმა" - "ნაგულისხმები ხმა (%1$s)" - "ტელეფონის ზარი" - "ვიბრაცია დარეკვისას" - "ზარის მელოდია და ვიბრაცია" - "საკონფერენციო ზარის მართვა" - "გადაუდებელი დახმარების ნომერი" - "პროფილის ფოტო" - "კამერა გამორთულია" - "%s-დან" - "შენიშვნა გაიგზავნა" - "ბოლო შეტყობინებები" - "ბიზნეს-ინფორმაცია" - "%.1f მილში" - "%.1f კმ-ში" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "იხსნება ხვალ %s-ზე" - "იხსნება დღეს %s-ზე" - "იკეტება %s-ზე" - "დაიკეტა დღეს %s-ზე" - "ახლა ღიაა" - "ახლა დაკეტილია" - "სავარაუდ.სპამ.აბონ." - "ზარი დასრულდა %1$s" - "ამ ნომრიდან პირველად დაგირეკეს." - "ეჭვი გვაქვს, რომ ეს ზარი სპამია." - "დაბლოკ./სპამ.შეტყობ." - "კონტაქტის დამატება" - "არ არის სპამი" - diff --git a/InCallUI/res/values-kk/strings.xml b/InCallUI/res/values-kk/strings.xml deleted file mode 100644 index 07ea6fb78..000000000 --- a/InCallUI/res/values-kk/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Телефон" - "Күтуде" - "Белгісіз" - "Жеке нөмір" - "Автомат-телефон" - "Конференциялық қоңырау" - "Қоңырау үзілді" - "Динамик" - "Телефон құлаққабы" - "Сымды құлақаспап жинағы" - "Bluetooth" - "Келесі әуендер жіберілсін бе?\n" - "Жіберу әуендері\n" - "Жіберу" - "Иә" - "Жоқ" - "Қойылмалы таңбаны келесі таңбамен алмастыру" - "%s конференциялық қоңырауы" - "Дауыстық пошта нөмірі" - "Терілуде" - "Қайта терілуде" - "Конференциялық қоңырау" - "Кіріс қоңырау" - "Кіріс жұмыс қоңырауы" - "Қоңырау аяқталды" - "Күтуде" - "Қоңырау аяқталуда" - "Қоңырауда" - "Mенің нөмірім — %s" - "Бейне қосылуда" - "Бейне қоңырау" - "Бейне сұралуда" - "Бейне қоңырауға қосылу мүмкін емес" - "Бейне сұрауы қабылданбады" - "Кері қоңырау шалу нөміріңіз\n %1$s" - "Төтенше кері қоңырау шалу нөміріңіз\n %1$s" - "Терілуде" - "Өткізіп алған қоңырау" - "Өткізіп алған қоңыраулар" - "%s өткізіп алған қоңырау" - "%s қоңырауы өткізіп алынған" - "Ағымдағы қоңырау" - "Ағымдағы жұмыс қоңырауы" - "Ағымдағы Wi-Fi қоңырауы" - "Ағымдағы Wi-Fi жұмыс қоңырауы" - "Күтуде" - "Кіріс қоңырау" - "Кіріс жұмыс қоңырауы" - "Кіріс Wi-Fi қоңырауы" - "Кіріс Wi-Fi жұмыс қоңырауы" - "Кіріс бейне қоңырау" - "Кіріс қоңырауы спам болуы мүмкін" - "Кіріс бейне сұрау" - "Жаңа дауыстық хабар" - "Жаңа дауыстық хабар (%d)" - "%s нөмірін теру" - "Дауыстық пошта нөмірі белгісіз" - "Қызмет жоқ" - "Таңдалған (%s) желісі қол жетімді емес" - "Жауап" - "Қоңырауды аяқтау" - "Бейне" - "Дауыс" - "Қабылдау" - "Қабылдамау" - "Кері қоңырау шалу" - "Хабар" - "Қоңырау басқа құрылғыдан шалынуда" - "Қоңырауды басқа құрылғыға бағыттау" - "Қоңырау шалу үшін алдымен ұшақ режимін өшіріңіз." - "Желіде тіркелмеген." - "Ұялы желі қол жетімді емес." - "Қоңырау шалу үшін жарамды нөмірді енгізіңіз." - "Қоңырау шалу мүмкін емес." - "MMI қатарын бастау…" - "Қызметке қолдау көрсетілмейді." - "Қоңырауларды ауыстыру мүмкін емес." - "Қоңырауды бөлу мүмкін емес." - "Тасымалдау мүмкін емес." - "Конференция мүмкін емес." - "Қоңырауды қабылдамау мүмкін емес." - "Қоңырау(лар)ды босату мүмкін емес." - "SIP қоңырауы" - "Төтенше қоңырау" - "Радио қосылуда…" - "Қызмет жоқ. Әрекет қайталануда…" - "Қоңырау шалу мүмкін емес. %s төтенше нөмір емес." - "Қоңырау шалу мүмкін емес. Төтенше нөмірді теріңіз." - "Теру үшін пернетақтаны пайдалану" - "Қоңырауды ұстап тұру" - "Қоңырауды жалғастыру" - "Қоңырауды аяқтау" - "Теру тақтасын көрсету" - "Теру тақтасын жасыру" - "Дыбысты өшіру" - "Дыбысын қосу" - "Қоңырау қосу" - "Қоңырауларды біріктіру" - "Алмастыру" - "Қоңырауларды басқару" - "Конференциялық қоңырауды басқару" - "Конференциялық қоңырау" - "Басқару" - "Aудио" - "Бейне қоңырау" - "Дауыстық қоңырауға өзгерту" - "Камераны ауыстыру" - "Камераны қосу" - "Камераны өшіру" - "Қосымша опциялар" - "Ойнатқыш іске қосылды" - "Ойнатқыш тоқтатылды" - "Камера дайын емес" - "Камера дайын" - "Белгісіз қоңырау сеансы оқиғасы" - "Қызмет" - "Реттеу" - "<Орнатылмаған>" - "Басқа қоңырау параметрлері" - "%s арқылы қоңырау шалу" - "%s арқылы кіріс" - "контакт фотосуреті" - "жеке қоңырауға ауысу" - "контакт таңдау" - "Өзіңіздікін жазыңыз..." - "Бас тарту" - "Жіберу" - "Жауап" - "SMS жіберу" - "Қабылдамау" - "Бейне қоңырауға жауап беру" - "Аудио қоңырауға жауап беру" - "Бейне сұрауды қабылдау" - "Бейне сұрауды қабылдамау" - "Бейне тасымалдау сұрауын қабылдау" - "Бейне тасымалдау сұрауын қабылдамау" - "Бейне алу сұрауын қабылдау" - "Бейне алу сұрауын қабылдамау" - "%s үшін жоғары сырғытыңыз." - "%s үшін сол жаққа сырғытыңыз." - "%s үшін оң жаққа сырғытыңыз." - "%s үшін төмен сырғытыңыз." - "Діріл" - "Діріл" - "Дыбыс" - "Әдепкі дыбыс (%1$s)" - "Телефонның қоңырау әуені" - "Шылдырлағанда дірілдеу" - "Қоңырау әуені және діріл" - "Конференциялық қоңырауды басқару" - "Төтенше нөмір" - "Профиль фотосуреті" - "Камераны өшіру" - "%s арқылы" - "Ескертпе жіберілді" - "Жақындағы хабарлар" - "Іскери ақпарат" - "%.1f миля қашықтықта" - "%.1f км қашықтықта" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "Ертең %s уақытында ашылады" - "Бүгін %s уақытында ашылады" - "%s уақытында жабылады" - "Бүгін %s уақытында жабық" - "Қазір ашық" - "Қазір жабық" - "Спам қоңырау шалушы болуы мүмкін" - "Қоңырау аяқталды (%1$s)" - "Бұл нөмірдің сізге алғашқы қоңырау шалуы." - "Бұл қоңырау спамер деп күдіктенеміз." - "Бөгеу/спамға жіберу" - "Контакт қосу" - "Спам емес" - diff --git a/InCallUI/res/values-km/strings.xml b/InCallUI/res/values-km/strings.xml deleted file mode 100644 index 7a31d8933..000000000 --- a/InCallUI/res/values-km/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "ទូរស័ព្ទ" - "រង់ចាំ" - "មិនស្គាល់" - "លេខ​ឯកជន" - "ទូរស័ព្ទសាធារណៈ" - "ការហៅជាក្រុម" - "ការហៅទូរស័ព្ទបានដាក់ចុះ" - "ឧបករណ៍បំពងសម្លេង" - "អូប៉ាល័រសំឡេងទូរស័ព្ទ" - "កាសមានខ្សែ" - "ប៊្លូធូស" - "ផ្ញើសំឡេងដូចខាងក្រោមឬ?\n" - "ផ្ញើ​សំឡេង \n" - "ផ្ញើ" - "បាទ/ចាស" - "ទេ" - "ជំនួស​តួ​អក្សរ​ជំនួស​ដោយ" - "ការហៅជាក្រុម %s" - "លេខ​សារ​ជា​សំឡេង" - "កំពុងហៅ" - "ការចុចហៅឡើងវិញ" - "ការហៅជាក្រុម" - "ការហៅចូល" - "កំពុងហៅចូលពីកន្លែងការងារ" - "បាន​បញ្ចប់​ការ​ហៅ" - "រង់ចាំ" - "បញ្ចប់​ការ​សន្ទនា" - "កំពុង​ហៅ" - "លេខ​របស់​ខ្ញុំ​គឺ %s" - "ភ្ជាប់​វីដេអូ" - "ហៅជាវីដេអូ" - "ស្នើ​វីដេអូ" - "មិនអាចភ្ជាប់ការហៅជាវីដេអូបានទេ" - "បានបដិសេធសំណើហៅជាវីដេអូ" - "លេខហៅទៅវិញរបស់អ្នក\n%1$s" - "លេខហៅទៅវិញពេលអាសន្នរបស់អ្នក\n %1$s" - "កំពុង​ហៅ" - "ខកខាន​ទទួល" - "ខកខាន​ទទួល" - "ខកខានទទួល %s ដង" - "ខកខាន​ទទួល​ពី %s" - "កំពុង​បន្ត​ការ​ហៅ" - "ការហៅពីកន្លែងការងារកំពុងដំណើរការ" - "ការហៅតាម Wi-Fi កំពុងបន្ត" - "ការហៅតាម Wi-Fi ពីកន្លែងការងារកំពុងដំណើរការ" - "រង់ចាំ" - "ការហៅចូល" - "កំពុងហៅចូលពីកន្លែងការងារ" - "មានការហៅចូលតាម Wi-Fi" - "កំពុងហៅចូលពីកន្លែងការងារតាម Wi-Fi" - "ការ​ហៅចូលជា​វីដេអូ​" - "ការ​ហៅ​បន្លំ​​ចូល​​​ដែល​សង្ស័យ" - "សំណើ​ការ​ហៅ​ជា​វីដេអូ​ចូល" - "សារ​ជា​សំឡេង​ថ្មី" - "សារ​ជា​សំឡេង​ថ្មី (%d)" - "ហៅ %s" - "លេខសារជាសំឡេងមិនស្គាល់" - "គ្មានសេវាទេ" - "បណ្ដាញ​ដែល​បាន​ជ្រើស ( %s ) មិន​អាច​ប្រើ​បាន​ទេ" - "ឆ្លើយតប" - "បញ្ចប់​ការ​សន្ទនា" - "វីដេអូ" - "សំឡេង" - "ព្រម​ទទួល" - "បដិសេធ" - "ហៅ​ទៅ​វិញ" - "សារ" - "ការ​ហៅ​កំពុង​ដំណើរការ​លើ​ឧបករណ៍​ផ្សេង" - "ផ្ទេរ​ការហៅ" - "ដើម្បីកំណត់ការហៅ សូមបិទរបៀបពេលជិះយន្តហោះជាមុនសិន" - "មិនបានចុះឈ្មោះនៅលើបណ្ដាញទេ" - "បណ្ដាញចល័តមិនអាចប្រើបានទេ" - "ដើម្បីធ្វើការហៅ សូមបញ្ចូលលេខដែលត្រឹមត្រូវ" - "មិនអាចហៅបានទេ" - "កំពុងចាប់ផ្តើមលំដាប់ MMI…" - "សេវាកម្មមិនត្រូវបានគាំទ្រទេ" - "មិនអាចប្តូរការហៅបានទេ" - "មិនអាចបំបែកការហៅបានទេ" - "មិនអាចផ្ទេរបានទេ" - "មិនអាចធ្វើការហៅជាក្រុមបានទេ" - "មិនអាចបដិសេធការហៅបានទេ" - "មិនអាចធ្វើការហៅបានទេ" - "ការ​ហៅ SIP" - "ការ​ហៅ​ពេល​អាសន្ន" - "កំពុងបើកវិទ្យុ…" - "គ្មានសេវាទេ សូមព្យាយាមម្តង…" - "មិនអាចហៅបានទេ។ %s មិនមែនជាលេខអាសន្នទេ" - "មិនអាចហៅបានទេ សូមចុចហៅលេខអាសន្ន" - "ប្រើ​ក្ដារ​ចុច ​ដើម្បី​ចុច​លេខ" - "រង់ចាំការហៅ" - "បន្តការហៅ" - "បញ្ចប់ការហៅ" - "បង្ហាញ​បន្ទះ​លេខ" - "លាក់​បន្ទះ​លេខ" - "បិទ" - "បើក​សំឡេង" - "បន្ថែម​ការ​ហៅ" - "បញ្ចូល​ការ​ហៅ​ចូល​គ្នា" - "ប្ដូរ" - "គ្រប់គ្រង​ការ​ហៅ" - "គ្រប់គ្រងការហៅជាក្រុម" - "ការហៅជា​សន្និសិទ" - "គ្រប់គ្រង" - "សំឡេង" - "ហៅជាវីដេអូ" - "ប្ដូរ​ទៅ​ការ​ហៅ​ជា​សំឡេង" - "ប្ដូរកាមេរ៉ា" - "បើកកាមេរ៉ា" - "បិទកាមេរ៉ា" - "ជម្រើសច្រើនទៀត" - "អ្នកលេងបានចាប់ផ្តើម" - "អ្នកលេងបានឈប់" - "កាមេរ៉ាមិនទាន់ត្រៀមរួចរាល់ទេ" - "កាមេរ៉ាត្រៀមរួចរាល់ហើយ" - "ព្រឹត្តិការណ៍វេននៃការហៅមិនស្គាល់" - "សេវាកម្ម" - "ដំឡើង" - "<មិន​បាន​កំណត់>" - "​កំណត់​ការ​​ហៅ​ផ្សេងទៀត" - "ហៅតាមរយៈ %s" - "ចូល​តាមរយៈ %s" - "រូបថត​ទំនាក់ទំនង" - "ចូលជាលក្ខណៈឯកជន" - "ជ្រើសទំនាក់ទំនង" - "សរសេរដោយខ្លួនអ្នកផ្ទាល់..." - "បោះបង់" - "ផ្ញើ" - "ឆ្លើយតប" - "ផ្ញើសារ SMS" - "បដិសេធ" - "ឆ្លើយតប​ជា​ការ​ហៅ​ជា​​វីដេអូ" - "ឆ្លើយតប​ជា​ការ​ហៅ​ជា​សំឡេង" - "ទទួលយក​សំណើ​វីដេអូ" - "ទទួលយក​សំណើ​វីដេអូ" - "ទទួលយកសំណើបញ្ជូនជាវីដេអូ" - "បដិសេធសំណើបញ្ជូនជាវីដេអូ" - "ទទួលយកសំណើទទួលជាវីដេអូ" - "បដិសេធសំណើទទួលជាវីដេអូ" - "រុញឡើងលើដើម្បី %s" - "រុញទៅឆ្វេងដើម្បី %s" - "រុញទៅស្ដាំដើម្បី %s" - "រុញចុះក្រោមដើម្បី %s" - "ញ័រ" - "ញ័រ" - "សំឡេង" - "សំឡេង​លំនាំដើម (%1$s)" - "សំឡេងរោទ៍ទូរស័ព្ទ" - "ញ័រពេលរោទ៍" - "សំឡេងរោទ៍ និងញ័រ" - "គ្រប់គ្រងការហៅជាក្រុម" - "លេខអាសន្ន" - "រូបថត​ប្រវត្តិរូប" - "បិទកាមេរ៉ា" - "តាមរយៈ %s" - "បានផ្ញើចំណាំ" - "សារថ្មីៗ" - "ព័ត៌មានធុរកិច្ច" - "ចម្ងាយ %.1f ម៉ាយល៍" - "ចម្ងាយ %.1f គម" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "បើកថ្ងៃស្អែកនៅម៉ោង %s" - "បើកថ្ងៃនេះនៅម៉ោង %s" - "បិទនៅម៉ោង %s" - "បានបិទថ្ងៃនេះនៅម៉ោង %s" - "បើកឥឡូវនេះ" - "បិទឥឡូវនេះ" - "អ្នក​ហៅ​​បន្លំ​ដែល​សង្ស័យ" - "ការ​ហៅ​បាន​បញ្ចប់ %1$s" - "នេះ​គឺ​ជា​លើក​ដំបូង​ដែល​លេខ​នេះ​បាន​ហៅ​មក​អ្នក។" - "យើង​បាន​សង្ស័យ​ថា​​ការ​ហៅ​នេះ​ជា​សារ​ឥត​បាន​ការ។" - "រារាំង/រាយការណ៍សារឥតបានការ" - "បញ្ចូល​​ទំនាក់ទំនង" - "មិនមែន​សារ​ឥតបានការ" - diff --git a/InCallUI/res/values-kn/strings.xml b/InCallUI/res/values-kn/strings.xml deleted file mode 100644 index 441f6e189..000000000 --- a/InCallUI/res/values-kn/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "ಫೋನ್" - "ತಡೆಹಿಡಿಯಲಾಗಿದೆ" - "ಅಪರಿಚಿತ" - "ಖಾಸಗಿ ಸಂಖ್ಯೆ" - "ಪೇಫೋನ್" - "ಕಾನ್ಫರೆನ್ಸ್ ಕರೆ" - "ಕರೆಯನ್ನು ಬಿಡಲಾಗಿದೆ" - "ಸ್ಪೀಕರ್‌" - "ಹ್ಯಾಂಡ್‌ಸೆಟ್ ಇಯರ್‌ಪೀಸ್" - "ವೈರ್ಡ್ ಹೆಡ್‌ಸೆಟ್‌" - "ಬ್ಲೂಟೂತ್" - "ಕೆಳಗಿನ ಟೋನ್‌ಗಳನ್ನು ಕಳುಹಿಸುವುದೇ?\n" - "ಟೋನ್‌ಗಳನ್ನು ಕಳುಹಿಸಲಾಗುತ್ತಿದೆ\n" - "ಕಳುಹಿಸು" - "ಹೌದು" - "ಇಲ್ಲ" - "ಇದರೊಂದಿಗೆ ವಿಶೇಷ ಅಕ್ಷರಗಳನ್ನು ಸ್ಥಳಾಂತರಿಸು" - "ಕಾನ್ಫರೆನ್ಸ್ ಕರೆ %s" - "ಧ್ವನಿಮೇಲ್‌ ಸಂಖ್ಯೆ" - "ಡಯಲ್‌ ಮಾಡಲಾಗುತ್ತಿದೆ" - "ಮರು ಡಯಲ್ ಮಾಡಲಾಗುತ್ತಿದೆ" - "ಕಾನ್ಫರೆನ್ಸ್ ಕರೆ" - "ಒಳಬರುವ ಕರೆ" - "ಒಳಬರುವ ಕೆಲಸದ ಕರೆ" - "ಕರೆ ಅಂತ್ಯಗೊಂಡಿದೆ" - "ತಡೆಹಿಡಿಯಲಾಗಿದೆ" - "ಹ್ಯಾಂಗ್ ಮಾಡಲಾಗುತ್ತಿದೆ" - "ಕರೆಯಲ್ಲಿ" - "ನನ್ನ ಸಂಖ್ಯೆ %s" - "ವೀಡಿಯೊ ಸಂಪರ್ಕಪಡಿಸಲಾಗುತ್ತಿದೆ" - "ವೀಡಿಯೊ ಕರೆ" - "ವೀಡಿಯೊ ವಿನಂತಿಸಲಾಗುತ್ತಿದೆ" - "ವೀಡಿಯೊ ಕರೆಯನ್ನು ಸಂಪರ್ಕಪಡಿಸಲಾಗುವುದಿಲ್ಲ" - "ವೀಡಿಯೊ ವಿನಂತಿಯನ್ನು ತಿರಸ್ಕರಿಸಲಾಗಿದೆ" - "ನಿಮ್ಮ ಮರಳಿಕರೆ ಮಾಡುವ ಸಂಖ್ಯೆ\n %1$s" - "ನಿಮ್ಮ ತುರ್ತು ಮರಳಿಕರೆ ಮಾಡುವ ಸಂಖ್ಯೆ\n %1$s" - "ಡಯಲ್‌ ಮಾಡಲಾಗುತ್ತಿದೆ" - "ಮಿಸ್ಡ್‌ ಕಾಲ್‌" - "ಮಿಸ್ಡ್ ಕಾಲ್‌ಗಳು" - "%s ಮಿಸ್ಡ್ ಕಾಲ್‌ಗಳು" - "%s ಅವರಿಂದ ಮಿಸ್ಡ್ ಕಾಲ್" - "ಚಾಲ್ತಿಯಲ್ಲಿರುವ ಕರೆ" - "ಚಾಲ್ತಿಯಲ್ಲಿರುವ ಕೆಲಸದ ಕರೆ" - "ಚಾಲ್ತಿಯಲ್ಲಿರುವ ವೈ-ಫೈ ಕರೆ" - "ಚಾಲ್ತಿಯಲ್ಲಿರುವ ವೈ-ಫೈ ಕೆಲಸದ ಕರೆ" - "ತಡೆಹಿಡಿಯಲಾಗಿದೆ" - "ಒಳಬರುವ ಕರೆ" - "ಒಳಬರುವ ಕೆಲಸದ ಕರೆ" - "ಒಳಬರುವ ವೈ-ಫೈ ಕರೆ" - "ಒಳಬರುವ ವೈ-ಫೈ ಕೆಲಸದ ಕರೆ" - "ಒಳಬರುವ ವೀಡಿಯೊ ಕರೆ" - "ಒಳಬರುವ ಶಂಕಿತ ಸ್ಪ್ಯಾಮ್ ಕರೆ" - "ಒಳಬರುವ ವೀಡಿಯೊ ವಿನಂತಿ" - "ಹೊಸ ಧ್ವನಿಮೇಲ್‌" - "ಹೊಸ ಧ್ವನಿಮೇಲ್‌‌ (%d)" - "%s ಗೆ ಡಯಲ್‌‌ ಮಾಡು" - "ಅಪರಿಚಿತ ಧ್ವನಿಮೇಲ್‌ ಸಂಖ್ಯೆ" - "ಸೇವೆ ಇಲ್ಲ" - "ಆಯ್ಕೆಮಾಡಿದ (%s) ನೆಟ್‌ವರ್ಕ್‌ ಲಭ್ಯವಿಲ್ಲ" - "ಉತ್ತರ" - "ಹ್ಯಾಂಗ್ ಅಪ್" - "ವೀಡಿಯೊ" - "ಧ್ವನಿ" - "ಸಮ್ಮತಿಸು" - "ವಜಾಗೊಳಿಸಿ" - "ಮರಳಿ ಕರೆ" - "ಸಂದೇಶ" - "ಮತ್ತೊಂದು ಸಾಧನದಲ್ಲಿ ಚಾಲ್ತಿಯಲ್ಲಿರುವ ಕರೆ" - "ಕರೆ ವರ್ಗಾಯಿಸಿ" - "ಕರೆ ಮಾಡಲು, ಮೊದಲು ಏರ್‌ಪ್ಲೇನ್‌‌ ಮೋಡ್‌‌ ಆಫ್‌ ಮಾಡಿ." - "ನೆಟ್‌ವರ್ಕ್‌ನಲ್ಲಿ ಇನ್ನೂ ನೋಂದಣಿಯಾಗಿಲ್ಲ." - "ಸೆಲ್ಯುಲಾರ್ ನೆಟ್‌ವರ್ಕ್‌ ಲಭ್ಯವಿಲ್ಲ." - "ಕರೆಯನ್ನು ಮಾಡಲು, ಮಾನ್ಯವಾದ ಸಂಖ್ಯೆಯನ್ನು ನಮೂದಿಸಿ." - "ಕರೆ ಮಾಡಲು ಸಾಧ್ಯವಿಲ್ಲ." - "MMI ಅನುಕ್ರಮ ಪ್ರಾರಂಭವಾಗುತ್ತಿದೆ…" - "ಸೇವೆ ಬೆಂಬಲಿತವಾಗಿಲ್ಲ." - "ಕರೆಗಳನ್ನು ಬದಲಾಯಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ." - "ಕರೆಯನ್ನು ಪ್ರತ್ಯೇಕಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ." - "ವರ್ಗಾಯಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ." - "ಕಾನ್ಫರೆನ್ಸ್ ಮಾಡಲು ಸಾಧ್ಯವಿಲ್ಲ." - "ಕರೆ ತಿರಸ್ಕರಿಸಲಾಗುವುದಿಲ್ಲ." - "ಕರೆ(ಗಳು) ಬಿಡುಗಡೆ ಮಾಡಲು ಸಾಧ್ಯವಿಲ್ಲ." - "SIP ಕರೆ" - "ತುರ್ತು ಕರೆ" - "ರೇಡಿಯೋ ಆನ್‌ ಮಾಡಲಾಗುತ್ತಿದೆ…" - "ಯಾವುದೇ ಸೇವೆ ಇಲ್ಲ. ಮತ್ತೆ ಪ್ರಯತ್ನಿಸಲಾಗುತ್ತಿದೆ..." - "ಕರೆ ಮಾಡಲು ಸಾಧ್ಯವಿಲ್ಲ. %s ತುರ್ತು ಸಂಖ್ಯೆಯಲ್ಲ." - "ಕರೆ ಮಾಡಲು ಸಾಧ್ಯವಿಲ್ಲ. ತುರ್ತು ಸಂಖ್ಯೆಯನ್ನು ಡಯಲ್ ಮಾಡಿ." - "ಡಯಲ್‌ ಮಾಡಲು ಕೀಬೋರ್ಡ್‌ ಬಳಸಿ" - "ಕರೆಯನ್ನು ಹೋಲ್ಡ್‌‌ ಮಾಡು" - "ಕರೆಯನ್ನು ಮುಂದುವರಿಸಿ" - "ಕರೆ ಅಂತ್ಯಗೊಳಿಸಿ" - "ಡಯಲ್‌ಪ್ಯಾಡ್ ತೋರಿಸು" - "ಡಯಲ್‌ಪ್ಯಾಡ್ ಮರೆಮಾಡು" - "ಮ್ಯೂಟ್" - "ಅನ್‌ಮ್ಯೂಟ್" - "ಕರೆಯನ್ನು ಸೇರಿಸು" - "ಕರೆಗಳನ್ನು ವಿಲೀನಗೊಳಿಸು" - "ಸ್ವ್ಯಾಪ್‌ ಮಾಡು" - "ಕರೆಗಳನ್ನು ನಿರ್ವಹಿಸಿ" - "ಕಾನ್ಫರೆನ್ಸ್ ಕರೆಯನ್ನು ನಿರ್ವಹಿಸಿ" - "ಕಾನ್ಫರೆನ್ಸ್ ಕರೆ" - "ನಿರ್ವಹಿಸು" - "ಆಡಿಯೊ" - "ವೀಡಿಯೊ ಕರೆ" - "ಧ್ವನಿ ಕರೆಗೆ ಬದಲಾಯಿಸಿ" - "ಕ್ಯಾಮರಾ ಬದಲಿಸಿ" - "ಕ್ಯಾಮರಾ ಆನ್ ಮಾಡಿ" - "ಕ್ಯಾಮರಾ ಆಫ್ ಮಾಡಿ" - "ಇನ್ನಷ್ಟು ಆಯ್ಕೆಗಳು" - "ಪ್ಲೇಯರ್‌ ಪ್ರಾರಂಭವಾಗಿದೆ" - "ಪ್ಲೇಯರ್‌ ನಿಲ್ಲಿಸಲಾಗಿದೆ" - "ಕ್ಯಾಮರಾ ಸಿದ್ಧವಾಗಿಲ್ಲ" - "ಕ್ಯಾಮರಾ ಸಿದ್ಧವಾಗಿದೆ" - "ಅಪರಿಚಿತ ಕರೆಯ ಸೆಶನ್‌ ಈವೆಂಟ್‌" - "ಸೇವೆ" - "ಸೆಟಪ್" - "<ಹೊಂದಿಸಿಲ್ಲ>" - "ಇತರ ಕರೆ ಸೆಟ್ಟಿಂಗ್‌ಗಳು" - "%s ಮೂಲಕ ಕರೆ ಮಾಡಲಾಗುತ್ತಿದೆ" - "%s ಮೂಲಕ ಒಳಬರುತ್ತಿರುವ ಕರೆ" - "ಸಂಪರ್ಕ ಫೋಟೋ" - "ಖಾಸಗಿಯಾಗಿ ಹೋಗಿ" - "ಸಂಪರ್ಕವನ್ನು ಆಯ್ಕೆಮಾಡಿ" - "ನಿಮ್ಮ ಸ್ವಂತದ್ದನ್ನು ಬರೆಯಿರಿ..." - "ರದ್ದುಮಾಡಿ" - "ಕಳುಹಿಸು" - "ಉತ್ತರ" - "SMS ಕಳುಹಿಸಿ" - "ನಿರಾಕರಿಸು" - "ವೀಡಿಯೊ ಕರೆ ರೂಪದಲ್ಲಿ ಉತ್ತರಿಸಿ" - "ಆಡಿಯೊ ಕರೆಯಂತೆ ಉತ್ತರಿಸಿ" - "ವೀಡಿಯೊ ವಿನಂತಿ ಒಪ್ಪಿಕೊಳ್ಳು" - "ವೀಡಿಯೊ ವಿನಂತಿ ತಿರಸ್ಕರಿಸು" - "ವೀಡಿಯೊ ಪ್ರಸಾರ ವಿನಂತಿ ಸಮ್ಮತಿಸಿ" - "ವೀಡಿಯೊ ಪ್ರಸಾರ ವಿನಂತಿ ತಿರಸ್ಕರಿಸಿ" - "ವೀಡಿಯೊ ಸ್ವೀಕರಿಸುವಿಕೆ ವಿನಂತಿ ಸಮ್ಮತಿಸಿ" - "ವೀಡಿಯೊ ಸ್ವೀಕರಿಸುವಿಕೆ ವಿನಂತಿ ತಿರಸ್ಕರಿಸಿ" - "%s ಗೆ ಮೇಲಕ್ಕೆ ಸ್ಲೈಡ್ ಮಾಡಿ." - "%s ಗೆ ಎಡಕ್ಕೆ ಸ್ಲೈಡ್ ಮಾಡಿ." - "%s ಗೆ ಬಲಕ್ಕೆ ಸ್ಲೈಡ್ ಮಾಡಿ." - "%s ಗೆ ಕೆಳಕ್ಕೆ ಸ್ಲೈಡ್ ಮಾಡಿ." - "ವೈಬ್ರೇಟ್‌" - "ವೈಬ್ರೇಟ್‌" - "ಶಬ್ದ" - "ಡಿಫಾಲ್ಟ್‌ ಧ್ವನಿ (%1$s)" - "ಫೋನ್ ರಿಂಗ್‌ಟೋನ್" - "ರಿಂಗ್ ಆಗುವಾಗ ವೈಬ್ರೇಟ್‌ ಆಗು" - "ರಿಂಗ್‌ಟೋನ್‌‌ ಮತ್ತು ವೈಬ್ರೇಟ್‌" - "ಕಾನ್ಫರೆನ್ಸ್ ಕರೆಯನ್ನು ನಿರ್ವಹಿಸಿ" - "ತುರ್ತು ಸಂಖ್ಯೆ" - "ಪ್ರೊಫೈಲ್ ಫೋಟೋ" - "ಕ್ಯಾಮರಾ ಆಫ್‌" - "%s ಮೂಲಕ" - "ಟಿಪ್ಪಣಿ ಕಳುಹಿಸಲಾಗಿದೆ" - "ಇತ್ತೀಚಿನ ಸಂದೇಶಗಳು" - "ವ್ಯಾಪಾರ ಮಾಹಿತಿ" - "%.1f ಮೈಲು ದೂರ" - "%.1f ಕಿಮೀ ದೂರ" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "ನಾಳೆ %s ಗಂಟೆಗೆ ತೆರೆಯುತ್ತದೆ" - "ಇಂದು %s ಗಂಟೆಗೆ ತೆರೆಯುತ್ತದೆ" - "%s ಗಂಟೆಗೆ ಮುಚ್ಚಲಾಗಿದೆ" - "ಇಂದು %s ಗಂಟೆಗೆ ಮುಚ್ಚಲಾಗಿದೆ" - "ಇದೀಗ ತೆರೆಯಲಾಗಿದೆ" - "ಇದೀಗ ಮುಚ್ಚಲಾಗಿದೆ" - "ಶಂಕಿತ ಸ್ಪ್ಯಾಮ್ ಕರೆದಾರರು" - "ಕರೆ ಮುಕ್ತಾಯಗೊಂಡಿದೆ %1$s" - "ಇದೇ ಮೊದಲ ಬಾರಿಗೆ ಈ ಸಂಖ್ಯೆಯಿಂದ ನಿಮಗೆ ಕರೆ ಮಾಡಲಾಗಿದೆ." - "ನಾವು ಈ ಕರೆಯನ್ನು ಸ್ಪ್ಯಾಮರ್‌ ಎಂದು ಶಂಕಿಸಿದ್ದೇವೆ." - "ಸ್ಪ್ಯಾಮ್ ನಿರ್ಬಂಧಿಸು/ವರದಿ ಮಾಡು" - "ಸಂಪರ್ಕ ಸೇರಿಸಿ" - "ಸ್ಪ್ಯಾಮ್‌ ಅಲ್ಲ" - diff --git a/InCallUI/res/values-ko/strings.xml b/InCallUI/res/values-ko/strings.xml deleted file mode 100644 index 973dc4637..000000000 --- a/InCallUI/res/values-ko/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "전화" - "대기 중" - "알 수 없음" - "비공개 번호" - "공중전화" - "다자간 통화" - "연락되지 않음" - "스피커" - "핸드셋 수화부" - "유선 헤드셋" - "블루투스" - "다음 톤을 보내시겠습니까?\n" - "신호음 보내기\n" - "전송" - "예" - "아니요" - "와일드 문자를 다음으로 바꿈:" - "다자간 통화 %s" - "음성사서함 번호" - "전화 거는 중" - "재다이얼 중" - "다자간 통화" - "수신 전화" - "수신 업무 전화" - "통화 종료됨" - "대기 중" - "전화 끊는 중" - "통화 중" - "내 전화번호는 %s입니다." - "화상 통화 연결 중" - "화상 통화" - "화상 통화 요청 중" - "화상 통화를 연결할 수 없습니다." - "화상 통화 요청이 거부되었습니다." - "콜백 번호\n %1$s" - "긴급 콜백 번호\n%1$s" - "전화 거는 중" - "부재중 전화" - "부재중 전화" - "부재중 전화 %s통" - "%s의 부재중 전화" - "발신 전화" - "발신 업무 전화" - "발신 Wi-Fi 전화" - "발신 Wi-Fi 업무 전화" - "대기 중" - "수신 전화" - "수신 업무 전화" - "Wi-Fi 수신 전화" - "수신 Wi-Fi 업무 전화" - "수신 화상 통화" - "의심스러운 스팸 발신자로부터 온 전화" - "수신 화상 통화 요청" - "새로운 음성사서함" - "새 음성사서함(%d개)" - "%s(으)로 전화 걸기" - "알 수 없는 음성사서함 번호" - "서비스 불가" - "선택한 네트워크(%s)를 사용할 수 없음" - "전화 받기" - "전화 끊기" - "화상" - "음성" - "수락" - "해제" - "전화 걸기" - "메시지" - "다른 기기에서 진행 중인 통화" - "통화 전환" - "전화를 걸려면 먼저 비행기 모드를 해제하세요." - "네트워크에서 등록되지 않았습니다." - "사용 가능한 이동통신망이 없습니다." - "전화를 걸려면 올바른 번호를 입력하세요." - "전화를 걸 수 없습니다." - "MMI 시퀀스 시작 중..." - "서비스가 지원되지 않습니다." - "통화를 전환할 수 없습니다." - "통화를 분리할 수 없습니다." - "통화를 전환할 수 없습니다." - "다자간 통화를 이용할 수 없습니다." - "통화를 거부할 수 없습니다." - "통화를 끊을 수 없습니다." - "SIP 통화" - "긴급 전화" - "무선을 켜는 중..." - "서비스를 사용할 수 없습니다. 다시 시도 중..." - "전화를 걸 수 없습니다. %s은(는) 긴급 번호가 아닙니다." - "전화를 걸 수 없습니다. 긴급 번호를 사용하세요." - "키보드를 사용하여 전화 걸기" - "통화 대기" - "통화 재개" - "통화 종료" - "다이얼패드 표시" - "다이얼패드 숨기기" - "음소거" - "음소거 해제" - "통화 추가" - "통화 병합" - "전환" - "통화 관리" - "다자간 통화 관리" - "다자간 통화" - "관리" - "오디오" - "화상 통화" - "음성 통화로 변경" - "카메라 전환" - "카메라 켜기" - "카메라 끄기" - "옵션 더보기" - "플레이어가 시작되었습니다." - "플레이어가 중지되었습니다." - "카메라가 준비되지 않았습니다." - "카메라가 준비되었습니다." - "알 수 없는 통화 세션 이벤트" - "서비스" - "설정" - "<설정 안됨>" - "기타 통화 설정" - "%s을(를) 통해 걸려온 전화" - "%s을(를) 통해 걸려온 전화" - "연락처 사진" - "비공개로 실행" - "연락처 선택" - "나만의 응답 작성…" - "취소" - "전송" - "전화 받기" - "SMS 보내기" - "거부" - "화상 통화로 받기" - "음성 통화로 받기" - "화상 통화 요청 수락" - "화상 통화 요청 거부" - "화상 통화 전송 요청 허용" - "화상 통화 전송 요청 거부" - "화상 통화 수신 요청 허용" - "화상 통화 수신 요청 거부" - "%s하려면 위로 슬라이드합니다." - "%s하려면 왼쪽으로 슬라이드합니다." - "%s하려면 오른쪽으로 슬라이드합니다." - "%s하려면 아래로 슬라이드합니다." - "진동" - "진동" - "소리" - "기본 알림음(%1$s)" - "전화 벨소리" - "전화 수신 시 진동" - "벨소리 및 진동" - "다자간 통화 관리" - "비상 전화번호" - "프로필 사진" - "카메라 꺼짐" - "수신 번호: %s" - "메모가 전송되었습니다." - "최근 메시지" - "비즈니스 정보" - "%.1fmi 거리" - "%.1fkm 거리" - "%1$s, %2$s" - "%1$s~%2$s" - "%1$s, %2$s" - "내일 %s에 영업 시작" - "오늘 %s에 영업 시작" - "%s에 영업 종료" - "오늘 %s에 영업 종료됨" - "영업 중" - "영업 종료" - "의심스러운 스팸 발신자" - "%1$s번으로 끝나는 번호에서 걸려온 전화" - "이 번호에서 처음으로 걸려온 전화입니다." - "스팸 전화로 의심됩니다." - "스팸 차단/신고" - "연락처 추가" - "스팸 해제" - diff --git a/InCallUI/res/values-ky/strings.xml b/InCallUI/res/values-ky/strings.xml deleted file mode 100644 index bebdcfde8..000000000 --- a/InCallUI/res/values-ky/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Телефон" - "Күтүлүүдө" - "Белгисиз" - "Купуя номер" - "Таксофон" - "Конференц-чалуу" - "Чалуу үзүлдү" - "Катуу сүйлөткүч" - "Гарнитура" - "Зымдуу гарнитура" - "Bluetooth" - "Төмөнкү номер жөнөтүлсүнбү?\n" - "Обондор жөнөтүлүүдө\n" - "Жөнөтүү" - "Ооба" - "Жок" - "Атайын белгини төмөнкүгө алмаштыруу" - "Конференц-чалуу %s" - "Үн почтасынын номери" - "Терилүүдө" - "Кайра терилүүдө" - "Конференц-чалуу" - "Кирүүчү чалуу" - "Жумуш боюнча чалуу" - "Чалуу аяктады" - "Күтүлүүдө" - "Чалуу аяктоодо" - "Чалууда" - "Менин номерим %s" - "Видео туташтырылууда" - "Видео чалуу" - "Видео суралууда" - "Видео чалууга туташуу мүмкүн болбой жатат" - "Видео сурам четке кагылды" - "Кайра чалына турган номер\n %1$s" - "Өзгөчө кырдаалда кайра чалына турган номер\n %1$s" - "Терилүүдө" - "Кабыл алынбаган чалуу" - "Кабыл алынбаган чалуулар" - "%s кабыл алынбаган чалуу" - "%s дегенден кабыл алынбаган чалуу" - "Учурдагы чалуу" - "Учурдагы чалуу (жумуш боюнча)" - "Учурдагы Wi-Fi чалуу" - "Учурдагы Wi-Fi чалуу (жумуш боюнча)" - "Күтүлүүдө" - "Кирүүчү чалуу" - "Жумуш боюнча чалуу" - "Кирүүчү Wi-Fi чалуу" - "Жумуш боюнча келип жаткан Wi-Fi чалуу" - "Кирүүчү видео чалуу" - "Келип жаткан чалуу спам окшойт" - "Кирүүчү видео сурамы" - "Жаңы үн почтасы" - "Жаңы үн почтасы (%d)" - "%s номерин терүү" - "Үн почтасынын номери белгисиз" - "Байланыш жок" - "Тандалган тармак (%s) жеткиликсиз" - "Жооп берүү" - "Чалууну бүтүрүү" - "Видео" - "Үн" - "Кабыл алуу" - "Этибарга албоо" - "Кайра чалуу" - "Билдирүү" - "Башка түзмөктө сүйлөшүп жатасыз" - "Чалууну бул түзмөккө өткөрүү" - "Учак режимин өчүрүп туруп чалыңыз." - "Тармакта катталган эмес." - "Мобилдик тармак жеткиликтүү эмес." - "Чалуу үчүн, жарактуу номер киргизиңиз." - "Чалынбай жатат." - "MMI кезеги башталууда…" - "Кызмат колдоого алынбайт." - "Чалуулар которуштурулбай жатат." - "Чалуу бөлүнбөй жатат." - "Өткөрүлбөй жатат." - "Конференц-чалуу түзүлбөй жатат." - "Чалуу четке кагылбай жатат." - "Чалуу (-лар) ажыратылбай жатат." - "SIP чалуу" - "Өзгөчө кырдаалда чалуу" - "Радио күйгүзүлүүдө…" - "Кызмат жок. Кайра аракет кылууда…" - "Чалынбай жатат. %s өзгөчө кырдаал номери эмес." - "Чалынбай жатат. Өзгөчө кырдаал номерин териңиз." - "Баскычтоп менен териңиз" - "Чалууну кармап туруу" - "Чалууну улантуу" - "Чалууну бүтүрүү" - "Номер тергичти көрсөтүү" - "Номер тергичти жашыруу" - "Үнсүз" - "Үндү чыгаруу" - "Чалуу кошуу" - "Чалууларды бириктирүү" - "Алмаштыруу" - "Чалууларды башкаруу" - "Конференц-чалууну башкаруу" - "Конференц чалуу" - "Башкаруу" - "Аудио" - "Видео чалуу" - "Үн чалууга өзгөртүү" - "Камераны которуштуруу" - "Камераны күйгүзүү" - "Камераны өчүрүү" - "Дагы параметрлер" - "Ойноткуч башталды" - "Ойноткуч токтотулду" - "Камера даяр эмес" - "Камера даяр" - "Чалуу сеансынын окуясы белгисиз" - "Кызмат" - "Орнотуу" - "<Коюлган эмес>" - "Башка чалуу жөндөөлөрү" - "%s аркылуу чалуу" - "%s аркылуу келүүдө" - "байланыштын сүрөтү" - "купуя режимине өтүү" - "байланыш тандоо" - "Сиздин жообуңуз…" - "Жокко чыгаруу" - "Жөнөтүү" - "Жооп берүү" - "SMS жөнөтүү" - "Четке кагуу" - "Видео чалуу түрүндө жооп берүү" - "Аудио чалуу түрүндө жооп берүү" - "Видео сурамын кабыл алуу" - "Видео сурамын четке кагуу" - "Видео өткөрүү сурамын кабыл алуу" - "Видео өткөрүү сурамын четке кагуу" - "Видео алуу сурамын кабыл алуу" - "Видео алуу сурамын четке кагуу" - "%s үчүн жогору жылмыштырыңыз." - "%s үчүн солго жылмыштырыңыз." - "%s үчүн оңго жылмыштырыңыз." - "%s үчүн төмөн жылмыштырыңыз." - "Дирилдөө" - "Дирилдөө" - "Үн" - "Демейки үнү (%1$s)" - "Телефондун рингтону" - "Дирилдеп шыңгырасын" - "Шыңгыр жана дирилдөө" - "Конференц-чалууну башкаруу" - "Өзгөчө кырдаал номери" - "Профилдин сүрөтү" - "Камера өчүк" - "%s аркылуу" - "Билдирүү жөнөтүлдү" - "Акыркы билдирүүлөр" - "Компания тууралуу маалымат" - "%.1f миля алыста" - "%.1f км алыста" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "Эртең саат %s ачылат" - "Бүгүн саат %s ачылат" - "Саат %s жабылат" - "Бүгүн саат %s жабылды" - "Азыр ачык" - "Эми жабылды" - "Спам окшойт" - "Чалуу %1$s бүттү" - "Бул номер сизге биринчи жолу чалып жатат." - "Бул чалуу спам окшойт." - "Бөгөттөө/спам катары кабарлоо" - "Байланыш кошуу" - "Спам эмес" - diff --git a/InCallUI/res/values-lo/strings.xml b/InCallUI/res/values-lo/strings.xml deleted file mode 100644 index 3e7e81520..000000000 --- a/InCallUI/res/values-lo/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "ໂທລະສັບ" - "ຖືສາຍລໍຖ້າ" - "ບໍ່ຮູ້ຈັກ" - "ເບີສ່ວນຕົວ" - "ຕູ້​ໂທ​ລະ​ສັບ​ສາ​ທາ​ລະ​ນະ" - "ການປະຊຸມທາງໂທລະສັບ" - "ສາຍ​ຫຼຸດ​ແລ້ວ" - "ລຳໂພງ" - "ຊຸດຫູຟັງ" - "ຊຸດຫູຟັງແບບມີສາຍ" - "Bluetooth" - "ສົ່ງໂທນສຽງຕໍ່ໄປນີ້ບໍ?\n" - "ກຳລັງສົ່ງໂທນສຽງ\n" - "ສົ່ງ" - "ແມ່ນ" - "ບໍ່" - "ປ່ຽນແທນ \"ອັກຂະລະຕົວແທນ\" ດ້ວຍ" - "ການປະຊຸມທາງໂທລະສັບ %s" - "ເບີຂໍ້ຄວາມສຽງ" - "ກຳລັງໂທ" - "ກຳ​ລັງ​ໂທ​ຄືນ" - "ການປະຊຸມທາງໂທລະສັບ" - "​ສາຍ​ໂທ​ເຂົ້າ" - "ສາຍໂທເຂົ້າຈາກບ່ອນເຮັດວຽກ" - "ວາງສາຍແລ້ວ" - "ຖືສາຍລໍຖ້າ" - "ກຳລັງວາງສາຍ" - "ຢູ່ໃນສາຍ" - "ເບີໂທຂອງຂ້ອຍແມ່ນ %s" - "​ກຳ​ລັງ​ເຊື່ອມ​ຕໍ່​ວິ​ດີ​ໂອ" - "​ການໂທ​​ວິ​ດີ​ໂອ" - "​ກຳ​ລັງ​ຮ້ອງ​ຂໍການໂທ​ວິ​ດີ​ໂອ" - "ບໍ່​ສາ​ມາດ​ເຊື່ອມ​ຕໍ່​ການ​ໂທວິດີໂອ​ໄດ້" - "ປະ​ຕິ​ເສດ​ການຮ້ອງ​ຂໍການ​ໂທວິ​ດີ​ໂອ​ແລ້ວ" - "ເບີໂທກັບຂອງທ່ານ\n %1$s" - "ເບີ​ໂທ​ກັບ​ສຸກ​ເສີນ​ຂອງ​ທ່ານ\n %1$s" - "ກຳລັງໂທ" - "ສາຍບໍ່ໄດ້ຮັບ" - "ສາຍບໍ່ໄດ້ຮັບ" - "%s ສາຍບໍ່ໄດ້ຮັບ" - "ສາຍບໍ່ໄດ້ຮັບຈາກ %s" - "ສາຍກຳລັງໂທ" - "ສາຍກຳລັງໂທຈາກບ່ອນເຮັດວຽກ" - "ສາຍກຳລັງໂທຜ່ານ Wi​-Fi" - "ສາຍກຳລັງໂທຜ່ານ Wi-Fi ຈາກບ່ອນເຮັດວຽກ" - "ຖືສາຍລໍຖ້າ" - "​ສາຍ​ໂທ​ເຂົ້າ" - "ສາຍໂທເຂົ້າຈາກບ່ອນເຮັດວຽກ" - "ສາຍໂທເຂົ້າຜ່ານ Wi-Fi" - "ສາຍໂທເຂົ້າຜ່ານ Wi-Fi ຈາກບ່ອນເຮັດວຽກ" - "ສາຍໂທ​ວິດີໂອ​ເຂົ້າ" - "ມີການໂທທີ່ຄາດວ່າເປັນສະແປມໂທເຂົ້າມາ" - "​ຄຳ​ຮ້ອງ​ຂໍ​ວິ​ດີ​ໂອທີ່​ເຂົ້າ​ມາ" - "ຂໍ້ຄວາມສຽງໃໝ່" - "ຂໍ້ຄວາມສຽງໃໝ່ (%d)" - "ໂທຫາ %s" - "ເບີຂໍ້ຄວາມສຽງບໍ່ຮູ້ຈັກ" - "ບໍ່ມີການບໍລິການ" - "ເຄືອຂ່າຍທີ່ເລືອກ (%s) ບໍ່ສາມາດໃຊ້ໄດ້" - "ຮັບສາຍ" - "ວາງສາຍ" - "ວິດີໂອ" - "ສຽງ" - "ຍອມຮັບ" - "ປິດໄວ້" - "ໂທກັບ" - "ຂໍ້ຄວາມ" - "ສາຍທີ່ກຳລັງໂທອອກໃນອຸປະກອນອື່ນ" - "ໂອນສາຍ" - "ເພື່ອເຮັດການໂທ, ໃຫ້ປິດໂໝດເຮືອບິນກ່ອນ" - "ບໍ່ໄດ້ລົງທະບຽນໃນເຄືອຂ່າຍ." - "ບໍ່​ມີ​ເຄືອ​ຂ່າຍ​ມື​ຖື​ທີ່​​ໃຊ້​ໄດ້." - "ເພື່ອເຮັດການ​ໂທ, ປ້ອນ​ເບີ​ໂທ​ທີ່​ໃຊ້​ໄດ້​." - "ບໍ່​ສາ​ມາດ​ໂທ​ໄດ້." - "ກຳລັງເລີ່ມຕົ້ນລຳດັບ MMI..." - "ບໍ່ຮອງຮັບການ​ບໍ​ລິ​ການ." - "ບໍ່​ສາ​ມາດ​ສະ​ຫຼັບ​ສາ​ຍ​ໂທ​ໄດ້." - "ບໍ່​ສາ​ມາດ​ແຍກ​ສາຍ​ໂທ​ໄດ້." - "ບໍ່​ສາ​ມາດ​ໂອນສາຍ​ໄດ້." - "ບໍ່​ສາ​ມາດ​ປະ​ຊຸມ​ໄດ້." - "ບໍ່​ສາ​ມາດ​ປະ​ຕິ​ເສດ​ສາຍ​ໂທ​ໄດ້." - "ບໍ່​ສາ​ມາດ​ປ່ອຍ​ສາຍ​ໂທ​ໄດ້." - "ການໂທ SIP" - "ການໂທສຸກເສີນ" - "ກຳລັງເປີດວິທະຍຸ" - "ບໍ່​ມີ​ການ​ບໍ​ລິ​ການ. ກຳ​ລັງ​ລອງ​ໃໝ່​ອີກ…" - "ບໍ່ສາມາດໂທໄດ້. %s ບໍ່ແມ່ນເບີໂທສຸກເສີນ." - "ບໍ່​ສາ​ມາດ​ໂທ​ໄດ້. ກົດ​ເບີ​ໂທ​ສຸກ​ເສີນ." - "ໃຊ້ແປ້ນພິມເພື່ອກົດໂທ" - "ຖືສາຍ" - "​ສືບ​ຕໍ່​ສາຍ" - "ວາງສາຍ" - "ສະແດງປຸ່ມກົດ" - "ເຊື່ອງປຸ່ມກົດ" - "ປິດສຽງ" - "ເຊົາປິດສຽງ" - "ເພີ່ມການໂທ" - "ລວມສາຍ" - "ສະຫຼັບ" - "ຈັດການການໂທ" - "ຈັດ​ການ​ການ​ປະ​ຊຸມ​ທາງໂທລະສັບ" - "ການປະຊຸມທາງໂທລະສັບ" - "ຈັດການ" - "ສຽງ" - "​ການໂທ​​ວິ​ດີ​ໂອ" - "ປ່ຽນ​ເປັນ​ການ​ໂທ​ດ້ວຍ​ສຽງ" - "ສັບປ່ຽນກ້ອງ" - "ເປີດກ້ອງ" - "ປິດກ້ອງ" - "ຕົວເລືອກ​ເພີ່ມ​ເຕີມ" - "ເຄື່ອງ​ຫຼິ້ນ​ເລີ່ມ​ຕົ້ນ​ແລ້ວ" - "ເຄື່ອງ​ຫຼິ້ນ​ຢຸດ​ແລ້ວ" - "ກ້ອງ​ຖ່າຍ​ຮູບ​ບໍ່​ພ້ອມ" - "ກ້ອງ​ຖ່າຍ​ຮູບ​ພ້ອມ​ແລ້ວ" - "ເຫດ​ການ​ເຊ​ສ​ຊັນ​ການ​ໂທ​ບໍ່​ຮູ້​ຈັກ" - "ການບໍລິການ" - "ຕັ້ງຄ່າ" - "<ບໍ່ໄດ້ຕັ້ງ>" - "ການຕັ້ງຄ່າການໂທອື່ນ" - "ກຳລັງໂທຜ່ານ %s" - "ສາຍໂທເຂົ້າ​ຈາກ %s" - "ຮູບລາຍຊື່ຜູ້ຕິດຕໍ່" - "ໃຊ້ແບບສ່ວນຕົວ" - "ເລືອກລາຍຊື່ຜູ້ຕິດຕໍ່" - "ຂຽນ...ຂອງທ່ານເອງ" - "ຍົກເລີກ" - "ສົ່ງ" - "ຮັບສາຍ" - "ສົ່ງ SMS" - "ປະຕິເສດ" - "ຮັບສາຍໂທວິດີໂອ" - "ຮັບສາຍໂທແບບສຽງ" - "ຍອມຮັບການຂໍວິດີໂອ" - "ປະຕິເສດການຂໍວິດີໂອ" - "ຍອມ​ຮັບ​ການ​ຂໍ​ສົ່ງ​ວິ​ດີ​ໂອ" - "ປະ​ຕິ​ເສດ​ການ​ຂໍ​ສົ່ງ​ວິ​ດີ​ໂອ" - "ຍອມ​ຮັບ​ການ​ຂໍ​ຮັບ​ວິ​ດີ​ໂອ" - "ປະ​ຕິ​ເສດ​ການ​ຂໍ​ຮັບ​ວິ​ດີ​ໂອ" - "ເລື່ອນຂຶ້ນເພື່ອ %s." - "ເລື່ອນໄປຊ້າຍເພື່ອ %s." - "ເລື່ອນໄປຂວາເພື່ອ %s." - "ເລື່ອນລົງເພື່ອ %s." - "ສັ່ນເຕືອນ" - "ສັ່ນເຕືອນ" - "ສຽງ" - "ສຽງເລີ່ມຕົ້ນ (%1$s)" - "ຣິງໂທນໂທລະສັບ" - "ສັ່ນເຕືອນເມື່ອດັງ" - "ຣິງໂທນ ແລະ ການສັ່ນເຕືອນ" - "ຈັດ​ການ​ການ​ປະ​ຊຸມ​ທາງໂທລະສັບ" - "ເບີໂທສຸກເສີນ" - "ຮູບໂປຣໄຟລ໌" - "ກ້ອງ​ຖ່າຍ​ຮູບ​ປິດຢູ່" - "ຜ່ານ %s" - "ສົ່ງ​ບັນ​ທຶກ​ແລ້ວ" - "ຂໍ້​ຄວາມ​ບໍ່​ດົນ​ມາ​ນີ້" - "ຂໍ້​ມູນ​ທຸ​ລະ​ກິດ" - "ຫ່າງອອກໄປ %.1f ໄມ​ລ໌​" - "ຫ່າງອອກໄປ %.1f ກມ" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "ເປີດມື້ອື່ນເວລາ %s" - "ເປີດມື້ນີ້ເວລາ %s" - "ປິດເວລາ %s" - "ປິດແລ້ວມື້ນີ້ເວລາ %s" - "ດຽວ​ນີ້​ເປີດ" - "​ປິດ​ແລ້ວດຽວນີ້" - "ຄາດວ່າເປັນການໂທສະແປມ" - "ການໂທສິ້ນສຸດແລ້ວ %1$s" - "ນີ້ເປັນເທື່ອທຳອິດທີ່ເບີນີ້ໂທຫາທ່ານ." - "ພວກເຮົາສົງໄສວ່າເບີໂທນີ້ເປັນສະແປມ." - "ບລັອກ/ລາຍງານສະແປມ" - "ເພີ່ມລາຍຊື່ຜູ້ຕິດຕໍ່" - "ບໍ່ແມ່ນສະແປມ" - diff --git a/InCallUI/res/values-lt/strings.xml b/InCallUI/res/values-lt/strings.xml deleted file mode 100644 index 2d2a70146..000000000 --- a/InCallUI/res/values-lt/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefonas" - "Sulaikyta" - "Nežinoma" - "Privatus numeris" - "Taksofonas" - "Konferencinis skambutis" - "Skambutis atmestas" - "Garsiakalbis" - "Tel. su gars. prie ausies" - "Laidinės ausinės" - "Bluetooth" - "Siųsti šiuo tonus?\n" - "Siunčiami tonai\n" - "Siųsti" - "Taip" - "Ne" - "Pakaitos simbolį pakeisti" - "Konferencinis skambutis %s" - "Balso pašto numeris" - "Renkamas numeris" - "Numeris renkamas pakartotinai" - "Konferencinis skambutis" - "Gaunamasis skambutis" - "Gaunamasis darbo skambutis" - "Skambutis baigtas" - "Sulaikyta" - "Baigiamas pokalbis" - "Dalyvauju skambutyje" - "Mano numeris: %s" - "Prisijungiama prie vaizdo skambučio" - "Vaizdo skambutis" - "Pateikiama vaizdo skambučio užklausa" - "Nepavyko prijungti vaizdo įrašo skambučio" - "Vaizdo įrašo užklausa atmesta" - "Atskambinimo numeris\n%1$s" - "Atskambinimo numeris, kuriuos skambina pagalbos tarnyba\n%1$s" - "Renkamas numeris" - "Praleistas skambutis" - "Praleisti skambučiai" - "Praleistų skambučių: %s" - "Praleistas skambutis nuo %s" - "Vykstantis pokalbis" - "Vykstantis darbo skambutis" - "Vykstantis „Wi-Fi“ skambutis" - "Vykstantis „Wi-Fi“ darbo skambutis" - "Sulaikyta" - "Gaunamasis skambutis" - "Gaunamasis darbo skambutis" - "Gaunamasis „Wi-Fi“ skambutis" - "Gaunamasis „Wi-Fi“ darbo skambutis" - "Gaunamas vaizdo skambutis" - "Gaunamasis įtartinas šlamšto skambutis" - "Gaunama vaizdo skambučio užklausa" - "Naujas balso pašto pranešimas" - "Naujas balso pašto pranešimas (%d)" - "Rinkti %s" - "Nežinomas balso pašto numeris" - "Nėra paslaugos" - "Pasirinktas tinklas (%s) negalimas" - "Atsiliepti" - "Padėti ragelį" - "Vaizdo įrašas" - "Balsas" - "Priimti" - "Atsisakyti" - "Perskambinti" - "Siųsti pranešimą" - "Kitame įrenginyje vykstantis skambutis" - "Perkelti skambutį" - "Jei norite skambinti, išjunkite lėktuvo režimą." - "Neregistruota tinkle." - "Korinis tinklas nepasiekiamas" - "Kad galėtumėte paskambinti, įveskite tinkamą numerį." - "Nepavyko paskambinti." - "Paleidžiama MMI seka..." - "Paslauga nepalaikoma." - "Nepavyko perjungti skambučių." - "Nepavyko atskirti skambučio." - "Nepavyko peradresuoti." - "Nepavyko sukurti konferencijos." - "Nepavyko atmesti skambučio." - "Nepavyko atjungti skamb." - "SIP skambutis" - "Skambutis pagalbos numeriu" - "Įjungiamas radijas…" - "Nėra ryšio. Bandoma dar kartą…" - "Nepavyko paskambinti. %s nėra pagalbos numeris." - "Nepavyko paskambinti. Surinkite pagalbos tarnybos numerį." - "Naudokite klaviatūrą ir rinkite numerius" - "Sulaikyti skambutį" - "Tęsti skambutį" - "Baigti skambutį" - "Rodyti skambinimo skydelį" - "Slėpti skambinimo skydelį" - "Nutildyti" - "Įjungti garsą" - "Pridėti skambutį" - "Sujungti skambučius" - "Apkeisti" - "Valdyti skambučius" - "Tvarkyti konferencinį skambutį" - "Konferencinis skambutis" - "Tvarkyti" - "Garsas" - "Vaizdo skambutis" - "Pakeisti į balso skambutį" - "Perjungti fotoaparatą" - "Įjungti fotoaparatą" - "Išjungti fotoaparatą" - "Daugiau parinkčių" - "Leistuvė paleista" - "Leistuvė sustabdyta" - "Fotoaparatas neparuoštas" - "Fotoaparatas paruoštas" - "Nežinomas skambučio sesijos įvykis" - "Paslaugos teikėjas" - "Sąranka" - "<Nenustatyta>" - "Kiti skambučio nustatymai" - "Skambinama naudojantis „%s“ paslaugomis" - "Gaunama per „%s“" - "kontakto nuotrauka" - "naudoti privatų režimą" - "pasirinkti kontaktą" - "Sukurkite patys..." - "Atšaukti" - "Siųsti" - "Atsiliepti" - "Siųsti SMS" - "Atmesti" - "Atsiliepti kaip į vaizdo skambutį" - "Atsiliepti kaip į garso skambutį" - "Priimti vaizdo įrašo užkl" - "Atmesti vaizdo įrašo užklausą" - "Priimti vaizdo įrašo perdavimo užklausą" - "Atmesti vaizdo įrašo perdavimo užklausą" - "Priimti vaizdo įrašo gavimo užklausą" - "Atmesti vaizdo įrašo gavimo užklausą" - "Slyskite aukštyn link parinkties „%s“." - "Slyskite į kairę link parinkties „%s“." - "Slyskite į dešinę link parinkties „%s“." - "Slyskite žemyn link %s." - "Vibruoti" - "Vibruoti" - "Garsas" - "Numatytasis garsas (%1$s)" - "Telefono skambėjimo tonas" - "Vibruoti, kai skambina" - "Skambėjimo tonas ir vibracija" - "Tvarkyti konferencinį skambutį" - "Pagalbos numeris" - "Profilio nuotrauka" - "Fotoaparatas išjungtas" - "naudojant %s" - "Užrašas išsiųstas" - "Naujausi pranešimai" - "Įmonės informacija" - "Už %.1f myl." - "Už %.1f km" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "Rytoj atidaroma %s" - "Šiandien atidaroma %s" - "Uždaroma %s" - "Šiandien uždaryta %s" - "Dabar atidaryta" - "Dabar uždaryta" - "Įt. skamb. dėl šl." - "Skambutis baigtas (%1$s)" - "Tai pirmas kartas, kai jums buvo skambinama iš šio numerio." - "Įtarėme, kad šis skambutis yra šlamštas." - "Bl. / pran. apie šl." - "Pridėti kontaktą" - "Ne šlamštas" - diff --git a/InCallUI/res/values-lv/strings.xml b/InCallUI/res/values-lv/strings.xml deleted file mode 100644 index 685ba8b0c..000000000 --- a/InCallUI/res/values-lv/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Tālrunis" - "Aizturēts" - "Nezināms" - "Privāts numurs" - "Maksas tālrunis" - "Konferences zvans" - "Zvans tika pārtraukts." - "Skaļrunis" - "Auss skaļrunis" - "Austiņas ar vadu" - "Bluetooth" - "Vai sūtīt tālāk norādītos signālus?\n" - "Sūtīšanas signāli\n" - "Sūtīt" - "Jā" - "Nē" - "Aizstāt aizstājējzīmi ar:" - "Konferences zvans: %s" - "Balss pasta numurs" - "Notiek numura sastādīšana" - "Notiek atkārtota zvanīšana" - "Konferences zvans" - "Ienākošs zvans" - "Ienākošs darba zvans" - "Zvans ir pabeigts" - "Aizturēts" - "Notiek klausules nolikšana" - "Notiek zvans" - "Mans tālruņa numurs: %s" - "Notiek video savienojuma izveide" - "Videozvans" - "Notiek video pieprasīšana" - "Nevar veikt videozvanu" - "Video pieprasījums noraidīts" - "Jūsu atzvana numurs\n %1$s" - "Jūsu ārkārtas atzvana numurs\n %1$s" - "Notiek numura sastādīšana" - "Neatbildēts zvans" - "Neatbildēti zvani" - "%s neatbildēti zvani" - "Neatbildēts zvans no: %s" - "Notiekošs zvans" - "Notiekošs darba zvans" - "Notiekošs Wi-Fi zvans" - "Notiekošs darba Wi-Fi zvans" - "Aizturēts" - "Ienākošs zvans" - "Ienākošs darba zvans" - "Ienākošs Wi-Fi zvans" - "Ienākošs darba Wi-Fi zvans" - "Ienākošs videozvans" - "Ienākošs, iespējams, nevēlams zvans" - "Ienākošs video pieprasījums" - "Jauns balss pasta ziņojums" - "Jauns balss pasts (%d)" - "Sastādiet šādu numuru: %s" - "Balss pasta numurs nav zināms." - "Nav pakalpojuma" - "Atlasītais tīkls (%s) nav pieejams." - "Atbildēt" - "Beigt zvanu" - "Video" - "Balss" - "Pieņemt" - "Noraidīt" - "Atzvanīt" - "Sūtīt īsziņu" - "Notiekošs zvans citā ierīcē" - "Pāradresēt zvanu" - "Lai veiktu zvanu, vispirms izslēdziet lidojuma režīmu." - "Nav reģistrēts tīklā." - "Mobilais tīkls nav pieejams." - "Lai veiktu zvanu, ievadiet derīgu numuru." - "Nevar veikt zvanu." - "Notiek MMI secības startēšana…" - "Pakalpojums netiek atbalstīts." - "Nevar pārslēgt zvanus." - "Nevar nošķirt zvanu." - "Nevar pārsūtīt." - "Nevar veikt konferences zvanu." - "Nevar noraidīt zvanu." - "Nevar pārtraukt zvanu(-us)." - "SIP zvans" - "Ārkārtas izsaukums" - "Notiek radio ieslēgšana…" - "Nav pakalpojuma. Notiek atkārtots mēģinājums…" - "Nevar veikt zvanu. %s nav ārkārtas numurs." - "Nevar veikt zvanu. Zvaniet ārkārtas numuram." - "Izmantojiet tastatūru, lai sastādītu numuru" - "Aizturēt zvanu" - "Atsākt zvanu" - "Beigt zvanu" - "Rādīt numura sastādīšanas tastatūru" - "Slēpt numura sastādīšanas tastatūru" - "Izslēgt skaņu" - "Ieslēgt skaņu" - "Pievienot zvanu" - "Apvienot zvanus" - "Mainīt" - "Pārvaldīt zvanus" - "Pārvaldīt konferences zvanu" - "Konferences zvans" - "Pārvaldīt" - "Audio" - "Videozvans" - "Mainīt uz balss zvanu" - "Pārslēgt kameru" - "Ieslēgt kameru" - "Izslēgt kameru" - "Citas iespējas" - "Atskaņošana sākta" - "Atskaņošana apturēta" - "Kamera nav gatava" - "Kamera ir gatava" - "Nezināms zvana sesijas notikums" - "Pakalpojums" - "Iestatīšana" - "<Nav iestatīts>" - "Citi zvanu iestatījumi" - "Zvans, ko nodrošina %s" - "Ienākošie zvani, ko nodrošina %s" - "kontaktpersonas fotoattēls" - "pārslēgt uz privāto režīmu" - "atlasīt kontaktpersonu" - "Rakstīt savu…" - "Atcelt" - "Sūtīt" - "Atbildēt" - "Sūtīt īsziņu" - "Noraidīt" - "Atbildēt videozvanā" - "Atbildēt audiozvanā" - "Apstiprināt video pieprasījumu" - "Noraidīt video pieprasījumu" - "Apstiprināt video pārsūtīšanas pieprasījumu" - "Noraidīt video pārsūtīšanas pieprasījumu" - "Apstiprināt video saņemšanas pieprasījumu" - "Noraidīt video saņemšanas pieprasījumu" - "Velciet uz augšu, lai veiktu šādu darbību: %s." - "Velciet pa kreisi, lai veiktu šādu darbību: %s." - "Velciet pa labi, lai veiktu šādu darbību: %s." - "Velciet uz leju, lai veiktu šādu darbību: %s." - "Vibrācija" - "Vibrācija" - "Signāls" - "Noklusējuma signāls (%1$s)" - "Tālruņa zvana signāls" - "Vibrācija zvana laikā" - "Zvana signāls un vibrācija" - "Konferences zvana pārvaldība" - "Ārkārtas numurs" - "Profila fotoattēls" - "Kamera ir izslēgta" - "no numura %s" - "Piezīme nosūtīta" - "Pēdējie ziņojumi" - "Informācija par uzņēmumu" - "%.1f jūdzes(-džu) attālumā" - "%.1f km attālumā" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "Tiks atvērts rīt plkst. %s" - "Tiks atvērts šodien plkst. %s" - "Tiks slēgts plkst. %s" - "Tika slēgts šodien plkst. %s" - "Atvērts" - "Slēgts" - "Nevēlams zvanītājs" - "Zvans beidzās: %1$s" - "Šis jums ir pirmais zvans no šī numura." - "Iespējams, šis zvans bija no nevēlama zvanītāja." - "Bloķēt numuru/ziņot" - "Pievienot personu" - "Nav nevēlams numurs" - diff --git a/InCallUI/res/values-mk/strings.xml b/InCallUI/res/values-mk/strings.xml deleted file mode 100644 index 3161c5475..000000000 --- a/InCallUI/res/values-mk/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Телефон" - "На чекање" - "Непознат" - "Приватен број" - "Говорница" - "Конференциски повик" - "Повикот е прекинат" - "Звучник" - "Слушалка" - "Жичени слушалки" - "Bluetooth" - "Испратете ги следниве тонови?\n" - "Се испраќаат тонови\n" - "Испрати" - "Да" - "Не" - "Заменете го резервниот знак со" - "Конференциски повик %s" - "Број на говорна пошта" - "Бирање" - "Повторно бирање" - "Конференциски повик" - "Дојдовен повик" - "Дојдовен работен повик" - "Повикот заврши" - "На чекање" - "Повикот се прекинува" - "Повик во тек" - "Мојот број е %s" - "Се поврзува видео" - "Видеоповик" - "Се бара видео" - "Не може да се поврзе видеоповик" - "Барањето за видео е одбиено" - "Вашиот број за повратен повик\n %1$s" - "Вашиот број за итен повик\n %1$s" - "Бирање" - "Пропуштен повик" - "Пропуштени повици" - "%s пропуштени повици" - "Пропуштен повик од %s" - "Тековен повик" - "Тековен работен повик" - "Појдовен повик преку Wi-Fi" - "Тековен работен повик преку Wi-Fi" - "На чекање" - "Дојдовен повик" - "Дојдовен работен повик" - "Дојдовен повик преку Wi-Fi" - "Дојдовен работен повик преку Wi-Fi" - "Дојдовен видеоповик" - "Дојдовниот повик може да е спам" - "Дојдовно барање за видео" - "Нова говорна пошта" - "Нова говорна пошта (%d)" - "Бирај %s" - "Непознат број на говорна пошта" - "Нема услуга" - "Избраната мрежа (%s) е недостапна" - "Одговори" - "Спушти" - "Видео" - "Гласовен" - "Прифати" - "Отфрли" - "Врати повик" - "Порака" - "Повик во тек на друг уред" - "Префрлање повик" - "За да остварите повик, прво исклучете го авионскиот режим." - "Не е регистриран на мрежа." - "Не е достапна мобилна мрежа." - "За да остварите повик, внесете важечки број." - "Не може да се повика." - "Започнува ММИ низа..." - "Услугата не е поддржана." - "Не може да се префрлат повици." - "Не може да се оддели повик." - "Не може да се пренесе." - "Не може да се оствари конференциски повик." - "Не може да се отфрли повик." - "Не може да се оствари повик." - "Повик преку SIP" - "Повик за итни случаи" - "Се вклучува радиото..." - "Нема услуга. Се обидува повторно…" - "Не може да се повика. %s не е број за итни повици." - "Не може да се повика. Бирајте го бројот за итни повици." - "Користете ја тастатурата за бирање" - "Стави на чекање" - "Продолжи го повикот" - "Заврши го повикот" - "Прикажи копчиња за бирање" - "Сокриј копчиња за бирање" - "Исклучи звук" - "Вклучи звук" - "Додај повик" - "Спои повици" - "Замени" - "Управувај со повици" - "Управувај со конференциски повик" - "Конференциски повик" - "Управувај" - "Аудио" - "Видеоповик" - "Промени во гласовен повик" - "Промени ја камерата" - "Вклучете ја камерата" - "Исклучете ја камерата" - "Повеќе опции" - "Плеерот се вклучи" - "Плеерот запре" - "Камерата не е подготвена" - "Камерата е подготвена" - "Непознат настан при сесија повици" - "Услуга" - "Поставување" - "<Нема поставка>" - "Други поставки за повик" - "Повикување преку %s" - "Дојдовни повици преку %s" - "фотографија на контакт" - "префли на приватно" - "избери контакт" - "Напиши сопствена..." - "Откажи" - "Испрати" - "Одговори" - "Испрати SMS" - "Одбиј" - "Одговори со видеоповик" - "Одговори со аудиоповик" - "Прифати барање за видео" - "Одбиј барање за видео" - "Прифати барање за пренос на видео" - "Одбиј барање за пренос на видео" - "Прифати барање за прием на видео" - "Одбиј барање за прием на видео" - "Лизгај нагоре за %s." - "Лизгај налево за %s." - "Лизгај надесно за %s." - "Лизгај надолу за %s." - "Вибрации" - "Вибрации" - "Звук" - "Стандарден звук (%1$s)" - "Мелодија на телефонот" - "Вибрации при ѕвонење" - "Мелодија и вибрации" - "Управувај со конференциски повик" - "Број за итни случаи" - "Фотографија на профилот" - "Камерата е исклучена" - "преку %s" - "Испратена е белешка" - "Неодамнешни пораки" - "Деловни информации" - "Оддалечено %.1f милји" - "Оддалечено %.1f км" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "Отвора утре во %s" - "Отвора денес во %s" - "Затвора во %s" - "Денес затвори во %s" - "Сега е отворено" - "Сега е затворено" - "Повикот е можен спам" - "Повикот заврши %1$s" - "За првпат добивте повик од бројов." - "Постоеше сомнеж дека повиков е спам." - "Блок./пријави спам" - "Додајте го контактот" - "Не е спам" - diff --git a/InCallUI/res/values-ml/strings.xml b/InCallUI/res/values-ml/strings.xml deleted file mode 100644 index 5c313ad31..000000000 --- a/InCallUI/res/values-ml/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "ഫോൺ" - "ഹോൾഡിലാണ്" - "അജ്ഞാതം" - "സ്വകാര്യ നമ്പർ" - "പണം നൽകി ഉപയോഗിക്കുന്ന ഫോൺ" - "കോൺഫറൻസ് കോൾ" - "കോൾ വിട്ടു" - "സ്പീക്കർ" - "ഹാൻഡ്‌സെറ്റ് ഇയർപീസ്" - "വയേർഡ് ഹെഡ്സെറ്റ്" - "Bluetooth" - "ഇനിപ്പറയുന്ന ടോണുകൾ അയയ്‌ക്കണോ?\n" - "ടോണുകൾ അയയ്‌ക്കുന്നു\n" - "അയയ്‌ക്കുക" - "ഉവ്വ്" - "ഇല്ല" - "വൈൽഡ് പ്രതീകം ഇതുപയോഗിച്ച് മാറ്റിസ്ഥാപിക്കുക" - "കോൺഫറൻസ് കോൾ %s" - "വോയ്‌സ്‌മെയിൽ നമ്പർ" - "ഡയൽ ചെയ്യുന്നു" - "വീണ്ടും ഡയൽചെയ്യുന്നു" - "കോൺഫറൻസ് കോൾ" - "ഇൻകമിംഗ് കോൾ" - "ഇൻകമിംഗ് ഔദ്യോഗിക കോൾ" - "കോൾ അവസാനിച്ചു" - "ഹോൾഡിലാണ്" - "ഹാംഗ് അപ്പ് ചെയ്യുന്നു" - "കോളിലാണ്" - "എന്റെ നമ്പർ %s ആണ്" - "വീഡിയോ കണക്‌റ്റുചെയ്യുന്നു" - "വീഡിയോ കോൾ" - "വീഡിയോ അഭ്യർത്ഥിക്കുന്നു" - "വീഡിയോ കോളുമായി കണക്‌റ്റുചെയ്യാനാവില്ല" - "വീഡിയോ അഭ്യർത്ഥന നിരസിച്ചു" - "നിങ്ങൾ തിരിച്ചുവിളിക്കേണ്ട നമ്പർ\n %1$s" - "അടിയന്തിരമായി നിങ്ങൾ തിരിച്ചുവിളിക്കേണ്ട നമ്പർ\n %1$s" - "ഡയൽ ചെയ്യുന്നു" - "മിസ്‌ഡ് കോൾ" - "മിസ്‌ഡ് കോളുകൾ" - "%s മിസ്‌ഡ് കോളുകൾ" - "%s എന്നതിൽ നിന്നുള്ള മിസ്‌ഡ് കോൾ" - "കോൾ സജീവമാണ്" - "ഓൺഗോയിംഗ് ഔദ്യോഗിക കോൾ" - "ഓൺഗോയിംഗ് വൈഫൈ കോൾ" - "ഓൺഗോയിംഗ് വൈഫൈ ഔദ്യോഗിക കോൾ" - "ഹോൾഡിലാണ്" - "ഇൻകമിംഗ് കോൾ" - "ഇൻകമിംഗ് ഔദ്യോഗിക കോൾ" - "ഇൻകമിംഗ് വൈഫൈ കോൾ" - "ഇൻകമിംഗ് വൈഫൈ ഔദ്യോഗിക കോൾ" - "ഇൻകമിംഗ് വീഡിയോ കോൾ" - "സംശയാസ്‌പദമായ ഇൻകമിംഗ് സ്‌പാം കോൾ" - "ഇൻകമിംഗ് വീഡിയോ അഭ്യർത്ഥന" - "പുതിയ വോയ്‌സ്‌മെയിൽ" - "പുതിയ വോയ്‌സ്‌മെയിൽ (%d)" - "%s ഡയൽ ചെയ്യുക" - "വോയ്‌സ്‌മെയിൽ നമ്പർ അജ്ഞാതമാണ്" - "സേവനമില്ല" - "തിരഞ്ഞെടുത്ത നെറ്റ്‌വർക്ക് (%s) ലഭ്യമല്ല" - "മറുപടി" - "ഹാംഗ് അപ്പുചെയ്യുക" - "വീഡിയോ" - "വോയ്‌സ്" - "അംഗീകരിക്കുക" - "ഡിസ്മിസ്" - "തിരിച്ചുവിളിക്കുക" - "സന്ദേശം" - "മറ്റൊരു ഉപകരണത്തിൽ നടന്നുകൊണ്ടിരിക്കുന്ന കോൾ" - "കോൾ കൈമാറുക" - "ഒരു കോൾ ചെയ്യാൻ, ആദ്യം ഫ്ലൈറ്റ് മോഡ് ഓഫുചെയ്യുക." - "നെറ്റ്‌വർക്കിൽ രജിസ്റ്റർ ചെയ്‌തിട്ടില്ല." - "സെല്ലുലാർ നെറ്റ്‌വർക്ക് ലഭ്യമല്ല." - "ഒരു കോൾ ചെയ്യുന്നതിന്, സാധുതയുള്ള നമ്പർ നൽകുക." - "കോൾ ചെയ്യാനായില്ല." - "MMI സീക്വൻസ് ആരംഭിക്കുന്നു…" - "സേവനം പിന്തുണയ്‌ക്കുന്നില്ല." - "കോളുകൾ മാറാനാവില്ല." - "കോൾ വേർതിരിക്കാനാവില്ല." - "കൈമാറ്റം ചെയ്യാനാവില്ല." - "കോൺഫറൻസ് കോൾ ചെയ്യാനാവില്ല." - "കോൾ നിരസിക്കാനാവില്ല." - "കോൾ (കോളുകൾ) വിളിക്കാനാവില്ല." - "SIP കോൾ" - "എമർജൻസി കോൾ" - "റേഡിയോ ഓൺ ചെയ്യുന്നു…" - "സേവനമൊന്നുമില്ല. വീണ്ടും ശ്രമിക്കുന്നു…" - "കോൾ ചെയ്യാനാവില്ല. %s എന്നത് ഒരു അടിയന്തിര നമ്പറല്ല." - "കോൾ ചെയ്യാനാവില്ല. ഒരു അടിയന്തിര കോൾ നമ്പർ ഡയൽചെയ്യുക." - "ഡയൽ ചെയ്യാൻ കീബോർഡ് ഉപയോഗിക്കുക" - "കോൾ ഹോൾഡുചെയ്യുക" - "കോൾ പുനരാരംഭിക്കുക" - "കോൾ അവസാനിപ്പിക്കുക" - "ഡയൽപാഡ് കാണിക്കുക" - "ഡയൽപാഡ് മറയ്‌ക്കുക" - "മ്യൂട്ടുചെയ്യുക" - "അൺമ്യൂട്ടുചെയ്യുക" - "കോൾ ചേർക്കുക" - "കോളുകൾ ലയിപ്പിക്കുക" - "സ്വാപ്പുചെയ്യുക" - "കോളുകൾ നിയന്ത്രിക്കുക" - "കോൺഫറൻസ് കോൾ നിയന്ത്രിക്കുക" - "കോൺഫറൻസ് കോൾ" - "മാനേജുചെയ്യുക" - "ഓഡിയോ" - "വീഡിയോ കോൾ" - "വോയ്‌സ്‌ കോളിലേക്ക് മാറ്റുക" - "ക്യാമറ സ്വിച്ചുചെയ്യുക" - "ക്യാമറ ഓണാക്കുക" - "ക്യാമറ ഓഫാക്കുക" - "കൂടുതൽ ഓ‌പ്‌ഷനുകൾ" - "പ്ലെയർ ആരംഭിച്ചു" - "പ്ലേയർ നിർത്തി" - "ക്യാമറ തയ്യാറായില്ല" - "ക്യാമറ തയ്യാറായി" - "അജ്ഞാത കോൾ സെഷൻ ഇവന്റ്" - "സേവനം" - "സജ്ജമാക്കുക" - "<ക്രമീകരിച്ചിട്ടില്ല>" - "മറ്റ് കോൾ ക്രമീകരണം" - "%s മുഖേന വിളിക്കുന്നു" - "%s മുഖേനയുള്ള ഇൻകമിംഗ്" - "കോൺടാക്റ്റ് ഫോട്ടോ" - "സ്വകാര്യം എന്നതിലേക്ക് പോകുക" - "കോൺടാക്റ്റ് തിരഞ്ഞെടുക്കുക" - "നിങ്ങളുടെ സ്വന്തം സന്ദേശമെഴുതുക..." - "റദ്ദാക്കുക" - "അയയ്‌ക്കുക" - "മറുപടി" - "SMS അയയ്ക്കുക" - "നിരസിക്കുക" - "വീഡിയോ കോളായി മറുപടി നൽകുക" - "ഓഡിയോ കോളായി മറുപടി നൽകുക" - "വീഡിയോ കോളിനുള്ള അഭ്യർത്ഥന അംഗീകരിക്കുക" - "വീഡിയോ കോൾ അഭ്യർത്ഥന നിരസിക്കുക" - "വീഡിയോ പ്രക്ഷേപണ അഭ്യർത്ഥന അംഗീകരിക്കുക" - "വീഡിയോ പ്രക്ഷേപണ അഭ്യർത്ഥന നിരസിക്കുക" - "വീഡിയോ കോൾ സ്വീകരിക്കാനുള്ള അഭ്യർത്ഥന അംഗീകരിക്കുക" - "വീഡിയോ കോൾ സ്വീകരിക്കാനുള്ള അഭ്യർത്ഥന നിരസിക്കുക" - "%s എന്നതിനായി മുകളിലേയ്‌ക്ക് സ്ലൈഡുചെയ്യുക." - "%s എന്നതിനായി ഇടത്തേയ്‌ക്ക് സ്ലൈഡുചെയ്യുക." - "%s എന്നതിനായി വലത്തേയ്‌ക്ക് സ്ലൈഡുചെയ്യുക." - "%s എന്നതിനായി താഴേക്ക് സ്ലൈഡുചെയ്യുക." - "വൈബ്രേറ്റുചെയ്യുക" - "വൈബ്രേറ്റുചെയ്യുക" - "ശബ്‌ദം" - "സ്ഥിര ശബ്‌ദം (%1$s)" - "ഫോൺ റിംഗ്ടോൺ" - "റിംഗുചെയ്യുമ്പോൾ വൈബ്രേറ്റുചെയ്യുക" - "റിംഗ്ടോണും വൈബ്രേറ്റുചെയ്യലും" - "കോൺഫറൻസ് കോൾ നിയന്ത്രിക്കുക" - "അടിയന്തര നമ്പർ" - "പ്രൊഫൈൽ ഫോട്ടോ" - "ക്യാമറ ഓഫാക്കുക" - "%s വഴി" - "കുറിപ്പ് അയച്ചു" - "ഏറ്റവും പുതിയ സന്ദേശങ്ങൾ" - "ബിസിനസ്സ് വിവരം" - "%.1f മൈൽ അകലെ" - "%.1f കിലോമീറ്റർ അകലെ" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "നാളെ %s-ന് തുറക്കുന്നു" - "ഇന്ന് %s-ന് തുറക്കുന്നു" - "%s-ന് അടയ്ക്കുന്നു" - "ഇന്ന് %s-ന് അടച്ചു" - "ഇപ്പോൾ തുറന്നിരിക്കുന്നു" - "ഇപ്പോൾ അടച്ചിരിക്കുന്നു" - "സംശയാസ്‌പദമായ സ്‌പാം കോളർ" - "കോൾ അവസാനിച്ചു, %1$s" - "ഈ നമ്പറിൽ നിന്ന് ആദ്യമായാണ് നിങ്ങൾക്ക് കോൾ വരുന്നത്." - "ഈ കോൾ ഒരു സ്‌പാമർ ആണെന്ന് ഞങ്ങൾക്ക് സംശയമുണ്ടായിരുന്നു." - "ബ്ലോക്കുചെയ്യുക/സ്പാമാണെന്ന് റിപ്പോർട്ടുചെയ്യുക" - "കോൺടാക്റ്റ് ചേർക്കുക" - "സ്പാം അല്ല" - diff --git a/InCallUI/res/values-mn/strings.xml b/InCallUI/res/values-mn/strings.xml deleted file mode 100644 index 972f914a4..000000000 --- a/InCallUI/res/values-mn/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Утас" - "Хүлээлгэнд байгаа" - "Тодорхойгүй" - "Нууцалсан дугаар" - "Төлбөртэй утас" - "Хурлын дуудлага" - "Дуудлага таслагдсан" - "Чанга яригч" - "Утасны чихэвч" - "Утастай чихэвч" - "Bluetooth" - "Дараах аяыг илгээх үү?\n" - "Ая илгээж байна\n" - "Илгээх" - "Тийм" - "Үгүй" - "Тэмдэгтийг дараахаар солих" - "Хурлын дуудлага %s" - "Дуут шуудангийн дугаар" - "Залгаж байна" - "Дахин залгаж байна" - "Хурлын дуудлага" - "Орох дуудлага" - "Орох ажлын дуудлага" - "Дуудлага дууссан" - "Хүлээлгэнд" - "Тасалж байна" - "Дуудлагатай" - "Миний дугаар %s" - "Видеог холбож байна" - "Видео дуудлага" - "Видео хүлээж байна" - "Видео дуудлагад холбогдож чадсангүй" - "Бичлэг хийх хүсэлтийг зөвшөөрсөнгүй" - "Таны буцаан залгах дугаар\n %1$s" - "Таны яаралтай хулээн авах дугаар\n %1$s" - "Залгаж байна" - "Аваагүй дуудлага" - "Аваагүй дуудлага" - "%s аваагүй дуудлага" - "%s-н аваагүй дуудлага" - "Залгаж буй дуудлага" - "Холбогдсон албаны дуудлага" - "Холбогдсон Wi-Fi дуудлага" - "Залгаж буй Wi-Fi албаны дуудлага" - "Хүлээгдэж байна" - "Орох дуудлага" - "Орох ажлын дуудлага" - "Орох Wi-Fi дуудлага" - "Орох Wi-Fi албаны дуудлага" - "Орох видео дуудлага" - "Орж ирсэн сэжигтэй спам дуудлага" - "Орох видео хүсэлт" - "Шинэ дуут шуудан" - "Шинэ дуут шуудан (%d)" - "%s руу залгах" - "Дуут шуудангийн дугаар тодорхойгүй" - "Үйлчилгээ байхгүй" - "Сонгосон сүлжээг (%s) ашиглах боломжгүй" - "Хариулт" - "Таслах" - "Видео" - "Дуу хоолой" - "Зөвшөөрөх" - "Алгасах" - "Буцааж залгах" - "Зурвас" - "Өөр төхөөрөмж дээр хийгдэж буй дуудлага" - "Дуудлага шилжүүлэх" - "Залгахын тулд эхлээд Нислэгийн горимоос гарна уу." - "Сүлжээнд бүртгэгдээгүй байна." - "Үүрэн сүлжээ байхгүй." - "Залгахын тулд хүчин төгөлдөр дугаар оруулна уу." - "Залгах боломжгүй байна." - "MMI дарааллыг эхлүүлж байна…" - "Дэмжигдээгүй үйлчилгээ байна." - "Дуудлагыг солих боломжгүй байна." - "Дуудлагыг салгаж чадахгүй байна." - "Шилжүүлэх боломжгүй байна." - "Хурлын дуудлага хийх боломжгүй байна." - "Дуудлагыг цуцлах боломжгүй байна." - "Дуудлага чөлөөлөх боломжгүй байна." - "SIP дуудлага" - "яаралтай" - "Радиог асааж байна..." - "Ажиллагаагүй байна. Дахин оролдоно уу..." - "Залгах боломжгүй. %s нь яаралтай дугаар биш байна." - "Залгах боломжгүй. Яаралтай дугаар луу залгана уу." - "Залгахдаа гар ашиглана уу" - "Дуудлага хүлээлгэх" - "Дуудлагыг үргэлжлүүлэх" - "Дуудлагыг дуусгах" - "Залгах товчлуурыг харуулах" - "Залгах товчлуурыг нуух" - "Дуу хаах" - "Дууг нээх" - "Дуудлага нэмэх" - "Дуудлага нэгтгэх" - "Солих" - "Дуудлага удирдах" - "Хурлын дуудлага удирдах" - "Хурлын дуудлага" - "Удирдах" - "Аудио" - "Видео дуудлага" - "Дуут дуудлага руу өөрчлөх" - "Камер солих" - "Камераа асаана уу" - "Камер унтраах" - "Нэмэлт сонголт" - "Тоглуулагчийг эхлүүлсэн" - "Тоглуулагчийг зогсоосон" - "Камер бэлэн бус байна" - "Камер бэлэн байна" - "Үл мэдэгдэх дуудлагын үе" - "Үйлчилгээ" - "Тохируулга" - "Тохируулаагүй" - "Бусад дуудлагын тохиргоо" - "%s-р залгаж байна" - "%s-р ирж байна" - "харилцагчийн зураг" - "хувийн яриа" - "харилцагч сонгох" - "Өөрийн ...-г бичээрэй" - "Цуцлах" - "Илгээх" - "Хариулт" - "SMS илгээх" - "Татгалзах" - "Видео дуудлагаар хариулах" - "Аудио дуудлагаар хариулах" - "Видео хүсэлтийг хүлээн зөвшөөрөх" - "Видео хүсэлтээс татгалзах" - "Видео дамжуулах хүсэлтийг хүлээн зөвшөөрөх" - "Видео дамжуулах хүсэлтээс татгалзах" - "Видео хүлээж авах хүсэлтийг зөвшөөрөх" - "Видео хүлээн авах хүсэлтээс татгалзах" - "%s хийх бол дээш гулсуулна уу." - "%s-г харахын тулд зүүн тийш гулсуулна уу." - "%s харахын тулд баруун тийш гулсуулна уу." - "%s-г харахын тулд доош гулсуулна уу." - "Чичиргээ" - "Чичиргээ" - "Дуу" - "Үндсэн дуу (%1$s)" - "Утасны хонхны ая" - "Хонх дуугарах үед чичрэх" - "Хонхны ая, Чичиргээ" - "Хурлын дуудлагыг удирдах" - "Яаралтай дугаар" - "Профайл зураг" - "Камер унтраалттай байна" - "%s-р" - "Тэмдэглэлийг илгээсэн" - "Саяхны зурвас" - "Бизнес мэдээлэл" - "%.1f милийн зайтай" - "%.1f км-н зайтай" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "Маргааш %s-с нээгдэнэ" - "Өнөөдөр %s-с нээгдэнэ" - "%s-с хаадаг" - "Өнөөдөр %s-с хаасан" - "Одоо нээлттэй" - "Одоо хаалттай" - "Сэжигтэй спам дуудлага хийгч" - "Дуудлага дууссан %1$s" - "Энэ дугаараас танд анх удаа дуудлага ирсэн." - "Бид үүнийг спам дуудлага гэж үзсэн." - "Спам гэж мэдээлэх/хориглох" - "Харилцагч нэмэх" - "Спам биш" - diff --git a/InCallUI/res/values-mr/strings.xml b/InCallUI/res/values-mr/strings.xml deleted file mode 100644 index 15b3dfc32..000000000 --- a/InCallUI/res/values-mr/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "फोन" - "होल्ड वर" - "अज्ञात" - "खाजगी नंबर" - "सार्वजनिक फोन" - "परिषद कॉल" - "कॉल सोडला" - "स्पीकर" - "हँडसेट इअरपीस" - "वायर्ड हेडसेट" - "ब्लूटुथ" - "खालील टोन पाठवायचे?\n" - "टोन पाठवित आहे\n" - "पाठवा" - "होय" - "नाही" - "खराब वर्णास यासह पुनर्स्थित करा" - "परिषद कॉल %s" - "व्हॉइसमेल नंबर" - "डायल करीत आहे" - "रीडायल करत आहे" - "परिषद कॉल" - "येणारा कॉल" - "येणारा कार्य कॉल" - "कॉल संपला" - "होल्ड वर" - "हँग अप करणेे" - "कॉल मधील" - "माझा नंबर %s आहे" - "व्हिडिओ कनेक्ट करत आहे" - "व्हिडिओ कॉल" - "व्हिडिओ विनंती करत आहे" - "व्हिडिओ कॉल कनेक्ट करू शकत नाही" - "व्हिडिओ विनंती नाकारली" - "आपला कॉलबॅक नंबर\n %1$s" - "आपला आणीबाणी कॉलबॅक नंबर\n %1$s" - "डायल करीत आहे" - "सुटलेला कॉल" - "सुटलेले कॉल" - "%s सुटलेले कॉल" - "%s कडील सुटलेला कॉल" - "सुरू असलेला कॉल" - "सुरु असलेला कार्य कॉल" - "सुरु असलेला वाय-फाय कॉल" - "सुरु असलेला वाय-फाय कार्य कॉल" - "होल्ड वर" - "येणारा कॉल" - "येणारा कार्य कॉल" - "येणारा वाय-फाय कॉल" - "येणारा वाय-फाय कार्य कॉल" - "येणारा व्हिडिओ कॉल" - "येणारा संशयित स्पॅम कॉल" - "येणारी व्हिडिओ विनंती" - "नवीन व्हॉइसमेल" - "नवीन व्हॉइसमेल (%d)" - "%s डायल करा" - "व्हॉइसमेल नंबर अज्ञात" - "सेवा नाही" - "निवडलेले नेटवर्क (%s) अनुपलब्ध" - "उत्तर" - "हँग अप" - "व्हिडिओ" - "व्हॉइस" - "स्वीकार करा" - "डिसमिस करा" - "पुन्हा कॉल करा" - "संदेश" - "दुसऱ्या डिव्हाइसवर सुरु असलेला कॉल" - "कॉल स्थानांतरित करा" - "कॉल करण्यासाठी, प्रथम विमान मोड बंद करा." - "नेटवर्कवर नोंदणीकृत नाही." - "मोबाईल नेटवर्क उपलब्ध नाही." - "कॉल करण्यासाठी, एक वैध नंबर प्रविष्ट करा." - "कॉल करू शकत नाही." - "MMI क्रम प्रारंभ करीत आहे..." - "सेवा समर्थित नाही." - "कॉल स्विच करू शकत नाही." - "कॉल विभक्त करू शकत नाही." - "हस्तांतर करू शकत नाही." - "परिषद घेऊ शकत नाही." - "कॉल नाकारू शकत नाही." - "कॉल रिलीज करू शकत नाही." - "SIP कॉल" - "आणीबाणी कॉल" - "रेडिओ चालू करीत आहे..." - "सेवा नाही. पुन्हा प्रयत्न करत आहे…" - "कॉल करू शकत नाही. %s हा आणीबाणी नंबर नाही." - "कॉल करू शकत नाही. आणीबाणी नंबर डायल करा." - "डायल करण्यासाठी कीबोर्डचा वापर करा" - "कॉल होल्ड करा" - "कॉल पुनः सुरु करा" - "कॉल समाप्त करा" - "डायलपॅड दर्शवा" - "डायलपॅड लपवा" - "नि:शब्द करा" - "सशब्द करा" - "कॉल जोडा" - "कॉल विलीन करा" - "अदलाबदल करा" - "कॉल व्यवस्थापित करा" - "परिषद कॉल व्यवस्थापित करा" - "परिषद कॉल" - "व्यवस्थापित करा" - "ऑडिओ" - "व्हिडिओ कॉल" - "व्हॉइस कॉल वर बदला" - "कॅमेरा स्विच करा" - "कॅमेरा चालू करा" - "कॅमेरा बंद करा" - "अधिक पर्याय" - "प्लेअर सुरु झाले" - "प्लेअर थांबले" - "कॅमेरा तयार नाही" - "कॅमेरा तयार" - "अज्ञात कॉल सत्र इव्हेंट" - "सेवा" - "सेटअप" - "<सेट नाही>" - "इतर कॉल सेटिंग्ज" - "%s द्वारे कॉल करीत आहे" - "%s द्वारे येणारे" - "संपर्क फोटो" - "खाजगी व्हा" - "संपर्क निवडा" - "आपण स्वतः लिहा…" - "रद्द करा" - "पाठवा" - "उत्तर" - "SMS पाठवा" - "नकार द्या" - "व्हिडिओ कॉल म्हणून उत्तर द्या" - "ऑडिओ कॉल म्हणून उत्तर द्या" - "व्हिडिओ विनंती स्वीकारा" - "व्हिडिओ विनंतीस नकार द्या" - "व्हिडिओ प्रसारण विनंती स्वीकार करा" - "व्हिडिओ प्रसारण विनंतीस नकार द्या" - "व्हिडिओ प्राप्त करा विनंती स्वीकार करा" - "व्हिडिओ प्राप्त करा विनंतीस नकार द्या" - "%s साठी वर स्लाइड करा." - "%s साठी डावीकडे स्लाइड करा." - "%s साठी उजवीकडे स्लाइड करा." - "%s साठी खाली स्लाइड करा." - "कंपन करा" - "कंपन करा" - "ध्वनी" - "डीफॉल्ट आवाज (%1$s)" - "फोन रिंगटोन" - "रिंग करताना कंपन करा" - "रिंगटोन आणि कंपन" - "परिषद कॉल व्यवस्थापित करा" - "आणीबाणी नंबर" - "प्रोफाइल फोटो" - "कॅमेरा बंद" - "%s द्वारा" - "टीप पाठविली" - "अलीकडील संदेश" - "व्यवसाय माहिती" - "%.1f मैल दूर" - "%.1f किमी दूर" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "उद्या %s वाजता उघडेल" - "आज %s उघडेल" - "आज %s वाजता बंद होईल" - "आज %s वाजता बंद केले" - "आता उघडा" - "आता बंद केले आहे" - "संशयित स्पॅम कॉलर" - "कॉल समाप्त झाला %1$s" - "या नंबरने अापल्याला कॉल केल्याची ही पहिलीच वेळ आहे." - "अाम्हाला संशय अाहे की हा कॉल एक स्पॅमर असू शकतो." - "अवरोधित करा/स्पॅमचा अहवाल द्या" - "संपर्क जोडा" - "स्पॅम नाही" - diff --git a/InCallUI/res/values-ms/strings.xml b/InCallUI/res/values-ms/strings.xml deleted file mode 100644 index 4c7be3805..000000000 --- a/InCallUI/res/values-ms/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefon" - "Ditunda" - "Tidak diketahui" - "Nombor peribadi" - "Telefon Awam" - "Panggilan sidang" - "Panggilan diputuskan" - "Pembesar suara" - "Alat dengar tel bimbit" - "Set kepala berwayar" - "Bluetooth" - "Hantar nada berikut?\n" - "Menghantar nada\n" - "Hantar" - "Ya" - "Tidak" - "Gantikan aksara bebas dengan" - "Panggilan sidang %s" - "Nombor mel suara" - "Mendail" - "Mendail semula" - "Panggilan sidang" - "Panggilan masuk" - "Pgln masuk tempat kerja" - "Panggilan tamat" - "Ditunda" - "Menamatkan panggilan" - "Dalam panggilan" - "Nombor saya ialah %s" - "Menyambungkan video" - "Panggilan video" - "Meminta video" - "Tidak dapat menyambungkan panggilan video" - "Permintaan video ditolak" - "Nombor panggilan balik anda%1$s\n" - "Nombor panggilan balik kecemasan anda\n%1$s" - "Mendail" - "Panggilan terlepas" - "Panggilan terlepas" - "%s panggilan terlepas" - "Panggilan tidak dijawab daripada %s" - "Panggilan sedang berlangsung" - "Panggilan sedang berlangsung daripada tempat kerja" - "Panggilan Wi-Fi sedang berlangsung" - "Panggian Wi-Fi sedang berlangsung daripada tempat kerja" - "Ditunda" - "Panggilan masuk" - "Panggilan masuk daripada tempat kerja" - "Panggilan masuk melalui Wi-Fi" - "Panggilan masuk melalui Wi-Fi daripada tempat kerja" - "Panggilan video masuk" - "Disyaki panggilan spam masuk" - "Permintaan video masuk" - "Mel suara baharu" - "Mel suara baharu (%d)" - "Dail %s" - "Nombor mel suara tidak dikenali" - "Tiada perkhidmatan" - "Rangkaian pilihan (%s) tidak tersedia" - "Jawab" - "Letakkan gagang" - "Video" - "Suara" - "Terima" - "Ketepikan" - "Panggil balik" - "Mesej" - "Panggilan sedang berlangsung pada peranti lain" - "Pindahkan Panggilan" - "Untuk membuat panggilan, matikan mod Pesawat terlebih dahulu." - "Tidak didaftarkan pada rangkaian." - "Rangkaian selular tidak tersedia." - "Untuk membuat panggilan, masukkan nombor yang sah." - "Tidak dapat memanggil." - "Memulakan jujukan MMI..." - "Perkhidmatan tidak disokong." - "Tidak dapat menukar panggilan." - "Tidak dapat mengasingkan panggilan." - "Tidak dapat memindahkan." - "Tidak dapat membuat panggilan persidangan." - "Tidak dapat menolak panggilan." - "Tidak dapat melepaskan panggilan." - "Panggilan SIP" - "Panggilan kecemasan" - "Menghidupkan radio..." - "Tiada perkhidmatan. Mencuba lagi..." - "Tidak dapat memanggil. %s bukan nombor kecemasan." - "Tidak dapat memanggil. Dail nombor kecemasan." - "Gunakan papan kekunci untuk mendail" - "Tahan Panggilan" - "Sambung Semula Panggilan" - "Tamatkan Panggilan" - "Tunjukkan Pad Pendail" - "Sembunyikan Pad Pendail" - "Redam" - "Nyahredam" - "Tambah panggilan" - "Gabung panggilan" - "Silih" - "Urus panggilan" - "Urus panggilan sidang" - "Panggilan sidang" - "Urus" - "Audio" - "Panggilan video" - "Tukar ke panggilan suara" - "Tukar kamera" - "Hidupkan kamera" - "Matikan kamera" - "Lagi pilihan" - "Pemain Dimulakan" - "Pemain Dihentikan" - "Kamera tidak bersedia" - "Kamera bersedia" - "Acara sesi panggilan tidak diketahui" - "Perkhidmatan" - "Persediaan" - "<Tidak ditetapkan>" - "Tetapan panggilan lain" - "Memanggil melalui %s" - "Panggilan masuk melalui %s" - "foto kenalan" - "jadi peribadi" - "pilih kenalan" - "Tulis sendiri…" - "Batal" - "Hantar" - "Jawab" - "Hantar SMS" - "Tolak" - "Jawab sebagai panggilan video" - "Jawab sebagai panggilan audio" - "Terima permintaan video" - "Tolak permintaan video" - "Terima permintaan hantar video" - "Tolak permintaan hantar video" - "Terima permintaan terima video" - "Tolak permintaan terima video" - "Luncurkan ke atas untuk %s." - "Luncurkan ke kiri untuk %s." - "Luncurkan ke kanan untuk %s." - "Luncurkan ke bawah untuk %s." - "Bergetar" - "Bergetar" - "Bunyi" - "Bunyi lalai (%1$s)" - "Nada dering telefon" - "Bergetar apabila berdering" - "Nada dering & Bergetar" - "Urus panggilan sidang" - "Nombor kecemasan" - "Foto profil" - "Kamera dimatikan" - "melalui %s" - "Nota dihantar" - "Mesej terbaharu" - "Maklumat perniagaan" - "%.1f batu dari sini" - "%.1f km dari sini" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "Dibuka esok pada pukul %s" - "Dibuka hari ini pada pukul %s" - "Tutup pada pukul %s" - "Ditutup hari ini pada pukul %s" - "Dibuka sekarang" - "Ditutup sekarang" - "Disyaki pmggil spam" - "Panggilan tamat %1$s" - "Ini kali pertama nombor ini memanggil anda." - "Kami mengesyaki panggilan ini adalah spam." - "Sekat/laporkan spam" - "Tambahkan kenalan" - "Bukan spam" - diff --git a/InCallUI/res/values-my/strings.xml b/InCallUI/res/values-my/strings.xml deleted file mode 100644 index efe91436f..000000000 --- a/InCallUI/res/values-my/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "ဖုန်း" - "ခဏ ကိုင်ထားစဉ်" - "အမျိုးအမည်မသိ" - "ကိုယ်ပိုင်ဖုန်းနံပါတ်" - "အများသုံးဖုန်း" - "အစည်းအဝေးခေါ်ဆိုမှု" - "ဖုန်းလိုင်းကျသွားခဲ့သည်" - "စပီကာ" - "လက်ကိုင်တယ်လီဖုန်းနားခွက်" - "ကြိုးတပ် မိုက်ခွက်ပါနားကြပ်" - "ဘလူးတုသ်" - "အောက်ပါ တီးလုံးများကို ပို့မလား။\n" - "အသံများ ပို့နေသည်\n" - "ပို့ပါ" - "Yes" - "No" - "အစားထိုး အထူးအက္ခရာတွင် အစားထိုးရန်" - "အစည်းအဝေးခေါ်ဆိုမှု %s" - "အသံစာနံပါတ်" - "ခေါ်ဆိုနေသည်" - "ပြန်ခေါ်နေသည်" - "အစည်းအဝေးခေါ်ဆိုမှု" - "အဝင် ခေါ်ဆိုမှု" - "အလုပ်ဆိုင်ရာ အဝင် ခေါ်ဆိုမှု" - "ဖုန်းခေါ်ဆိုမှု ပြီးဆုံးပါပြီ" - "ခဏ ကိုင်ထားစဉ်" - "ဖုန်းချနေပါသည်" - "ဖုန်းခေါ်ဆိုနေဆဲ" - "ကျွန်ုပ်၏ နံပါတ်မှာ %s ဖြစ်ပါသည်" - "ဗီဒီယို ချိတ်ဆက်နေသည်" - "ဗီဒီယို ခေါ်ဆိုမှု" - "ဗီဒီယိုခေါ်ဆိုနေသည်" - "ဗွီဒီယို ခေါ်ဆိုမှု ချိတ်ဆက်၍မရပါ။" - "ဗီဒီယို ခေါ်ဆိုမှုကို ပယ်ချလိုက်ပါပြီ" - "သင့်ကိုပြန်လည်ခေါ်ဆိုရန် နံပါတ်\n %1$s" - "သင့်ကိုအရေးပေါ် ပြန်လည်ခေါ်ဆိုရန် နံပါတ်\n %1$s" - "ဖုန်းခေါ်နေသည်" - "လွတ်သွားသော ခေါ်ဆိုမှု" - "လွတ်သွားသော ခေါ်ဆိုမှုများ" - "လွတ်သွားသော ခေါ်ဆိုမှု %s" - "%s မှလွတ်သွားသော ခေါ်ဆိုမှု" - "လက်ရှိခေါ်ဆိုမှု" - "လက်ရှိအလုပ်ခေါ်ဆိုမှု" - "လက်ရှိ Wi-Fi ခေါ်ဆိုမှု" - "လက်ရှိအလုပ် Wi-Fi ခေါ်ဆိုမှု" - "ခဏ ကိုင်ထားစဉ်" - "အဝင် ခေါ်ဆိုမှု" - "အလုပ်ဆိုင်ရာ အဝင်ခေါ်ဆိုမှု" - "အဝင် Wi-Fi ခေါ်ဆိုမှု" - "အလုပ်ဆိုင်ရာ အဝင် Wi-Fi ခေါ်ဆိုမှု" - "အဝင်ဗီဒီယိုခေါ်ဆိုမှု" - "ခေါ်နေသော မသင်္ကာဖွယ်ရာ စပမ်းခေါ်ဆိုမှု" - "ဗီဒီယိုအဝင် ခေါ်ဆိုမှု" - "အသံစာအသစ်" - "အသံစာ အသစ် (%d) ခု" - "%s ကိုခေါ်ပါ" - "အသံစာ၏နံပါတ်ကို မသိပါ" - "ဆက်သွယ်မှု ဧရိယာပြင်ပသို့ ရောက်ရှိနေသည်" - "ရွေးချယ်ထားသော ကွန်ရက် (%s) မရရှိနိုင်ပါ" - "ဖြေကြားပါ" - "ဖုန်းချပါ" - "ဗီဒီယို" - "အသံ" - "လက်ခံပါ" - "ပယ်ပါ" - "ပြန်ခေါ်ပါ" - "မက်ဆေ့ဂျ်" - "အခြားကိရိယာတွင် လက်ရှိခေါ်ဆိုနေမှု" - "ခေါ်ဆိုမှုကို လွှဲပြောင်းပါ" - "ခေါ်ဆိုမှု ပြုလုပ်ရန်အတွက် လေယာဉ်ပျံမုဒ်ကို ဦးစွာပိတ်ပါ။" - "ကွန်ယက်ပေါ်တွင် မှတ်ပုံတင်ထားခြင်း မရှိပါ။" - "ဆဲလ်လူလာ ကွန်ရက် မရှိပါ။" - "ခေါ်ဆိုမှု ပြုလုပ်ရန်အတွက် မှန်ကန်သည့်နံပါတ်တစ်ခုကို ထည့်ပါ။" - "ခေါ်ဆို၍မရပါ။" - "MMI အစီအစဉ် စတင်နေသည်..." - "ဝန်ဆောင်မှုအား ပံ့ပိုးမထားပါ။" - "ခေါ်ဆိုမှုများကို လှည့်ပြောင်း၍မရပါ။" - "ခေါ်ဆိုမှုကို ခွဲခြား၍မရပါ။" - "မလွှဲပြောင်းနိုင်ပါ။" - "အစည်းအဝေးခေါ်ဆိုမှု ပြုလုပ်၍မရပါ။" - "ခေါ်ဆိုမှုကို ငြင်းဆို၍မရပါ။" - "ခေါ်ဆိုမှု(များ) ကို လွှတ်၍မရပါ။" - "SIP ခေါ်ဆိုမှု" - "အရေးပေါ် ခေါ်ဆိုမှု" - "ရေဒီယို ဖွင့်နေသည်…" - "ချိတ်ဆက်မှု ဧရိယာပြင်ပရောက်နေပါသည်။ ထပ်စမ်းကြည့်ပါ..." - "ခေါ်ဆို၍မရနိုင်ပါ။ %s သည်အရေးပေါ်နံပါတ်တစ်ခု မဟုတ်ပါ။" - "ခေါ်ဆို၍မရနိုင်ပါ။ အရေးပေါ်နံပါတ်တစ်ခုကို ခေါ်ဆိုပါ။" - "ခေါ်ဆိုရန် ကီးဘုတ်ကိုအသုံးပြုပါ" - "ခေါ်ဆိုမှု ခေတ္တရပ်ထားပါ" - "ခေါ်ဆိုမှုကို ဆက်လုပ်ပါ" - "ခေါ်ဆိုမှု အပြီးသတ်ပါ" - "နံပါတ်အကွက် ပြပါ" - "နံပါတ်အကွက် ဝှက်ထားပါ" - "အသံပိတ်ပါ" - "အသံပြန်ဖွင့်ပါ" - "ခေါ်ဆိုမှုထည့်ပါ" - "ခေါ်ဆိုမှုများကို ပေါင်းစည်းပါ" - "ဖလှယ်ပါ" - "ခေါ်ဆိုမှုများကို စီမံခန့်ခွဲပါ" - "အစည်းအဝေးခေါ်ဆိုမှုကို စီမံခန့်ခွဲပါ" - "မျက်နှာစုံညီစည်းဝေး ဖုန်းခေါ်ဆိုမှု" - "စီမံခန့်ခွဲပါ" - "အသံ" - "ဗီဒီယို ခေါ်ဆိုမှု" - "အသံခေါ်ဆိုမှုသို့ ပြောင်းပါ" - "ကင်မရာပြောင်းပါ" - "ကင်မရာဖွင့်ပါ" - "ကင်မရာပိတ်ပါ" - "နောက်ထပ် ရွေးစရာများ" - "ပလေယာ စပါပြီ" - "ပလေယာ ရပ်တန့်သွားပါပြီ" - "ကင်မရာအဆင်သင့် မဖြစ်သေးပါ" - "ကင်မရာအဆင်သင့်ဖြစ်ပါပြီ" - "အမျိုးအမည်မသိ ခေါ်ဆိုမှုအချိန်ကာလ" - "ဝန်ဆောင်မှု" - "စနစ်ထည့်သွင်းမှုပြုလုပ်ပါ" - "<မသတ်မှတ်ထားပါ>" - "အခြားခေါ်ဆိုမှုဆက်တင်များ" - "%s မှတစ်ဆင့် ခေါ်ဆိုခြင်း" - "%s မှတစ်ဆင့်အဝင်ခေါ်ဆိုမှု" - "အဆက်အသွယ်ဓာတ်ပုံ" - "တသီးတသန့်ချိတ်ဆက်ရန်" - "လိပ်စာရွေးပါ" - "သင့်ကိုယ်ပိုင် စာသား ရေးပါ..." - "မလုပ်တော့" - "ပို့ပါ" - "ဖြေကြားပါ" - "SMS ပို့ပါ" - "ငြင်းပယ်ပါ" - "ဗီဒီယိုခေါ်ဆိုမှုအဖြစ် ဖြေကြားပါ" - "အသံခေါ်ဆိုမှုအဖြစ် ဖြေကြားပါ" - "ဗီဒီယိုခေါ်ဆိုမှုကို လက်ခံပါ" - "ဗီဒီယိုခေါ်ဆိုမှုကို ငြင်းပယ်ပါ" - "ဗီဒီယိုထုတ်လွှင့်ခြင်းတောင်းဆိုမှုကို လက်ခံပါ" - "ဗီဒီယိုထုတ်လွှင့်ခြင်းတောင်းဆိုမှုကို ငြင်းပယ်ပါ" - "ဗီဒီယိုလက်ခံရရှိမှုတောင်းဆိုချက်ကို လက်ခံပါ" - "ဗီဒီယိုလက်ခံရရှိကြောင်းတောင်းဆိုမှုကို ငြင်းပယ်ပါ" - "%s အတွက် အပေါ်ကို ပွတ်ဆွဲပါ" - "%s အတွက် ဘယ်ဖက်ကို ပွတ်ဆွဲပါ" - "%s အတွက် ညာဖက်ကို ပွတ်ဆွဲပါ" - "%s အတွက် အောက်ကို ပွတ်ဆွဲပါ" - "တုန်ခါပါ" - "တုန်ခါပါ" - "အသံ" - "မူရင်း အသံ (%1$s)" - "ဖုန်းမြည်သံ" - "ဖုန်းမြည်စဉ် တုန်ခါပါ" - "ဖုန်းမြည်သံ & တုန်ခါသံ" - "အစည်းအဝေးခေါ်ဆိုမှုကို စီမံခန့်ခွဲပါ" - "အရေးပေါ်နံပါတ်" - "ပရိုဖိုင် ဓာတ်ပုံ" - "ကင်မရာ ပိတ်ပါ" - "%s မှတစ်ဆင့်" - "မှတ်ချက်ကို ပို့လိုက်ပါပြီ" - "မကြာသေးမီက မက်ဆေ့ဂျ်များ" - "စီးပွားရေး အချက်အလက်" - "%.1f မိုင်အကွာ" - "%.1f ကီလိုမီတာအကွာ" - "%1$s%2$s" - "%1$s - %2$s" - "%1$s%2$s" - "မနက်ဖြန် %s ၌ဖွင့်မည်" - "ယနေ့ %s ၌ဖွင့်မည်" - "%s ၌ပိတ်ပါမည်" - "ယနေ့ %s ၌ပိတ်ခဲ့သည်" - "ယခုဖွင့်ပါ" - "ယခုပိတ်ပါ" - "မသင်္ကာဖွယ်ရာ စပမ်းခေါ်ဆိုသူ" - "ဖုန်းခေါ်ဆိုမှု ပြီးဆုံးပါပြီ %1$s" - "ဤနံပါတ်သည် သင့်ထံ ပထမဆုံးခေါ်ဆိုသော နံပါတ်ဖြစ်သည်။" - "ယခုဖုန်းခေါ်ဆိုမှုသည် စပမ်းပို့သူဆီမှ ဖြစ်နိုင်သည်ဟု ထင်ပါသည်။" - "စပမ်းကို ပိတ်ဆို့ပါ/သတင်းပေးပို့ပါ" - "အဆက်အသွယ် ထည့်ပါ" - "စပမ်း မဟုတ်ပါ" - diff --git a/InCallUI/res/values-nb/strings.xml b/InCallUI/res/values-nb/strings.xml deleted file mode 100644 index d39e4d441..000000000 --- a/InCallUI/res/values-nb/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefon" - "På vent" - "Ukjent" - "Skjult nummer" - "Telefonkiosk" - "Konferansesamtale" - "Anropet ble avbrutt" - "Høyttaler" - "Telefonhøyttaler" - "Hodetelefoner med kabel" - "Bluetooth" - "Vil du sende disse lydene?\n" - "Sender lydene\n" - "Send" - "Ja" - "Nei" - "Erstatt jokertegn med" - "Konferansesamtale %s" - "Nummeret til talepostkassen" - "Ringer" - "Ringer på nytt" - "Konferansesamtale" - "Innkommende anrop" - "Innkommende jobbanrop" - "Anropet er avsluttet" - "På vent" - "Legger på" - "Anrop pågår" - "Nummeret mitt er %s" - "Kobler til video" - "Videoanrop" - "Ber om video" - "Kan ikke koble til videoanropet" - "Videoforespørselen er avvist" - "Tilbakeringingsnummeret ditt\n %1$s" - "Tilbakeringingsnummeret ditt for nødstilfeller\n %1$s" - "Ringer" - "Tapt anrop" - "Tapte anrop" - "%s tapte anrop" - "Tapt anrop fra %s" - "Pågående anrop" - "Pågående jobbanrop" - "Pågående Wi-Fi-anrop" - "Pågående jobbanrop via Wi-Fi" - "På vent" - "Innkommende anrop" - "Innkommende jobbanrop" - "Innkommende Wi-Fi-anrop" - "Innkommende jobbanrop via Wi-Fi" - "Innkommende videoanrop" - "Innkommende anrop fra en mulig useriøs oppringer" - "Innkommende videoforespørsel" - "Ny talepost" - "Ny talepost (%d)" - "Ring %s" - "Nummeret til talepostkassen er ukjent" - "Ingen tjeneste" - "Det valgte nettverket (%s) er ikke tilgjengelig" - "Svar" - "Legg på" - "Video" - "Uten video" - "Godta" - "Avvis" - "Ring tilbake" - "Melding" - "Samtale pågår på en annen enhet" - "Overfør samtalen" - "For å ringe, slå av flymodus først." - "Ikke registrert på nettverket." - "Mobilnettverket er ikke tilgjengelig." - "For å ringe, skriv inn et gyldig nummer." - "Kan ikke ringe." - "Starter MMI-sekvens …" - "Tjenesten støttes ikke." - "Kan ikke bytte samtaler." - "Kan ikke splitte opp anropet." - "Kan ikke overføre." - "Kan ikke opprette konferanse." - "Kan ikke avvise anropet." - "Kan ikke frigjøre samtale(r)." - "SIP-anrop" - "Nødanrop" - "Slår på radioen …" - "Ingen tjeneste. Prøver på nytt …" - "Kan ikke ringe. %s er ikke et nødnummer." - "Kan ikke ringe. Ring et nødnummer." - "Bruk tastaturet for å ringe" - "Sett anropet på vent" - "Gjenoppta anropet" - "Avslutt anropet" - "Vis tastaturet" - "Skjul tastaturet" - "Slå av lyden" - "Slå på lyden" - "Legg til anrop" - "Slå sammen anrop" - "Bytt" - "Administrer anrop" - "Administrer konferansesamtale" - "Konferansesamtale" - "Administrer" - "Lyd" - "Videoanrop" - "Bytt til taleanrop" - "Bytt kamera" - "Slå på kameraet" - "Slå av kameraet" - "Flere alternativer" - "Avspilleren har startet" - "Avspilleren har stoppet" - "Kameraet er ikke klart" - "Kameraet er klart" - "Ukjent anrop" - "Tjeneste" - "Konfigurering" - "<Ikke angitt>" - "Andre anropsinnstillinger" - "Ringer via %s" - "Innkommende via %s" - "kontaktbilde" - "aktivér privat samtale" - "velg kontakt" - "Skriv ditt eget" - "Avbryt" - "Send" - "Svar" - "Send SMS" - "Avslå" - "Svar med video" - "Svar uten video" - "Godta videoforespørselen" - "Avslå videoforespørselen" - "Godta forespørselen om å sende video" - "Avslå forespørselen om å sende video" - "Godta forespørselen om å motta video" - "Avslå forespørselen om å motta video" - "Dra opp for %s." - "Dra til venstre for å %s." - "Dra til høyre for å %s." - "Dra ned for å %s." - "Vibrering" - "Vibrering" - "Lyd" - "Standardlyd (%1$s)" - "Telefonringelyd" - "Vibrer når telefonen ringer" - "Ringelyd og vibrering" - "Administrer konferansesamtale" - "Nødnummer" - "Profilbilde" - "Kameraet er slått av" - "via %s" - "Notatet er sendt" - "Nylige meldinger" - "Informasjon om bedriften" - "%.1f mile unna" - "%.1f km unna" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "Åpner i morgen kl. %s" - "Åpner i dag kl. %s" - "Stenger kl. %s" - "Stengte i dag kl. %s" - "Åpen nå" - "Stengt nå" - "Mulig useriøst anrop" - "Samtalen ble avsluttet %1$s" - "Dette er første gang du blir oppringt fra dette nummeret." - "Vi har mistanke om at dette anropet kommer fra en useriøs oppringer." - "Blokkér/rapportér" - "Legg til som kontakt" - "Ikke useriøs" - diff --git a/InCallUI/res/values-ne/strings.xml b/InCallUI/res/values-ne/strings.xml deleted file mode 100644 index 71c1ccae9..000000000 --- a/InCallUI/res/values-ne/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "फोन" - "होल्डमा" - "अज्ञात" - "निजी नम्बर" - "पेफोन" - "सम्मेलन कल" - "कल ड्रप भयो" - "स्पिकर" - "हेन्डसेट इयरपिस" - "तारसहितको हेडसेट" - "ब्लुटुथ" - "निम्न टोनहरू पठाउने हो?\n" - "टोनहरू\n पठाउँदै" - "पठाउनुहोस्" - "हो" - "होइन" - "यसलाई वाइल्ड क्यारेक्टर राखेर बदल्नुहोस्" - "सम्मेलन कल %s" - "भ्वाइस मेल नम्बर" - "डायल गर्दै" - "पुन: डायल गर्दै" - "सम्मेलन कल" - "आगमन कल" - "कार्यालयबाट आएको कल" - "कल अन्त्य भयो" - "होल्डमा छ" - "फोन काट्दै" - "कलमा" - "मेरो नम्बर %s हो" - "भिडियो जडान गरिँदै" - "भिडियो कल" - "भिडियोका लागि अनुरोध गर्दै" - "भिडियो कलमा जडान गर्न सक्दैन" - "भिडियो अनुरोध अस्वीकार गरियो" - "तपाईंको कलब्याक नम्बर\n %1$s" - "तपाईंको आपतकालीन कलब्याक नम्बर\n %1$s" - "डायल गर्दै" - "छुटेको कल" - "छुटेका कलहरू" - "%s छुटेका कलहरू" - "%s बाट आएको छुटेको कल" - "चलिरहेको कल" - "चालु रहेको कार्यालयको कल" - "चालु रहेको WI-Fi कल" - "Wi-Fi मार्फत चालु रहेको कार्यालयको कल" - "होल्डमा" - "आगमन कल" - "कार्यालयबाट आएको कल" - "आगमन Wi-Fi कल" - "Wi-Fi मार्फत कार्यालयबाट आएको कल" - "आगमन भिडियो कल" - "शंकास्पद आगमन स्प्याम कल" - "आगमन भिडियो अनुरोध" - "नयाँ भ्वाइस मेल" - "नयाँ भ्वाइसमेल (%d)" - "%s मा डायल गर्नुहोस्" - "भ्वाइस मेल नम्बर अज्ञात छ" - "कुनै सेवा छैन" - "चयन गरिएको नेटवर्क (%s) अनुपलब्ध छ" - "जवाफ दिनुहोस्" - "राख्नुहोस्" - "भिडियो" - "आवाज" - "स्वीकार गर्नुहोस्" - "खारेज गर्नुहोस्" - "कल फर्काउने" - "सन्देश" - "अर्को यन्त्रमा चलिरहेको कल" - "कल स्थानान्तरण गर्नुहोस्" - "कल गर्नका लागि, पहिले हवाइजहाज मोड बन्द गर्नुहोस्।" - "नेटवर्कमा दर्ता भएको छैन।" - "सेलुलर नेटवर्क उपलब्ध छैन।" - "एक कल गर्नको लागि, मान्य नम्बर प्रविष्ट गर्नुहोस्।" - "कल गर्न सकिंदैन।" - "MMI अनुक्रम सुरु गर्दै..." - "सेवा समर्थित छैन।" - "कल स्विच गर्न सक्दैन।" - "कल अलग गर्न सक्दैन।" - "ट्रान्सफर गर्न सक्दैन।" - "सम्मेलन गर्न सक्दैन।" - "कल अस्वीकार गर्न सक्दैन।" - "कल (हरू) जारी गर्न सकिंदैन।" - "SIP कल" - "आपतकालीन कल" - "रेडियो खोल्दै..." - "कुनै सेवा छैन। फेरि प्रयास गर्दै..." - "कल गर्न सकिंदैन। %s आपतकालीन नम्बर होइन।" - "कल गर्न सकिंदैन। आपतकालीन नम्बर डायल गर्नुहोस्।" - "डायल गर्न किबोर्ड प्रयोग गर्नुहोस्" - "कललाई होल्ड गर्नुहोस्" - "कललाई पुन: निरन्तरता दिनुहोस्" - "कल अन्त्य गर्नुहोस्" - "डायलप्याड देखाउनुहोस्" - "डायलप्याड लुकाउनुहोस्" - "मौन" - "अनम्यूट गर्नुहोस्" - "कल थप्नुहोस्" - "कलहरू मर्ज गर्नुहोस्" - "स्वाप" - "कलहरूको प्रबन्ध मिलाउनुहोस्" - "सम्मेलन कलको प्रबन्ध मिलाउनहोस्" - "सम्मेलन कल" - "व्यवस्थापन गर्नुहोस्" - "अडियो" - "भिडियो कल" - "आवाज कलमा परिवर्तन गर्नुहोस्" - "क्यामेरा स्विच गर्नुहोस्" - "क्यामेरालाई सक्रिय गर्नुहोस्" - "क्यामेरालाई निष्क्रिय पार्नुहोस्" - "थप विकल्पहरू" - "प्लेयर सुरु भयो" - "प्लेयर रोकियो" - "क्यामेरा तयार छैन" - "क्यामेरा तयार छ" - "अज्ञात कल सत्र घटना" - "सेवा" - "सेटअप" - "<सेट गरिएको छैन>" - "अन्य कल सेटिङहरू" - "%s मार्फत कल गर्दै" - "%s मार्फत आगमन" - "सम्पर्क तस्बिर" - "निजी कलमा जानुहोस्" - "सम्पर्क चयन गर्नुहोस्" - "तपाईंको आफ्नै लेख्नुहोस्..." - "रद्द गर्नुहोस्" - "पठाउनुहोस्" - "जवाफ दिनुहोस्" - "SMS पठाउनुहोस्" - "अस्वीकार गर्नुहोस्" - "भिडियो कलको रूपमा जवाफ दिनुहोस्" - "अडियो कलको रूपमा जवाफ दिनुहोस्" - "भिडियो अनुरोध स्वीकार गर्नुहोस्" - "भिडियो अनुरोध अस्वीकार गर्नुहोस्" - "भिडियो प्रसारण गर्ने अनुरोध स्वीकार गर्नुहोस्" - "भिडियो प्रसारण गर्ने अनुरोध अस्वीकार गर्नुहोस्" - "भिडियो प्राप्त गर्ने अनुरोधलाई स्वीकार गर्नुहोस्" - "भिडियो प्राप्त गर्ने अनुरोध अस्वीकार गर्नुहोस्" - "%s को लागि माथि स्लाइड गर्नुहोस्।" - "%s को लागि बायाँ स्लाइड गर्नुहोस्।" - "%s को लागि दायाँ स्लाइड गर्नुहोस्।" - "%s को लागि तल स्लाइड गर्नुहोस्।" - "कम्पन हुने" - "कम्पन हुने" - "आवाज" - "पूर्वनिर्धारित ध्वनि (%1$s)" - "फोनको रिङटोन" - "घन्टी बज्दा कम्पन गराउनुहोस्" - "रिङटोन & कम्पन" - "सम्मेलन कलको प्रबन्ध मिलाउनुहोस्" - "आपतकालीन नम्बर" - "प्रोफाइल तस्बिर" - "क्यामेरा बन्द छ" - "%s बाट" - "नोट पठाइयो" - "भर्खरैका सन्देशहरू" - "व्यवसाय बारे जानकारी" - "%.1f माइल टाढा" - "%.1f किलोमिटर टाढा" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "भोलि %s मा खुल्छ" - "आज %s मा खुल्छ" - "%s मा बन्द हुन्छ" - "आज %s मा बन्द भयो" - "अहिले खुला छ" - "अब बन्द भयो" - "शंकास्पद स्प्याम कलर" - "कल समाप्त भयो %1$s" - "यो नम्बरबाट तपाईँलाई फोन आएको यो पहिलो पटक हो।" - "हामीले यो कल स्प्यामर हुन सक्ने आशङ्का गर्‍यौँ।" - "स्प्याम रोक्नु्/रिपोर्ट गर्ने" - "सम्पर्क थप्नुहोस्" - "स्प्याम होइन" - diff --git a/InCallUI/res/values-nl/strings.xml b/InCallUI/res/values-nl/strings.xml deleted file mode 100644 index 9eaf556c0..000000000 --- a/InCallUI/res/values-nl/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefoon" - "In de wacht" - "Onbekend" - "Privénummer" - "Betaaltelefoon" - "Telefonische vergadering" - "Oproep beëindigd" - "Luidspreker" - "Oortelefoon van handset" - "Bedrade headset" - "Bluetooth" - "De volgende tonen verzenden?\n" - "Nummers verzenden\n" - "Verzenden" - "Ja" - "Nee" - "Jokerteken vervangen door" - "Telefonische vergadering %s" - "Voicemailnummer" - "Kiezen" - "Opnieuw bellen" - "Telefonische vergadering" - "Inkomende oproep" - "Inkom. zakelijke oproep" - "Oproep beëindigd" - "In de wacht" - "Ophangen" - "In gesprek" - "Mijn nummer is %s" - "Verbinding maken met video" - "Videogesprek" - "Video aanvragen" - "Kan geen videogesprek starten" - "Videoverzoek geweigerd" - "Je terugbelnummer\n %1$s" - "Je terugbelnummer bij alarm\n %1$s" - "Kiezen" - "Gemiste oproep" - "Gemiste oproepen" - "%s gemiste oproepen" - "Gemiste oproep van %s" - "Actieve oproep" - "Actieve zakelijke oproep" - "Actieve wifi-oproep" - "Actieve zakelijke oproep via wifi" - "In de wacht" - "Inkomende oproep" - "Inkomende zakelijke oproep" - "Inkomende wifi-oproep" - "Inkomende zakelijke oproep via wifi" - "Inkomend videogesprek" - "Inkomende vermoedelijke spamoproep" - "Inkomend videoverzoek" - "Nieuwe voicemail" - "Nieuwe voicemail (%d)" - "%s bellen" - "Voicemailnummer onbekend" - "Geen service" - "Geselecteerd netwerk (%s) niet beschikbaar" - "Beantwoorden" - "Ophangen" - "Video" - "Spraak" - "Accepteren" - "Sluiten" - "Terugbellen" - "Bericht" - "Actief gesprek op een ander apparaat" - "Gesprek doorschakelen" - "Als je wilt bellen, moet je eerst de vliegtuigmodus uitschakelen." - "Niet geregistreerd op netwerk." - "Mobiel netwerk niet beschikbaar." - "Als je wilt bellen, moet je een geldig nummer invoeren." - "Kan niet bellen." - "MMI-reeks starten..." - "Service wordt niet ondersteund." - "Kan niet schakelen tussen oproepen." - "Kan oproep niet scheiden." - "Kan niet doorschakelen." - "Telefonische vergadering niet mogelijk." - "Kan oproep niet weigeren." - "Kan oproep(en) niet vrijgeven." - "SIP-oproep" - "Noodoproep" - "Radio inschakelen…" - "Geen bereik. Opnieuw proberen…" - "Kan niet bellen. %s is geen alarmnummer." - "Kan niet bellen. Bel een alarmnummer." - "Toetsen gebruiken om te bellen" - "Oproep in de wacht zetten" - "Oproep hervatten" - "Oproep beëindigen" - "Toetsenblok weergeven" - "Toetsenblok verbergen" - "Dempen" - "Dempen opheffen" - "Oproep toevoegen" - "Samenvoegen" - "Wisselen" - "Oproepen beheren" - "Telef. vergadering beheren" - "Telefonische vergadering" - "Beheren" - "Audio" - "Vid.gespr." - "Wijzigen in spraakoproep" - "Van camera wisselen" - "Camera inschakelen" - "Camera uitschakelen" - "Meer opties" - "Speler gestart" - "Speler gestopt" - "Camera niet gereed" - "Camera gereed" - "Onbekende oproepsessiegebeurtenis" - "Service" - "Configuratie" - "<Niet ingesteld>" - "Andere instellingen voor bellen" - "Bellen via %s" - "Inkomend via %s" - "contactfoto" - "privé" - "contact selecteren" - "Eigen reactie opstellen..." - "Annuleren" - "Verzenden" - "Beantwoorden" - "Sms verzenden" - "Weigeren" - "Beantwoorden als videogesprek" - "Beantwoorden als audiogesprek" - "Videoverzoek accepteren" - "Videoverzoek weigeren" - "Verzoek voor video-overdracht accepteren" - "Verzoek voor video-overdracht weigeren" - "Verzoek voor video-ontvangst accepteren" - "Verzoek voor video-ontvangst weigeren" - "Veeg omhoog voor %s." - "Veeg naar links voor %s." - "Veeg naar rechts voor %s." - "Veeg omlaag voor %s." - "Trillen" - "Trillen" - "Geluid" - "Standaardgeluid (%1$s)" - "Beltoon telefoon" - "Trillen bij bellen" - "Beltoon en trillen" - "Telefonische vergadering beheren" - "Alarmnummer" - "Profielfoto" - "Camera uit" - "via %s" - "Notitie verzonden" - "Recente berichten" - "Bedrijfsinformatie" - "%.1f mijl hiervandaan" - "%.1f km hiervandaan" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "Gaat morgen open om %s" - "Gaat vandaag open om %s" - "Sluit om %s" - "Vandaag gesloten vanaf %s" - "Nu geopend" - "Nu gesloten" - "Vermoedelijke spambeller" - "Oproep beëindigd %1$s" - "Dit is de eerste keer dat je bent gebeld door dit nummer." - "We vermoedden dat deze oproep afkomstig was van een spammer." - "Blokk./spam melden" - "Contact toevoegen" - "Geen spam" - diff --git a/InCallUI/res/values-pa/strings.xml b/InCallUI/res/values-pa/strings.xml deleted file mode 100644 index 318fc4cdc..000000000 --- a/InCallUI/res/values-pa/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "ਫੋਨ" - "ਰੋਕੀ ਗਈ" - "ਅਗਿਆਤ" - "ਨਿੱਜੀ ਨੰਬਰ" - "ਪੇ-ਫੋਨ" - "ਕਾਨਫਰੰਸ ਕਾਲ" - "ਕਾਲ ਡ੍ਰੌਪ ਹੋਈ" - "ਸਪੀਕਰ" - "ਹੈੱਡਸੈੱਟ ਈਯਰਪੀਸ" - "ਵਾਇਰ ਵਾਲਾ ਹੈੱਡਸੈੱਟ" - "ਬਲੂਟੁੱਥ" - "ਕੀ ਅੱਗੇ ਦਿੱਤੀਆਂ ਗਈਆਂ ਧੁਨੀਆਂ ਭੇਜਣੀਆਂ ਹਨ?\n" - "ਧੁਨੀਆਂ ਭੇਜੀਆਂ ਜਾ ਰਹੀਆਂ ਹਨ\n" - "ਭੇਜੋ" - "ਹਾਂ" - "ਨਹੀਂ" - "ਵਾਈਲਡ ਅੱਖਰ ਨੂੰ ਇਸ ਨਾਲ ਬਦਲੋ" - "ਕਾਨਫਰੰਸ ਕਾਲ %s" - "ਵੌਇਸਮੇਲ ਨੰਬਰ" - "ਡਾਇਲ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ" - "ਦੁਬਾਰਾ ਡਾਇਲ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ" - "ਕਾਨਫਰੰਸ ਕਾਲ" - "ਆ ਰਹੀ ਕਾਲ" - "ਕੰਮ ਸੰਬੰਧਿਤ ਆ ਰਹੀ ਕਾਲ" - "ਕਾਲ ਸਮਾਪਤ ਹੋਈ" - "ਰੋਕੀ ਗਈ" - "ਰੋਕੀ ਜਾ ਰਹੀ ਹੈ" - "ਚਾਲੂ ਕਾਲ" - "ਮੇਰਾ ਨੰਬਰ %s ਹੈ" - "ਵੀਡੀਓ ਨੂੰ ਕਨੈਕਟ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ" - "ਵੀਡੀਓ ਕਾਲ" - "ਵੀਡੀਓ ਲਈ ਬੇਨਤੀ ਕੀਤੀ ਜਾ ਰਹੀ ਹੈ" - "ਵੀਡੀਓ ਕਾਲ ਕਨੈਕਟ ਨਹੀਂ ਕੀਤੀ ਜਾ ਸਕਦੀ" - "ਵੀਡੀਓ ਬੇਨਤੀ ਅਸਵੀਕਾਰ ਕੀਤੀ ਗਈ" - "ਤੁਹਾਡਾ ਕਾਲਬੈਕ ਨੰਬਰ \n %1$s" - "ਤੁਹਾਡਾ ਐਮਰਜੈਂਸੀ ਕਾਲਬੈਕ ਨੰਬਰ\n %1$s" - "ਡਾਇਲ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ" - "ਖੁੰਝੀ ਹੋਈ ਕਾਲ" - "ਖੁੰਝੀਆਂ ਹੋਈਆਂ ਕਾਲਾਂ" - "%s ਖੁੰਝੀਆਂ ਹੋਈਆਂ ਕਾਲਾਂ" - "%s ਤੋਂ ਖੁੰਝੀ ਹੋਈ ਕਾਲ" - "ਜਾਰੀ ਕਾਲ" - "ਕੰਮ ਸੰਬੰਧਿਤ ਜਾਰੀ ਕਾਲ" - "ਜਾਰੀ Wi-Fi ਕਾਲ" - "ਕੰਮ ਸੰਬੰਧਿਤ ਜਾਰੀ Wi-Fi ਕਾਲ" - "ਰੋਕੀ ਗਈ" - "ਆ ਰਹੀ ਕਾਲ" - "ਕੰਮ ਸੰਬੰਧਿਤ ਆ ਰਹੀ ਕਾਲ" - "ਆ ਰਹੀ Wi-Fi ਕਾਲ" - "ਕੰਮ ਸੰਬੰਧਿਤ ਆ ਰਹੀ Wi-Fi ਕਾਲ" - "ਆ ਰਹੀ ਵੀਡੀਓ ਕਾਲ" - "ਸ਼ੱਕੀ ਸਪੈਮ ਕਾਲ ਆ ਰਹੀ ਹੈ" - "ਆ ਰਹੀ ਵੀਡੀਓ ਬੇਨਤੀ" - "ਨਵੀਂ ਵੌਇਸਮੇਲ" - "ਨਵੀਂ ਵੌਇਸਮੇਲ (%d)" - "%s ਡਾਇਲ ਕਰੋ" - "ਵੌਇਸਮੇਲ ਨੰਬਰ ਅਗਿਆਤ" - "ਕੋਈ ਸੇਵਾ ਨਹੀਂ" - "ਚੁਣਿਆ ਗਿਆ ਨੈੱਟਵਰਕ (%s) ਉਪਲਬਧ ਨਹੀਂ ਹੈ" - "ਜਵਾਬ ਦਿਓ" - "ਰੋਕੋ" - "ਵੀਡੀਓ" - "ਵੌਇਸ" - "ਸਵੀਕਾਰ ਕਰੋ" - "ਰੱਦ ਕਰੋ" - "ਵਾਪਸ ਕਾਲ ਕਰੋ" - "ਸੁਨੇਹਾ" - "ਕਿਸੇ ਹੋਰ ਡੀਵਾਈਸ \'ਤੇ ਜਾਰੀ ਕਾਲ" - "ਕਾਲ ਟ੍ਰਾਂਸਫਰ ਕਰੋ" - "ਇੱਕ ਕਾਲ ਕਰਨ ਲਈ, ਪਹਿਲਾਂ ਜਹਾਜ਼ ਮੋਡ ਬੰਦ ਕਰੋ।" - "ਨੈੱਟਵਰਕ \'ਤੇ ਰਜਿਸਟਰ ਨਹੀਂ।" - "ਸੈਲਿਊਲਰ ਨੈੱਟਵਰਕ ਉਪਲਬਧ ਨਹੀਂ ਹੈ।" - "ਇੱਕ ਕਾਲ ਕਰਨ ਲਈ, ਇੱਕ ਵੈਧ ਨੰਬਰ ਦਾਖਲ ਕਰੋ।" - "ਕਾਲ ਨਹੀਂ ਕੀਤੀ ਜਾ ਸਕਦੀ।" - "MMI ਕੜੀ ਨੂੰ ਸ਼ੁਰੂ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ…" - "ਸੇਵਾ ਸਮਰਥਿਤ ਨਹੀਂ ਹੈ।" - "ਕਾਲਾਂ ਸਵਿੱਚ ਨਹੀਂ ਕੀਤੀਆਂ ਜਾ ਸਕਦੀਆਂ।" - "ਵੱਖਰੇ ਤੌਰ \'ਤੇ ਕਾਲ ਨਹੀਂ ਕੀਤੀ ਜਾ ਸਕਦੀ।" - "ਕਾਲ ਟ੍ਰਾਂਸਫਰ ਨਹੀਂ ਕੀਤੀ ਜਾ ਸਕਦੀ।" - "ਕਾਨਫਰੰਸ ਕਾਲ ਨਹੀਂ ਕੀਤੀ ਜਾ ਸਕਦੀ।" - "ਕਾਲ ਰੱਦ ਨਹੀਂ ਕੀਤੀ ਜਾ ਸਕਦੀ।" - "ਕਾਲ(ਲਾਂ) ਰੀਲੀਜ਼ ਨਹੀਂ ਕੀਤੀਆਂ ਜਾ ਸਕਦੀਆਂ।" - "SIP ਕਾਲ" - "ਐਮਰਜੈਂਸੀ ਕਾਲ" - "ਰੇਡੀਓ ਚਾਲੂ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ…" - "ਕੋਈ ਸੇਵਾ ਨਹੀਂ। ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕੀਤੀ ਜਾ ਰਹੀ ਹੈ…" - "ਕਾਲ ਨਹੀਂ ਕੀਤੀ ਜਾ ਸਕਦੀ। %s ਇੱਕ ਐਮਰਜੈਂਸੀ ਨੰਬਰ ਨਹੀਂ ਹੈ।" - "ਕਾਲ ਨਹੀਂ ਕੀਤੀ ਜਾ ਸਕਦੀ। ਇੱਕ ਐਮਰਜੈਂਸੀ ਨੰਬਰ ਡਾਇਲ ਕਰੋ।" - "ਡਾਇਲ ਕਰਨ ਲਈ ਕੀ-ਬੋਰਡ ਵਰਤੋ" - "ਕਾਲ ਰੋਕੋ" - "ਕਾਲ ਮੁੜ-ਸ਼ੁਰੂ ਕਰੋ" - "ਕਾਲ ਸਮਾਪਤ ਕਰੋ" - "ਡਾਇਲਪੈਡ ਵਿਖਾਓ" - "ਡਾਇਲਪੈਡ ਲੁਕਾਓ" - "ਮਿਊਟ ਕਰੋ" - "ਅਣਮਿਊਟ ਕਰੋ" - "ਕਾਲ ਸ਼ਾਮਲ ਕਰੋ" - "ਕਾਲਾਂ ਸ਼ਾਮਲ ਕਰੋ" - "ਅਦਲਾ-ਬਦਲੀ ਕਰੋ" - "ਕਾਲਾਂ ਦਾ ਪ੍ਰਬੰਧਨ ਕਰੋ" - "ਕਾਨਫਰੰਸ ਕਾਲ ਦਾ ਪ੍ਰਬੰਧਨ ਕਰੋ" - "ਕਾਨਫਰੰਸ ਕਾਲ" - "ਪ੍ਰਬੰਧਨ ਕਰੋ" - "ਔਡੀਓ" - "ਵੀਡੀਓ ਕਾਲ" - "ਵੌਇਸ ਕਾਲ ਵਿੱਚ ਬਦਲੋ" - "ਕੈਮਰੇ \'ਤੇ ਬਦਲੋ" - "ਕੈਮਰਾ ਚਾਲੂ ਕਰੋ" - "ਕੈਮਰਾ ਬੰਦ ਕਰੋ" - "ਹੋਰ ਚੋਣਾਂ" - "ਪਲੇਅਰ ਸ਼ੁਰੂ ਹੋ ਗਿਆ" - "ਪਲੇਅਰ ਰੁਕ ਗਿਆ" - "ਕੈਮਰਾ ਤਿਆਰ ਨਹੀਂ ਹੈ" - "ਕੈਮਰਾ ਤਿਆਰ ਹੈ" - "ਅਗਿਆਤ ਕਾਲ ਸੈਸ਼ਨ ਵਰਤਾਰਾ" - "ਸੇਵਾ" - "ਸਥਾਪਨਾ" - "<ਸੈੱਟ ਨਹੀਂ>" - "ਹੋਰ ਕਾਲ ਸੈਟਿੰਗਾਂ" - "%s ਰਾਹੀਂ ਕਾਲ ਕੀਤੀ ਜਾ ਰਹੀ ਹੈ" - "%s ਰਾਹੀਂ ਆ ਰਹੀਆਂ ਕਾਲਾਂ" - "ਸੰਪਰਕ ਫ਼ੋਟੋ" - "ਨਿੱਜੀ ਵਿੱਚ ਬਦਲੋ" - "ਸੰਪਰਕ ਚੁਣੋ" - "ਆਪਣੇ ਆਪ ਲਿਖੋ..." - "ਰੱਦ ਕਰੋ" - "ਭੇਜੋ" - "ਜਵਾਬ ਦਿਓ" - "SMS ਭੇਜੋ" - "ਅਸਵੀਕਾਰ ਕਰੋ" - "ਵੀਡੀਓ ਕਾਲ ਵਜੋਂ ਜਵਾਬ ਦਿਓ" - "ਔਡੀਓ ਕਾਲ ਵਜੋਂ ਜਵਾਬ ਦਿਓ" - "ਵੀਡੀਓ ਬੇਨਤੀ ਸਵੀਕਾਰ ਕਰੋ" - "ਵੀਡੀਓ ਬੇਨਤੀ ਨੂੰ ਅਸਵੀਕਾਰ ਕਰੋ" - "ਵੀਡੀਓ ਟ੍ਰਾਂਸਮਿਟ ਬੇਨਤੀ ਨੂੰ ਸਵੀਕਾਰ ਕਰੋ" - "ਵੀਡੀਓ ਟ੍ਰਾਂਸਮਿਟ ਬੇਨਤੀ ਨੂੰ ਅਸਵੀਕਾਰ ਕਰੋ" - "ਵੀਡੀਓ ਪ੍ਰਾਪਤ ਕਰਨ ਦੀ ਬੇਨਤੀ ਨੂੰ ਸਵੀਕਾਰ ਕਰੋ" - "ਵੀਡੀਓ ਪ੍ਰਾਪਤ ਕਰਨ ਦੀ ਬੇਨਤੀ ਨੂੰ ਅਸਵੀਕਾਰ ਕਰੋ" - "%s ਲਈ ਉੱਤੇ ਸਲਾਈਡ ਕਰੋ।" - "%s ਲਈ ਖੱਬੇ ਪਾਸੇ ਸਲਾਈਡ ਕਰੋ।" - "%s ਲਈ ਸੱਜੇ ਪਾਸੇ ਸਲਾਈਡ ਕਰੋ।" - "%s ਲਈ ਹੇਠਾਂ ਸਲਾਈਡ ਕਰੋ।" - "ਥਰਥਰਾਹਟ ਕਰੋ" - "ਥਰਥਰਾਹਟ ਕਰੋ" - "ਧੁਨੀ" - "ਪੂਰਵ-ਨਿਰਧਾਰਤ ਧੁਨੀ (%1$s)" - "ਫੋਨ ਰਿੰਗਟੋਨ" - "ਘੰਟੀ ਵੱਜਣ \'ਤੇ ਥਰਥਰਾਹਟ ਕਰੋ" - "ਰਿੰਗਟੋਨ ਅਤੇ ਥਰਥਰਾਹਟ" - "ਕਾਨਫਰੰਸ ਕਾਲ ਦਾ ਪ੍ਰਬੰਧਨ ਕਰੋ" - "ਐਮਰਜੈਂਸੀ ਨੰਬਰ" - "ਪ੍ਰੋਫਾਈਲ ਫ਼ੋਟੋ" - "ਕੈਮਰਾ ਬੰਦ ਹੈ" - "%s ਰਾਹੀਂ" - "ਨੋਟ-ਕਥਨ ਭੇਜਿਆ ਗਿਆ" - "ਹਾਲੀਆ ਸੁਨੇਹੇ" - "ਵਪਾਰ ਜਾਣਕਾਰੀ" - "%.1f ਮੀਲ ਦੂਰ" - "%.1f ਕਿ.ਮੀ. ਦੂਰ" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "ਕੱਲ੍ਹ %s ਵਜੇ ਖੁੱਲ੍ਹੇਗਾ" - "ਅੱਜ %s ਵਜੇ ਖੁੱਲ੍ਹੇਗਾ" - "%s ਵਜੇ ਬੰਦ ਹੋਵੇਗਾ" - "ਅੱਜ %s ਵਜੇ ਬੰਦ ਹੋਇਆ" - "ਹੁਣ ਖੁੱਲ੍ਹਾ ਹੈ" - "ਹੁਣ ਬੰਦ ਹੈ" - "ਸ਼ੱਕੀ ਸਪੈਮ ਕਾਲਰ" - "%1$s ਦੀ ਕਾਲ ਸਮਾਪਤ ਹੋਈ" - "ਇਸ ਨੰਬਰ ਤੋਂ ਤੁਹਾਨੂੰ ਪਹਿਲੀ ਵਾਰ ਕਾਲ ਪ੍ਰਾਪਤ ਹੋਈ ਹੈ।" - "ਸਾਨੂੰ ਇਹ ਕਾਲ ਇੱਕ ਸਪੈਮਰ ਜਾਪਦੀ ਸੀ।" - "ਸਪੈਮ ਨੂੰ ਬਲੌਕ ਕਰੋ/ਰਿਪੋਰਟ ਕਰੋ" - "ਸੰਪਰਕ ਸ਼ਾਮਲ ਕਰੋ" - "ਸਪੈਮ ਨਹੀਂ" - diff --git a/InCallUI/res/values-pl/strings.xml b/InCallUI/res/values-pl/strings.xml deleted file mode 100644 index f9f78ec79..000000000 --- a/InCallUI/res/values-pl/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefon" - "Wstrzymane" - "Nieznany" - "Numer prywatny" - "Automat telefoniczny" - "Połączenie konferencyjne" - "Połączenie przerwane" - "Głośnik" - "Słuchawka telefonu" - "Przewodowy zestaw słuchawkowy" - "Bluetooth" - "Wysłać te tony?\n" - "Wysyłanie tonów\n" - "Wyślij" - "Tak" - "Nie" - "Zastąp symbol wieloznaczny" - "Połączenie konferencyjne: %s" - "Numer poczty głosowej" - "Wybieranie" - "Ponowne wybieranie numeru" - "Połączenie konferencyjne" - "Połączenie przychodzące" - "Przychodzące połączenie służbowe" - "Połączenie zakończone" - "Wstrzymane" - "Rozłączanie" - "Trwa połączenie" - "Mój numer to %s" - "Rozpoczynanie rozmowy wideo" - "Rozmowa wideo" - "Wysyłanie żądania wideo" - "Nie można nawiązać połączenia wideo" - "Żądanie wideo zostało odrzucone" - "Twój numer oddzwaniania\n %1$s" - "Twój numer oddzwaniania dla połączeń alarmowych\n %1$s" - "Wybieranie" - "Nieodebrane połączenie" - "Nieodebrane połączenia" - "Nieodebrane połączenia: %s" - "Nieodebrane połączenie od: %s" - "Trwa połączenie" - "Trwa połączenie służbowe" - "Trwa połączenie przez Wi-Fi" - "Trwa połączenie służbowe przez Wi-Fi" - "Wstrzymane" - "Połączenie przychodzące" - "Przychodzące połączenie służbowe" - "Przychodzące połączenie przez Wi-Fi" - "Przychodzące połączenie służbowe przez Wi-Fi" - "Przychodząca rozmowa wideo" - "Przychodzące połączenie podejrzanie o spam" - "Przychodzące żądanie wideo" - "Nowa poczta głosowa" - "Nowa poczta głosowa (%d)" - "Wybierz numer %s" - "Nieznany numer poczty głosowej" - "Brak usługi" - "Wybrana sieć (%s) jest niedostępna" - "Odbierz" - "Rozłącz" - "Wideo" - "Głos" - "Zaakceptuj" - "Odrzuć" - "Oddzwoń" - "Wyślij SMS-a" - "Trwająca rozmowa na innym urządzeniu" - "Przełącz rozmowę" - "Aby rozpocząć połączenie, wyłącz najpierw tryb samolotowy." - "Nie zarejestrowano w sieci." - "Sieć komórkowa jest niedostępna." - "Aby zadzwonić, wybierz prawidłowy numer." - "Nie można dzwonić." - "Rozpoczynam sekwencję MMI…" - "Usługa nie jest obsługiwana." - "Nie można przełączyć połączeń." - "Nie można rozdzielić połączenia." - "Nie można przekazać." - "Nie można nawiązać połączenia konferencyjnego." - "Nie można odrzucić połączenia." - "Nie można zwolnić połączeń." - "Połączenie SIP" - "Połączenie alarmowe" - "Włączam radio…" - "Brak sieci. Próbuję ponownie…" - "Nie można dzwonić. %s nie jest numerem alarmowym." - "Nie można dzwonić. Wybierz numer alarmowy." - "Aby zadzwonić, użyj klawiatury" - "Wstrzymaj połączenie" - "Wznów połączenie" - "Zakończ połączenie" - "Pokaż klawiaturę" - "Ukryj klawiaturę" - "Wycisz" - "Wyłącz wyciszenie" - "Dodaj połączenie" - "Scal połączenia" - "Przełącz" - "Zarządzaj połączeniami" - "Zarządzaj połączeniem konferencyjnym" - "Połączenie konferencyjne" - "Zarządzaj" - "Dźwięk" - "Rozmowa wideo" - "Zmień na połączenie głosowe" - "Przełącz kamerę" - "Włącz kamerę" - "Wyłącz kamerę" - "Więcej opcji" - "Odtwarzacz włączony" - "Odtwarzacz zatrzymany" - "Kamera niegotowa" - "Kamera gotowa" - "Nieznane zdarzenie sesji połączenia" - "Usługa" - "Konfiguracja" - "<Nie ustawiono>" - "Inne ustawienia połączeń" - "Nawiązywanie połączenia przez %s" - "Przychodzące z sieci %s" - "zdjęcie kontaktu" - "przejdź do rozmowy prywatnej" - "wybierz kontakt" - "Napisz własną..." - "Anuluj" - "Wyślij" - "Odbierz" - "Wyślij SMS-a" - "Odrzuć" - "Odbierz jako rozmowę wideo" - "Odbierz jako rozmowę audio" - "Zaakceptuj żądanie wideo" - "Odrzuć żądanie wideo" - "Zaakceptuj wysyłanie obrazu wideo" - "Odrzuć wysyłanie obrazu wideo" - "Zaakceptuj odbieranie obrazu wideo" - "Odrzuć odbieranie obrazu wideo" - "Przesuń w górę: %s." - "Przesuń w lewo: %s." - "Przesuń w prawo: %s." - "Przesuń w dół: %s." - "Wibracje" - "Wibracje" - "Dźwięk" - "Domyślny dźwięk (%1$s)" - "Dzwonek telefonu" - "Wibracje z dzwonkiem" - "Dzwonek i wibracje" - "Zarządzaj połączeniem konferencyjnym" - "Numer alarmowy" - "Zdjęcie profilowe" - "Kamera wyłączona" - "z %s" - "Notatka wysłana" - "Ostatnie wiadomości" - "Informacje o firmie" - "%.1f mil(e) stąd" - "%.1f km stąd" - "%1$s, %2$s" - "%1$s-%2$s" - "%1$s, %2$s" - "Otwarte jutro od %s" - "Otwarte dzisiaj od %s" - "Zamknięte od %s" - "Zamknięte dzisiaj od %s" - "Teraz otwarte" - "Teraz zamknięte" - "Podejrzenie spamu" - "Połączenie zakończone (%1$s)" - "To pierwsze połączenie z tego numeru." - "Podejrzewamy, że to połączenie to spam." - "Zablokuj/zgłoś spam" - "Dodaj kontakt" - "To nie spam" - diff --git a/InCallUI/res/values-pt-rBR/strings.xml b/InCallUI/res/values-pt-rBR/strings.xml deleted file mode 100644 index 5271f54cc..000000000 --- a/InCallUI/res/values-pt-rBR/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefone" - "Em espera" - "Desconhecido" - "Número privado" - "Chamada a cobrar" - "Teleconferência" - "A chamada caiu." - "Alto-falante" - "Minifone do aparelho" - "Fone de ouvido com fio" - "Bluetooth" - "Enviar os seguintes tons?\n" - "Enviando tons\n" - "Enviar" - "Sim" - "Não" - "Substituir caractere curinga por" - "Teleconferência %s" - "Número do correio de voz" - "Discando" - "Rediscando" - "Teleconferência" - "Chamada recebida" - "Chamada trabalho recebida" - "Chamada encerrada" - "Em espera" - "Desligando" - "Em chamada" - "Meu número é %s" - "Conectando vídeo" - "Videochamada" - "Solicitando vídeo" - "Não é possível conectar a videochamada" - "Solicitação de vídeo rejeitada" - "Seu número de retorno de chamada\n %1$s" - "Seu número de retorno de chamada de emergência\n %1$s" - "Discando" - "Chamada perdida" - "Chamadas perdidas" - "%s chamadas perdidas" - "Chamada perdida de %s" - "Chamanda em andamento" - "Chamada de trabalho em andamento" - "Chamada por Wi-Fi em andamento" - "Chamada de trabalho por Wi-Fi em andamento" - "Em espera" - "Chamada recebida" - "Chamada de trabalho recebida" - "Chamada por Wi-Fi recebida" - "Chamada de trabalho recebida por Wi-Fi" - "Videochamada recebida" - "Chamada recebida suspeita (spam)" - "Solicitação de vídeo recebida" - "Novo correio de voz" - "Novo correio de voz (%d)" - "Discar %s" - "Número correio de voz desconhecido" - "Sem serviço" - "A rede selecionada (%s) está indisponível" - "Atender" - "Desligar" - "Vídeo" - "Voz" - "Aceitar" - "Dispensar" - "Retor. cham." - "Mensagem" - "Chamada em andamento em outro dispositivo" - "Transferir chamada" - "Para fazer uma chamada, primeiro desative o modo avião." - "Não registrado na rede." - "Rede celular não disponível." - "Para realizar uma chamada, digite um número válido." - "Não é possível realizar chamadas." - "Iniciando sequência MMI…" - "Serviço não compatível." - "Não é possível alternar as chamadas." - "Não é possível separar a chamada." - "Não é possível transferir a chamada." - "Não é possível fazer uma conferência." - "Não é possível rejeitar a chamada." - "Não é possível liberar chamadas." - "Chamada SIP" - "Chamada de emergência" - "Ativando o rádio…" - "Sem serviço. Tentando novamente..." - "Não é possível realizar chamadas. %s não é um número de emergência." - "Não é possível realizar chamadas. Disque um número de emergência." - "Use o teclado para discar" - "Colocar chamada em espera" - "Retomar chamada" - "Encerrar chamada" - "Mostrar teclado" - "Ocultar teclado" - "Desativar som" - "Ativar som" - "Adicionar chamada" - "Juntar chamadas" - "Trocar" - "Gerenciar chamadas" - "Gerenciar teleconferência" - "Teleconferência" - "Gerenciar" - "Áudio" - "Videocham." - "Alterar para chamada de voz" - "Alternar câmera" - "Ativar câmera" - "Desativar câmera" - "Mais opções" - "Player iniciado" - "Player interrompido" - "A câmera não está pronta" - "Câmera pronta" - "Evento de sessão de chamada desconhecido" - "Serviço" - "Configuração" - "<Não definido>" - "Outras configurações de chamada" - "Chamando via %s" - "Chamada de %s" - "foto do contato" - "conversar em particular" - "selecionar contato" - "Escreva sua resposta..." - "Cancelar" - "Enviar" - "Atender" - "Enviar SMS" - "Recusar" - "Atender como videochamada" - "Atender como chamada de áudio" - "Aceitar solicitação de vídeo" - "Recusar solicitação de vídeo" - "Aceitar solicitação de transmissão de vídeo" - "Recusar solicitação de transmissão de vídeo" - "Aceitar solicitação de recebimento de vídeo" - "Recusar solicitação de recebimento de vídeo" - "Para %s, deslize para cima." - "Para %s, deslize para a esquerda." - "Para %s, deslize para a direita." - "Para %s, deslize para baixo." - "Vibrar" - "Vibrar" - "Som" - "Som padrão (%1$s)" - "Toque do telefone" - "Vibrar ao tocar" - "Toque e vibração" - "Gerenciar teleconferência" - "Número de emergência" - "Foto do perfil" - "Câmera desligada" - "via %s" - "Nota enviada" - "Mensagens recentes" - "Informações sobre a empresa" - "%.1f milhas de distância" - "%.1f km de distância" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "Abre amanhã às %s" - "Abre hoje às %s" - "Fecha às %s" - "Fechou hoje às %s" - "Aberto agora" - "Fechado agora" - "Autor suspeito (spam)" - "Chamada encerra %1$s" - "Esta é a primeira vez que este número ligou para você." - "Suspeitamos que esta chamada seja de um criador de spams." - "Bloq./denunciar spam" - "Adicionar contato" - "Não é spam" - diff --git a/InCallUI/res/values-pt-rPT/strings.xml b/InCallUI/res/values-pt-rPT/strings.xml deleted file mode 100644 index 2a04556fe..000000000 --- a/InCallUI/res/values-pt-rPT/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefone" - "Em espera" - "Desconhecido" - "Número privado" - "Telefone público" - "Conferência" - "A chamada caiu" - "Altifalante" - "Auricular do telefone" - "Auscultadores com fios" - "Bluetooth" - "Pretende enviar os seguintes tons?\n" - "A enviar tons\n" - "Enviar" - "Sim" - "Não" - "Substituir o caráter universal por" - "Conferência %s" - "Número do correio de voz" - "A marcar" - "A remarcar" - "Conferência" - "Chamada recebida" - "Chamada de trab. recebida" - "Chamada terminada" - "Em espera" - "A desligar" - "Numa chamada" - "O meu número é %s" - "A ligar vídeo" - "Videochamada" - "A solicitar vídeo" - "Não é possível ligar a videochamada" - "Pedido de vídeo rejeitado" - "O seu número de retorno de chamadas\n %1$s" - "O seu número de retorno de chamadas de emergência\n %1$s" - "A marcar" - "Chamada não atendida" - "Chamadas não atendidas" - "%s chamadas não atendidas" - "Chamada não atendida de %s" - "Chamada em curso" - "Chamada de trabalho em curso" - "Chamada Wi-Fi em curso" - "Chamada de trabalho via Wi-Fi em curso" - "Em espera" - "Chamada recebida" - "Chamada de trab. recebida" - "Chamada Wi-Fi recebida" - "Chamada de trabalho recebida via Wi-Fi" - "Videochamada recebida" - "A receber chamada spam suspeita" - "Pedido de vídeo recebido" - "Nova mensagem de correio de voz" - "Nova mensagem de correio de voz (%d)" - "Marcar %s" - "Número do correio de voz desconhecido" - "Sem serviço" - "Rede selecionada (%s) indisponível" - "Atender" - "Desligar" - "Vídeo" - "Voz" - "Aceitar" - "Ignorar" - "Ligar de volta" - "Mensagem" - "Chamada em curso noutro dispositivo" - "Transferir chamada" - "Para efetuar uma chamada, desative primeiro o Modo de avião." - "Sem registo na rede." - "Rede móvel não disponível." - "Para efetuar uma chamada, introduza um número válido." - "Não é possível telefonar." - "A iniciar sequência de MMI..." - "Serviço não suportado." - "Não é possível alternar entre chamadas." - "Não é possível separar a chamada." - "Não é possível transferir." - "Não é possível efetuar uma conferência." - "Não é possível rejeitar a chamada." - "Não é possível libertar as chamadas." - "Chamada SIP" - "Chamada de emergência" - "A ligar o rádio..." - "Sem serviço. A tentar novamente…" - "Não é possível telefonar. %s não é um número de emergência." - "Não é possível telefonar. Marque um número de emergência." - "Utilizar o teclado para marcar" - "Colocar chamada em espera" - "Retomar chamada" - "Terminar chamada" - "Mostrar o teclado" - "Ocultar o teclado" - "Desativar som" - "Reativar o som" - "Adicionar chamada" - "Intercalar chamadas" - "Trocar" - "Gerir chamadas" - "Gerir conferência" - "Conferência" - "Gerir" - "Áudio" - "Videochamada" - "Mudar para chamada de voz" - "Trocar câmara" - "Ativar câmara" - "Desativar câmara" - "Mais opções" - "Leitor iniciado" - "Leitor interrompido" - "A câmara não está pronta" - "Câmara pronta" - "Evento de sessão de chamada desconhecido" - "Serviço" - "Configuração" - "<Não definido>" - "Outras definições de chamadas" - "A telefonar através de %s" - "Recebidas através de %s" - "foto do contacto" - "tornar privado" - "selecionar contacto" - "Escreva a sua própria..." - "Cancelar" - "Enviar" - "Atender" - "Enviar SMS" - "Recusar" - "Atender como videochamada" - "Atender como chamada de áudio" - "Aceitar pedido de vídeo" - "Recusar pedido de vídeo" - "Aceitar pedido para transmitir vídeo" - "Recusar pedido para transmitir vídeo" - "Aceitar pedido para receber vídeo" - "Recusar pedido para receber vídeo" - "Deslize lentamente para cima para %s." - "Deslize lentamente para a esquerda para %s." - "Deslize lentamente para a direita para %s." - "Deslize lentamente para baixo para %s." - "Vibrar" - "Vibrar" - "Som" - "Som predefinido (%1$s)" - "Toque do telemóvel" - "Vibrar ao tocar" - "Tocar e vibrar" - "Gerir conferência" - "Número de emergência" - "Foto do perfil" - "Câmara desligada" - "através de %s" - "Nota enviada" - "Mensagens recentes" - "Informações da empresa" - "A %.1f milhas de distância" - "A %.1f km de distância" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "Abre amanhã às %s" - "Abre hoje às %s" - "Fecha às %s" - "Fechou hoje às %s" - "Aberto agora" - "Fechado agora" - "Chmd. spam suspeita" - "Chamada terminada: %1$s" - "É a primeira vez que este número lhe liga." - "Suspeitamos que a pessoa que fez esta chamada seja um spammer." - "Bloq./denunciar spam" - "Adicionar contacto" - "Não é spam" - diff --git a/InCallUI/res/values-pt/strings.xml b/InCallUI/res/values-pt/strings.xml deleted file mode 100644 index 5271f54cc..000000000 --- a/InCallUI/res/values-pt/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefone" - "Em espera" - "Desconhecido" - "Número privado" - "Chamada a cobrar" - "Teleconferência" - "A chamada caiu." - "Alto-falante" - "Minifone do aparelho" - "Fone de ouvido com fio" - "Bluetooth" - "Enviar os seguintes tons?\n" - "Enviando tons\n" - "Enviar" - "Sim" - "Não" - "Substituir caractere curinga por" - "Teleconferência %s" - "Número do correio de voz" - "Discando" - "Rediscando" - "Teleconferência" - "Chamada recebida" - "Chamada trabalho recebida" - "Chamada encerrada" - "Em espera" - "Desligando" - "Em chamada" - "Meu número é %s" - "Conectando vídeo" - "Videochamada" - "Solicitando vídeo" - "Não é possível conectar a videochamada" - "Solicitação de vídeo rejeitada" - "Seu número de retorno de chamada\n %1$s" - "Seu número de retorno de chamada de emergência\n %1$s" - "Discando" - "Chamada perdida" - "Chamadas perdidas" - "%s chamadas perdidas" - "Chamada perdida de %s" - "Chamanda em andamento" - "Chamada de trabalho em andamento" - "Chamada por Wi-Fi em andamento" - "Chamada de trabalho por Wi-Fi em andamento" - "Em espera" - "Chamada recebida" - "Chamada de trabalho recebida" - "Chamada por Wi-Fi recebida" - "Chamada de trabalho recebida por Wi-Fi" - "Videochamada recebida" - "Chamada recebida suspeita (spam)" - "Solicitação de vídeo recebida" - "Novo correio de voz" - "Novo correio de voz (%d)" - "Discar %s" - "Número correio de voz desconhecido" - "Sem serviço" - "A rede selecionada (%s) está indisponível" - "Atender" - "Desligar" - "Vídeo" - "Voz" - "Aceitar" - "Dispensar" - "Retor. cham." - "Mensagem" - "Chamada em andamento em outro dispositivo" - "Transferir chamada" - "Para fazer uma chamada, primeiro desative o modo avião." - "Não registrado na rede." - "Rede celular não disponível." - "Para realizar uma chamada, digite um número válido." - "Não é possível realizar chamadas." - "Iniciando sequência MMI…" - "Serviço não compatível." - "Não é possível alternar as chamadas." - "Não é possível separar a chamada." - "Não é possível transferir a chamada." - "Não é possível fazer uma conferência." - "Não é possível rejeitar a chamada." - "Não é possível liberar chamadas." - "Chamada SIP" - "Chamada de emergência" - "Ativando o rádio…" - "Sem serviço. Tentando novamente..." - "Não é possível realizar chamadas. %s não é um número de emergência." - "Não é possível realizar chamadas. Disque um número de emergência." - "Use o teclado para discar" - "Colocar chamada em espera" - "Retomar chamada" - "Encerrar chamada" - "Mostrar teclado" - "Ocultar teclado" - "Desativar som" - "Ativar som" - "Adicionar chamada" - "Juntar chamadas" - "Trocar" - "Gerenciar chamadas" - "Gerenciar teleconferência" - "Teleconferência" - "Gerenciar" - "Áudio" - "Videocham." - "Alterar para chamada de voz" - "Alternar câmera" - "Ativar câmera" - "Desativar câmera" - "Mais opções" - "Player iniciado" - "Player interrompido" - "A câmera não está pronta" - "Câmera pronta" - "Evento de sessão de chamada desconhecido" - "Serviço" - "Configuração" - "<Não definido>" - "Outras configurações de chamada" - "Chamando via %s" - "Chamada de %s" - "foto do contato" - "conversar em particular" - "selecionar contato" - "Escreva sua resposta..." - "Cancelar" - "Enviar" - "Atender" - "Enviar SMS" - "Recusar" - "Atender como videochamada" - "Atender como chamada de áudio" - "Aceitar solicitação de vídeo" - "Recusar solicitação de vídeo" - "Aceitar solicitação de transmissão de vídeo" - "Recusar solicitação de transmissão de vídeo" - "Aceitar solicitação de recebimento de vídeo" - "Recusar solicitação de recebimento de vídeo" - "Para %s, deslize para cima." - "Para %s, deslize para a esquerda." - "Para %s, deslize para a direita." - "Para %s, deslize para baixo." - "Vibrar" - "Vibrar" - "Som" - "Som padrão (%1$s)" - "Toque do telefone" - "Vibrar ao tocar" - "Toque e vibração" - "Gerenciar teleconferência" - "Número de emergência" - "Foto do perfil" - "Câmera desligada" - "via %s" - "Nota enviada" - "Mensagens recentes" - "Informações sobre a empresa" - "%.1f milhas de distância" - "%.1f km de distância" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "Abre amanhã às %s" - "Abre hoje às %s" - "Fecha às %s" - "Fechou hoje às %s" - "Aberto agora" - "Fechado agora" - "Autor suspeito (spam)" - "Chamada encerra %1$s" - "Esta é a primeira vez que este número ligou para você." - "Suspeitamos que esta chamada seja de um criador de spams." - "Bloq./denunciar spam" - "Adicionar contato" - "Não é spam" - diff --git a/InCallUI/res/values-ro/strings.xml b/InCallUI/res/values-ro/strings.xml deleted file mode 100644 index ca0036d0d..000000000 --- a/InCallUI/res/values-ro/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefon" - "În așteptare" - "Necunoscut" - "Număr privat" - "Telefon public" - "Conferință telefonică" - "Apelul s-a încheiat" - "Difuzor" - "Casca receptorului" - "Set căști-microfon cu fir" - "Bluetooth" - "Trimiteți următoarele tonuri?\n" - "Se trimit tonuri\n" - "Trimiteți" - "Da" - "Nu" - "Înlocuiți metacaracterul cu" - "Conferință telefonică %s" - "Numărul mesageriei vocale" - "Se apelează" - "Se reapelează" - "Conferință telefonică" - "Apel primit" - "Apel de serviciu primit" - "Apel încheiat" - "În așteptare" - "Se încheie apelul" - "Apel în desfășurare" - "Numărul meu este %s" - "Se conectează apelul video" - "Apel video" - "Se solicită apel video" - "Nu se poate conecta apelul video" - "Solicitarea pentru apel video a fost respinsă" - "Numărul de apelare inversă\n%1$s" - "Numărul de apelare inversă de urgență\n%1$s" - "Se apelează" - "Apel nepreluat" - "Apeluri nepreluate" - "%s (de) apeluri nepreluate" - "Apel nepreluat de la %s" - "Apel în desfășurare" - "Apel de serviciu în desfășurare" - "Apel prin Wi-Fi în desfășurare" - "Apel de serviciu prin Wi-Fi în desfășurare" - "În așteptare" - "Apel primit" - "Apel de serviciu primit" - "Apel prin Wi-Fi primit" - "Apel de serviciu prin Wi-Fi primit" - "Apel video primit" - "Un apel primit posibil spam" - "Solicitare de trecere la apel video" - "Mesaj vocal nou" - "Mesaj vocal nou (%d)" - "Apelați %s" - "Numărul mesageriei vocale este necunoscut" - "Fără semnal" - "Rețeaua selectată (%s) nu este disponibilă" - "Răspundeți" - "Încheiați apelul" - "Apel video" - "Apel vocal" - "Acceptați" - "Refuzați" - "Apelați înapoi" - "Trimiteți mesaj" - "Apel în curs pe alt dispozitiv" - "Transferați apelul" - "Pentru a apela, mai întâi dezactivați modul Avion." - "Neînregistrat în rețea." - "Rețeaua mobilă nu este disponibilă." - "Pentru a apela, introduceți un număr valid." - "Nu se poate apela." - "Se pornește secvența MMI…" - "Serviciul nu este acceptat." - "Apelurile nu pot fi comutate." - "Apelul nu poate fi separat." - "Nu se poate transfera." - "Conferința telefonică nu poate fi inițiată." - "Apelul nu poate fi respins." - "Apelurile nu pot fi eliberate." - "Apel SIP" - "Apel de urgență" - "Se activează radio…" - "Fără semnal. Se încearcă din nou…" - "Nu se poate apela. %s nu este un număr de urgență." - "Nu se poate apela. Formați un număr de urgență." - "Folosiți tastatura pentru a apela" - "Puneți apelul în așteptare" - "Reluați apelul" - "Încheiați apelul" - "Afișează tastatura numerică" - "Ascunde tastatura numerică" - "Dezactivează sunetul" - "Activează sunetul" - "Adăugați un apel" - "Îmbinați apelurile" - "Schimbați" - "Gestionați apelurile" - "Gestionați conferința telefonică" - "Conferință telefonică" - "Gestionați" - "Audio" - "Apel video" - "Treceți la apel vocal" - "Comutați camera foto" - "Activați camera" - "Dezactivați camera" - "Mai multe opțiuni" - "Playerul a pornit" - "Playerul s-a oprit" - "Camera foto nu este pregătită" - "Camera foto este pregătită" - "Eveniment necunoscut privind o sesiune de apeluri" - "Furnizor de servicii" - "Configurați" - "<Nesetat>" - "Alte setări de apel" - "Se apelează prin %s" - "Primite prin %s" - "fotografia persoanei de contact" - "treceți în modul privat" - "selectați o persoană de contact" - "Scrieți propriul răspuns…" - "Anulați" - "Trimiteți" - "Răspundeți" - "Trimiteți SMS" - "Refuzați" - "Răspundeți ca apel video" - "Răspundeți ca apel audio" - "Acceptați solicitarea de a trece la apel video" - "Refuzați solicitarea de a trece la apel video" - "Acceptați solicitarea de a transmite conținut video" - "Refuzați solicitarea de a transmite conținut video" - "Acceptați solicitarea de a primi conținut video" - "Refuzați solicitarea de a primi conținut video" - "Glisați în sus ca să %s." - "Glisați spre stânga ca să %s." - "Glisați spre dreapta ca să %s." - "Glisați în jos ca să %s." - "Vibrații" - "Vibrații" - "Sunet" - "Sunet prestabilit (%1$s)" - "Ton de sonerie pentru telefon" - "Vibrează când sună" - "Ton de sonerie și vibrații" - "Gestionați conferința telefonică" - "Număr de urgență" - "Fotografie de profil" - "Camera foto este oprită" - "pe %s" - "Nota a fost trimisă" - "Mesaje recente" - "Informații despre companie" - "%.1f mi distanță" - "%.1f km distanță" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "Deschide mâine la %s" - "Deschide astăzi la %s" - "Închide la %s" - "A închis astăzi la %s" - "Acum este deschis" - "Acum este închis" - "Posibil apelant spam" - "Apelul s-a încheiat %1$s" - "Aceasta este prima dată când ați primit apel de la acest număr." - "Suspectăm că acesta este un apel spam." - "Blocați/raportați" - "Adăugați persoana" - "Nu este spam" - diff --git a/InCallUI/res/values-ru/strings.xml b/InCallUI/res/values-ru/strings.xml deleted file mode 100644 index 552ad4d02..000000000 --- a/InCallUI/res/values-ru/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Телефон" - "На удержании" - "Неизвестный абонент" - "Скрытый номер" - "Телефон-автомат" - "Конференц-вызов" - "Звонок сброшен" - "Динамик" - "Динамик гарнитуры" - "Проводная гарнитура" - "Bluetooth" - "Отправить следующие тоны?\n" - "Отправка тональных сигналов\n" - "Отправить" - "Да" - "Нет" - "Заменить универсальный символ на" - "Конференц-вызов: %s" - "Номер голосовой почты" - "Набор номера…" - "Повторный вызов" - "Конференц-вызов" - "Входящий вызов" - "Входящий вызов (работа)" - "Вызов завершен" - "На удержании" - "Завершение разговора" - "Вызов" - "Мой номер: %s" - "Подключение видео" - "Видеовызов" - "Запрос видео" - "Не удалось совершить видеовызов" - "Видеовызов отклонен" - "Номер обратного вызова:\n%1$s" - "Номер обратного вызова для экстренных служб:\n%1$s" - "Набор номера…" - "Пропущенный вызов" - "Пропущенные вызовы" - "Пропущенных вызовов: %s" - "Пропущенные вызовы от абонента %s" - "Текущий вызов" - "Текущий звонок (работа)" - "Текущий Wi-Fi-звонок" - "Текущий Wi-Fi-звонок (работа)" - "На удержании" - "Входящий вызов" - "Входящий вызов (работа)" - "Входящий Wi-Fi-звонок" - "Входящий Wi-Fi-звонок (работа)" - "Входящий видеовызов" - "Входящий вызов: подозрение на спам" - "Входящий видеовызов" - "Новое сообщение голосовой почты" - "Новое сообщение голосовой почты (%d)" - "Набрать номер %s" - "Номер голосовой почты неизвестен" - "Нет сигнала" - "Выбранная сеть (%s) недоступна." - "Ответить" - "Завершить" - "Видео" - "Голос" - "Разрешить" - "Закрыть" - "Перезвонить" - "Написать SMS" - "Вы участвуете в разговоре на другом устройстве" - "Перевести на это устройство" - "Отключите режим полета." - "Нет регистрации в сети." - "Мобильная сеть недоступна." - "Недействительный номер." - "Не удалось позвонить." - "Запуск последовательности MMI..." - "Сервис не поддерживается." - "Не удалось переключить вызов." - "Не удалось разделить вызов." - "Не удалось перенести." - "Не удалось выполнить конференц-вызов." - "Не удалось отклонить вызов." - "Не удалось разъединить." - "Вызов SIP" - "Экстренный вызов" - "Включение радио…" - "Нет сигнала. Повторная попытка…" - "Не удалось позвонить. Номер %s не принадлежит экстренным службам." - "Не удалось позвонить. Наберите номер экстренных служб." - "Используйте клавиатуру для набора номера" - "Удерживать вызов" - "Возобновить вызов" - "Завершить вызов" - "Показать панель набора номера" - "Скрыть панель набора номера" - "Выключить звук" - "Включить звук" - "Добавить вызов" - "Объединить вызовы" - "Перевести звонок" - "Управление вызовами" - "Настройка конференц-связи" - "Конференц-вызов" - "Управление" - "Аудио" - "Видеовызов" - "Отключить видео" - "Сменить камеру" - "Включить камеру" - "Выключить камеру" - "Другие настройки" - "Видеоплеер включен" - "Видеоплеер отключен" - "Камера недоступна" - "Камера доступна" - "Неизвестное событие сеанса связи" - "Служба" - "Настройка" - "<Не задано>" - "Другие настройки вызовов" - "Звонок через %s" - "Входящий вызов (оператор: %s)" - "фотография контакта" - "приватная конференция" - "выбрать контакт" - "Ваш ответ…" - "Отмена" - "Отправить" - "Ответить" - "Отправить SMS" - "Отклонить" - "Ответить с видео" - "Ответить на голосовой вызов" - "Ответить на видеовызов" - "Отклонить видеовызов" - "Разрешить передачу видео" - "Отклонить передачу видео" - "Принять видео" - "Отклонить видео" - "Проведите вверх, чтобы %s." - "Проведите влево, чтобы %s." - "Проведите вправо, чтобы %s." - "Проведите вниз, чтобы %s." - "Вибросигнал" - "Вибросигнал" - "Звук" - "По умолчанию (%1$s)" - "Рингтон" - "Вибросигнал и рингтон" - "Мелодия звонка и вибросигнал" - "Настройка конференц-связи" - "Экстренная служба" - "Фото профиля" - "Камера отключена" - "через %s" - "Сообщение отправлено" - "Недавние сообщения" - "Информация о компании" - "%.1f мил." - "%.1f км" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "Откроется завтра в %s" - "Откроется сегодня в %s" - "Работает до %s" - "Сегодня не работает с %s" - "Сейчас открыто" - "Сейчас закрыто" - "Подозрение на спам" - "Вызов завершен (%1$s)" - "Это первый вызов с этого номера." - "Похоже, этот вызов – спам." - "Заблокировать/в спам" - "Добавить контакт" - "Не спам" - diff --git a/InCallUI/res/values-si/strings.xml b/InCallUI/res/values-si/strings.xml deleted file mode 100644 index de0267a58..000000000 --- a/InCallUI/res/values-si/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "දුරකථනය" - "රඳවා ගනිමින්" - "නොදනී" - "රහසිගත අංකය" - "පේෆෝනය" - "සම්මන්ත්‍රණ ඇමතුම" - "ඇමතුම නැවතුණි" - "නාදකය" - "හෑන්ඩ්සෙටයේ සවන් කඬ" - "රැහැන් සහිත හෙඩ්සෙටය" - "බ්ලූටූත්" - "පහත නාද යවන්නද?\n" - "නාද යවමින්\n" - "යවන්න" - "ඔව්" - "නැත" - "අනුලකුණ ප්‍රතිස්ථාපනය කරන්නේ" - "සම්මන්ත්‍රණ ඇමතුම %s" - "හඬ තැපැල් අංකය" - "ඩයල් කරමින්" - "නැවත ඩයල් කරමින්" - "සම්මන්ත්‍රණ ඇමතුම" - "එන ඇමතුම" - "එන කාර්යාල ඇමතුම" - "ඇමතුම අවසන් විය" - "රඳවා ගනිමින්" - "විසන්ධි කරමින්" - "ඇමතුමක" - "මගේ අංකය %s" - "වීඩියෝවකට සම්බන්ධ කරමින්" - "වීඩියෝ ඇමතුම" - "වීඩියෝවක් ඉල්ලමින්" - "වීඩියෝ ඇමතුම සම්බන්ධ කළ නොහැක" - "වීඩියෝ ඉල්ලීම ප්‍රතික්ෂේප කරන ලදී" - "ඔබේ පසුඇමතුම් අංකය\n %1$s" - "ඔබගේ හදිසි පසුඇමතුම් අංකය\n %1$s" - "ඩයල් කරමින්" - "මඟ හැරුණු ඇමතුම" - "මඟ හැරුණු ඇමතුම්" - "මඟ හැරුණු ඇමතුම් %s" - "%s වෙතින් මඟ හැරුණු ඇමතුම" - "කරගෙන යන ඇමතුම" - "කරගෙන යන කාර්යාල ඇමතුම" - "දැනට කරගෙන යන Wi-Fi ඇමතුම" - "කරගෙන යන Wi-Fi කාර්යාල ඇමතුම" - "රඳවා ගනිමින්" - "එන ඇමතුම" - "එන කාර්යාල ඇමතුම" - "එන Wi-Fi ඇමතුම" - "එන Wi-Fi කාර්යාල ඇමතුම" - "එන වීඩියෝ ඇමතුම" - "එන සැකසහිත අයාචිත තැපැල් ඇමතුම" - "එන වීඩියෝ ඉල්ලීම" - "නව හඬ තැපෑල" - "නව හඬ තැපැල් (%d)" - "%s ඩයල් කරන්න" - "හඬ තැපැල් අංකය නොදනී" - "සේවාව නැත" - "තෝරා ඇති ජාලය (%s) නොමැත" - "පිළිතුරු දෙන්න" - "විසන්ධි කරන්න" - "වීඩියෝව" - "හඬ" - "පිළිගන්න" - "අස් කරන්න" - "පසුඇමතුම" - "පණිවිඩය" - "වෙනත් උපාංගයක සිදු වන ඇමතුම" - "ඇමතුම මාරු කරන්න" - "ඇමතුමක් ගැනීමට, මුලින්ම ගුවන් යානා ප්‍රකාරය ක්‍රියාවිරහිත කරන්න." - "ජාලය මත ලියාපදිංචි වී නැත." - "සෙලියුලර් ජාලය නොතිබේ." - "ඇමතුමක් ගැනීමට, වලංගු අංකයක් ඇතුළු කරන්න." - "ඇමතුම් ගැනීමට නොහැක." - "MMI අනුපිළිවෙළ ආරම්භ කරමින්…" - "සේවාවට සහාය දක්වන්නේ නැත." - "ඇමතුම් මාරු කිරීම කළ නොහැක." - "ඇමතුම වෙන් කිරීම කළ නොහැක." - "මාරු කිරීමට නොහැක." - "සම්මන්ත්‍රණය කළ නොහැක." - "ඇමතුම ප්‍රතික්ෂේප කළ නොහැක." - "ඇමතුම(ම්) මුදාහැරීම කළ නොහැක." - "SIP ඇමතුම" - "හදිසි ඇමතුම" - "රේඩියෝව ක්‍රියාත්මක කරමින්…" - "සේවාව නැත. නැවත උත්සාහ කරමින්…" - "ඇමතීමට නොහැකිය. %s මෙය හදිසි ඇමතුම් අංකයක් නොවේ." - "ඇමතිය නොහැක. හදිසි අංකයක් අමතන්න." - "ඩයල් කිරීමට යතුරු පුවරුව භාවිත කරන්න" - "ඇමතුම රඳවා ගන්න" - "ඇමතුම නැවත පටන් ගන්න" - "ඇමතුම අවසන් කරන්න" - "ඇමතුම් පෑඩය පෙන්වන්න" - "ඇමතුම් පෑඩය සඟවන්න" - "නිහඬ කරන්න" - "නිහඬ කිරීම ඉවත් කරන්න" - "ඇමතුමක් එක් කරන්න" - "ඇමතුම් ඒකාබද්ධ කරන්න" - "මාරු කරන්න" - "ඇමතුම් කළමනාකරණය කරන්න" - "සම්මන්ත්‍රණ ඇමතුම කළමනාකරණය කරන්න" - "සම්මන්ත්‍රණ ඇමතුම" - "කළමනාකරණය කරන්න" - "ශ්‍රව්‍යය" - "වීඩියෝ ඇමතුම" - "හඬ ඇමතුමක් වෙත මාරු කරන්න" - "කැමරාව මාරු කරන්න" - "කැමරාව ක්‍රියාත්මක කරන්න" - "කැමරාව ක්‍රියා විරහිත කරන්න" - "තවත් විකල්ප" - "ධාවකය ආරම්භ කරන ලදි" - "ධාවකය නැවතුණි" - "කැමරාව සූදානම් නැහැ" - "කැමරාව සූදානම්" - "නොදන්නා ඇමතුම් සැසි සිදුවීම" - "සේවාව" - "පිහිටුවීම" - "<පිහිටුවා නැත>" - "වෙනත් ඇමතුම් සැකසීම්" - "%s හරහා අමතමින්" - "%s හරහා එන" - "සම්බන්ධතා ඡායාරූපය" - "රහසිගත වන්න" - "සම්බන්ධතාවය තෝරාගන්න" - "ඔබේම එකක් ලියන්න..." - "අවලංගු කරන්න" - "යවන්න" - "පිළිතුරු දෙන්න" - "SMS යවන්න" - "ප්‍රතික්ෂේප කිරීම" - "වීඩියෝ ඇමතුමට පිළිතුරු දෙන්න" - "ශ්‍රව්‍ය ඇමතුමට පිළිතුරු දෙන්න" - "වීඩියෝ ඉල්ලීම පිළිගන්න" - "වීඩියෝ ඉල්ලීම ප්‍රතික්ෂේප කරන්න" - "වීඩියෝ සම්ප්‍ර්ෂණ ඉල්ලීම පිළිගන්න" - "වීඩියෝ සම්ප්‍ර්ෂණ ඉල්ලීම ප්‍රතික්ෂේප කරන්න" - "වීඩියෝ ලැබීමේ ඉල්ලීම පිළිගන්නා ලදි" - "වීඩියෝ ලැබීමේ ඉල්ලීම ප්‍රතික්ෂේප කරන්න" - "%s සඳහා උඩට සර්පණය කරන්න." - "%s සඳහා වමට සර්පණය කරන්න." - "%s සඳහා දකුණට සර්පණය කරන්න." - "%s සඳහා පහළට සර්පණය කරන්න." - "කම්පනය" - "කම්පනය" - "හඬ" - "පෙරනිමි ශබ්දය (%1$s)" - "දුරකථන රිගින්ටෝනය" - "රිගින් වන විට කම්පන වන්න" - "රිගින් ටෝන් සහ කම්පනය කරන්න" - "සම්මන්ත්‍රණ ඇමතුම කළමනාකරණය කරන්න" - "හදිසි ඇමතුම් අංකය" - "පැතිකඩ ඡායාරූපය" - "කැමරාව ක්‍රියාවිරහිතයි" - "%s හරහා" - "සටහන යවන ලදී" - "මෑත පණිවිඩ" - "ව්‍යාපාර තොරතුරු" - "සැතපුම් %.1fක් ඈතින්" - "කි.මි. %.1fක් ඈතින්" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "හෙට %sට විවෘත කෙරේ" - "අද %sට විවෘත කෙරේ" - "%sට වසයි" - "අද %sට වසන ලදී" - "දැන් විවෘතයි" - "දැන් වසා ඇත" - "සැකසහිත අයාචිත තැපැල් අමතන්නා" - "ඇමතුම අවසන් විය %1$s" - "මෙය ඔබට මෙම අංකයෙන් ඇමතුමක් ලැබුණ පළමු අවස්ථාව වේ." - "මෙම ඇමතුම අයාචිත එවන්නෙකු අපි සැක කළෙමු." - "අවහිර ක./අයාචිත වා." - "සම්බන්ධතාව එක් කරන්න" - "අයාචිත තැපෑලක් නොවේ" - diff --git a/InCallUI/res/values-sk/strings.xml b/InCallUI/res/values-sk/strings.xml deleted file mode 100644 index 07e8de671..000000000 --- a/InCallUI/res/values-sk/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefón" - "Podržaný hovor" - "Neznáme" - "Súkromné číslo" - "Telefónny automat" - "Konferenčný hovor" - "Hovor bol prerušený" - "Reproduktor" - "Slúchadlo" - "Náhlavná súprava s káblom" - "Bluetooth" - "Odoslať nasledujúce tóny?\n" - "Odosielanie tónov\n" - "Odoslať" - "Áno" - "Nie" - "Nahradiť zástupný znak znakom" - "Konferenčný hovor %s" - "Číslo hlasovej schránky" - "Vytáča sa" - "Znova sa vytáča" - "Konferenčný hovor" - "Prichádzajúci hovor" - "Prichádzajúci prac. hovor" - "Hovor bol ukončený" - "Podržaný hovor" - "Ukončovanie hovoru" - "Prebieha hovor" - "Moje číslo je %s" - "Pripája sa video" - "Videohovor" - "Žiada sa video" - "Videohovor nie je možné pripojiť" - "Žiadosť o video bola odmietnutá" - "Vaše číslo na spätné volanie\n %1$s" - "Vaše číslo na spätné tiesňové volanie\n %1$s" - "Vytáča sa" - "Zmeškaný hovor" - "Zmeškané hovory" - "Zmeškané hovory: %s" - "Zmeškaný hovor od volajúceho %s" - "Prebiehajúci hovor" - "Prebiehajúci pracovný hovor" - "Odchádzajúci hovor cez Wi-Fi" - "Prebiehajúci pracovný hovor cez Wi-Fi" - "Podržaný hovor" - "Prichádzajúci hovor" - "Prichádzajúci pracovný hovor" - "Prichádzajúci hovor cez Wi-Fi" - "Prichádzajúci pracovný hovor cez Wi-Fi" - "Prichádzajúci videohovor" - "Prichádzajúci hovor, pri ktorom je podozrenie, že ide o spam" - "Prichádzajúca žiadosť o video" - "Nová hlasová správa" - "Nová hlasová správa (%d)" - "Zavolať hlasovú schránku %s" - "Číslo hlasovej schránky je neznáme" - "Žiadny signál" - "Vybraná sieť (%s) nie je k dispozícii" - "Prijať" - "Položiť" - "Video" - "Hlas" - "Prijať" - "Odmietnuť" - "Zavolať späť" - "Správa" - "Prebiehajúci hovor v inom zariadení" - "Prepojiť hovor" - "Ak chcete volať, vypnite najprv režim v lietadle." - "Prihlásenie do siete nebolo úspešné." - "Mobilná sieť nie je k dispozícii." - "Ak chcete volať, zadajte platné číslo." - "Hovor sa nedá uskutočniť." - "Prebieha spúšťanie sekvencie MMI…" - "Služba nie je podporovaná." - "Nedajú sa prepínať hovory." - "Nedá sa rozdeliť hovor." - "Nedá sa preniesť." - "Konferenčný hovor sa nedá uskutočniť." - "Nedá sa odmietnuť hovor." - "Nedajú sa ukončiť hovory." - "Hovor SIP" - "Tiesňové volanie" - "Zapína sa rádio..." - "Žiadny signál. Prebieha ďalší pokus…" - "Hovor sa nedá uskutočniť. %s nie je číslo tiesňového volania." - "Hovor nie je možné uskutočniť. Vytočte číslo tiesňového volania." - "Číslo vytočíte pomocou klávesnice" - "Podržať hovor" - "Obnoviť hovor" - "Ukončiť hovor" - "Zobraziť číselnú klávesnicu" - "Skryť číselnú klávesnicu" - "Vypnúť zvuk" - "Zapnúť zvuk" - "Pridať hovor" - "Zlúčiť hovory" - "Zameniť" - "Spravovať hovory" - "Spravovať konferenčný hovor" - "Konferenčný hovor" - "Spravovať" - "Zvuk" - "Videohovor" - "Zmeniť na hlasový hovor" - "Zapnúť kameru" - "Zapnúť fotoaparát" - "Vypnúť fotoaparát" - "Ďalšie možnosti" - "Prehrávač bol spustený" - "Prehrávač bol zastavený" - "Kamera nie je pripravená" - "Kamera je pripravená" - "Neznáma udalosť relácie volania" - "Služba" - "Nastavenie" - "<Nenastavené>" - "Ďalšie nastavenia hovorov" - "Voláte prostredníctvom poskytovateľa %s" - "Prichádz. hovor prostred. poskytovateľa %s" - "fotka kontaktu" - "prepnúť na súkromné" - "vybrať kontakt" - "Napísať vlastnú..." - "Zrušiť" - "Odoslať" - "Prijať" - "Odoslať SMS" - "Odmietnuť" - "Prijať ako videohovor" - "Prijať ako zvukový hovor" - "Prijať žiadosť o videohovor" - "Odmietnuť žiadosť o videohovor" - "Prijať žiadosť o prenos videa" - "Odmietnuť žiadosť o prenos videa" - "Povoliť žiadosť o prijatie videa" - "Odmietnuť žiadosť o prijatie videa" - "Prejdite prstom nahor: %s." - "Prejdite prstom doľava: %s." - "Prejdite prstom doprava: %s." - "Prejdite prstom nadol: %s." - "Vibrovanie" - "Vibrovanie" - "Zvuk" - "Predvolený zvuk (%1$s)" - "Tón zvonenia telefónu" - "Vibrovať pri zvonení" - "Vyzváňací tón a vibrovanie" - "Správa konferenčného hovoru" - "Číslo tiesňového volania" - "Profilová fotka" - "Kamera je vypnutá" - "na čísle %s" - "Poznámka bola odoslaná" - "Nedávne správy" - "Informácie o firme" - "Vzdialené %.1f mi" - "Vzdialené %.1f km" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "Zajtra sa otvára o %s" - "Dnes sa otvára o %s" - "Zatvára sa o %s" - "Dnes bolo zatvorené o %s" - "Otvorené" - "Zatvorené" - "Podozrenie na spam" - "Hovor sa skončil %1$s" - "Toto bol prvý hovor z tohto čísla." - "Mali sme podozrenie, že tento hovor bol od šíriteľa spamu." - "Blok./nahlásiť spam" - "Pridať kontakt" - "Toto nie je spam" - diff --git a/InCallUI/res/values-sl/strings.xml b/InCallUI/res/values-sl/strings.xml deleted file mode 100644 index a2cf2102b..000000000 --- a/InCallUI/res/values-sl/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefon" - "Zadržan" - "Neznan" - "Zasebna številka" - "Telefonska govorilnica" - "Konferenčni klic" - "Klic je bil prekinjen" - "Zvočnik" - "Slušalka" - "Žične slušalke" - "Bluetooth" - "Ali želite poslati naslednje tone?\n" - "Pošiljanje tonov\n" - "Pošlji" - "Da" - "Ne" - "Zamenjaj nadomestni znak z" - "Konferenčni klic: %s" - "Številka odzivnika" - "Klicanje" - "Vnovično klicanje" - "Konferenčni klic" - "Dohodni klic" - "Dohodni delovni klic" - "Klic je končan" - "Zadržan" - "Prekinjanje" - "Klic poteka" - "Moja številka je %s" - "Povezovanje videa" - "Videoklic" - "Zahtevanje videa" - "Videoklica ni mogoče vzpostaviti" - "Zavrnjena zahteva za videoklic" - "Vaša številka za povratni klic:\n %1$s" - "Vaša številka za povratni klic v sili:\n %1$s" - "Klicanje" - "Neodgovorjeni klic" - "Neodgovorjeni klici" - "Št. neodgovorjenih klicev: %s" - "Neodgovorjeni klic od: %s" - "Aktivni klic" - "Aktivni delovni klic" - "Aktivni klic prek omrežja Wi-Fi" - "Aktivni delovni klic prek omrežja Wi-Fi" - "Zadržan" - "Dohodni klic" - "Dohodni delovni klic" - "Dohodni klic Wi-Fi" - "Dohodni delovni klic prek omrežja Wi-Fi" - "Dohodni videoklic" - "Domnevno neželeni dohodni klic" - "Zahteva za dohodni video" - "Novo sporočilo v odzivniku" - "Novo sporočilo v odzivniku (%d)" - "Klic: %s" - "Neznana številka odzivnika" - "Ni signala" - "Izbrano omrežje (%s) ni na voljo" - "Odgovor" - "Prekinitev" - "Video" - "Govor" - "Sprejmem" - "Opusti" - "Povrat. klic" - "SMS" - "Aktivni klic v drugi napravi" - "Prenos klica" - "Če želite poklicati, najprej izklopite način za letalo." - "Ni registrirano v omrežju." - "Mobilno omrežje ni na voljo." - "Če želite opraviti klic, vnesite veljavno številko." - "Klicanje ni mogoče." - "Začetek zaporedja MMI ..." - "Storitev ni podprta." - "Preklop med klici ni mogoč." - "Ločitev klica ni mogoča." - "Prenos ni mogoč." - "Konferenčni klic ni mogoč." - "Zavrnitev klica ni mogoča." - "Prekinitev klica ni mogoča." - "Klic SIP" - "Klic v sili" - "Vklop radia …" - "Ni signala. Vnovičen poskus …" - "Klicanje ni mogoče. %s ni številka za klic v sili." - "Klicanje ni mogoče. Opravite klic v sili." - "Za izbiranje številke uporabite tipkovnico" - "Zadrži klic" - "Nadaljuj klic" - "Končaj klic" - "Prikaži tipkovnico" - "Skrij tipkovnico" - "Izklopi zvok" - "Vklopi zvok" - "Dodaj klic" - "Združi klice" - "Zamenjaj" - "Upravljaj klice" - "Upravljaj konferenčne klice" - "Konferenčni klic" - "Upravljaj" - "Zvok" - "Videoklic" - "Preklopi na glasovni klic" - "Preklopi med fotoaparati" - "Vklopi kamero" - "Izklopi kamero" - "Več možnosti" - "Predvajanje začeto" - "Predvajanje ustavljeno" - "Fotoaparat ni pripravljen" - "Fotoaparat je pripravljen" - "Neznan dogodek seje klica" - "Storitev" - "Nastavitev" - "<Ni nastavljeno>" - "Druge klicne nastavitve" - "Klicanje prek ponudnika %s" - "Dohodni prek %s" - "fotografija stika" - "zasebno" - "izbira stika" - "Napišite lasten odgovor …" - "Prekliči" - "Pošlji" - "Odgovor" - "Pošiljanje SMS-ja" - "Zavrnitev" - "Odgovor z video povezavo" - "Odgovor z zvočno povezavo" - "Sprejemanje zahteve za video" - "Zavrnitev zahteve za video" - "Sprejemanje zahteve za pošiljanje videa" - "Zavrnitev zahteve za pošiljanje videa" - "Sprejemanje zahteve za prejem videa" - "Zavrnitev zahteve za prejem videa" - "Povlecite navzgor za %s." - "Povlecite v levo za %s." - "Povlecite v desno za %s." - "Povlecite navzdol za %s." - "Vibriranje" - "Vibriranje" - "Zvok" - "Privzeti zvok (%1$s)" - "Ton zvonjenja telefona" - "Vibriranje ob zvonjenju" - "Zvonjenje in vibriranje" - "Upravljanje konferenčnih klicev" - "Številka za klic v sili" - "Fotografija profila" - "Fotoaparat je izklopljen" - "prek %s" - "Opomba poslana" - "Nedavna sporočila" - "Podatki o podjetju" - "%.1f mi stran" - "%.1f km stran" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "Odpre se jutri ob %s" - "Odpre se danes ob %s" - "Zapre se ob %s" - "Zaprto danes ob %s" - "Trenutno odprto" - "Trenutno zaprto" - "Neželeni klicatelj" - "Klic je bil končan %1$s" - "To je prvi klic, ki ste ga prejeli s te številke." - "Predvidevali smo, da je to neželeni klic." - "Blok./prij. než. kl." - "Dodaj stik" - "Ni neželeni klic" - diff --git a/InCallUI/res/values-sq/strings.xml b/InCallUI/res/values-sq/strings.xml deleted file mode 100644 index 43fd2263d..000000000 --- a/InCallUI/res/values-sq/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefoni" - "Në pritje" - "I panjohur" - "Numër privat" - "Telefon me pagesë" - "Telefonatë konferencë" - "Telefonata ra" - "Altoparlant" - "Kufje për vesh" - "Kufje me tel" - "Bluetooth" - "Dërgo tonet e mëposhtme?\n" - "Po dërgon tone\n" - "Dërgo" - "Po" - "Jo" - "Zëvendëso karakterin variabël me" - "Telefonatë konferencë %s" - "Numri i postës zanore" - "Po formon numrin" - "Po riformon numrin" - "Telefonatë konferencë" - "Telefonatë hyrëse" - "Telefonatë pune hyrëse" - "Telefonata përfundoi" - "Në pritje" - "Mbyllja" - "Në telefonatë" - "Numri im është %s" - "Po rilidh videon" - "Telefonatë me video" - "Po kërkon video" - "Nuk mund të lidhë telefonatën me video" - "Kërkesa me video u refuzua" - "Numri i kthimit të telefonatës\n %1$s" - "Numri i kthimit të telefonatës së urgjencës\n %1$s" - "Po formon numrin" - "Telefonatë e humbur" - "Telefonata të humbura" - "%s telefonata të humbura" - "Telefonatë e humbur nga %s" - "Telefonatë në vazhdim" - "Telefonatë pune dalëse" - "Telefonatë me Wi-Fi në vazhdim" - "Telefonatë pune dalëse me Wi-Fi" - "Në pritje" - "Telefonatë hyrëse" - "Telefonatë pune hyrëse" - "Telefonatë hyrëse me Wi-Fi" - "Telefonatë pune hyrëse me Wi-Fi" - "Telefonatë hyrëse me video" - "Telefonatë e dyshuar si e padëshiruar" - "Kërkesë për video hyrëse" - "Postë e re zanore" - "Postë e re zanore (%d)" - "Formo numrin %s" - "Numri i postës zanore është i panjohur" - "Nuk ka shërbim" - "Rrjeti i zgjedhur (%s) i padisponueshëm" - "Përgjigju" - "Mbyll" - "Video" - "Zanore" - "Prano" - "Largoje" - "Ri-telefono" - "Mesazh" - "Telefonatë në vazhdim në një pajisje tjetër" - "Transfero telefonatën" - "Për të kryer telefonatë, së pari çaktivizo modalitetin e aeroplanit." - "I paregjistruar në rrjet." - "Rrjeti celular nuk mundësohet." - "Për të kryer një telefonatë, fut një numër të vlefshëm." - "Nuk mund të telefonojë." - "Po fillon sekuencën MMI…" - "Shërbimi nuk mbështetet." - "Nuk mund të ndryshojë telefonatat." - "Nuk mund të ndajë telefonatën." - "Nuk mund të transferojë." - "Nuk mund të kryejë telefonatë konference." - "Nuk mund të refuzojë telefonatën." - "Nuk mund të lëshojë telefonatën(at)." - "Telefonatë SIP" - "Telefonata e urgjencës" - "Po aktivizon radion…" - "Nuk ka shërbim. Po provon sërish…" - "Nuk mund të telefonohet. %s nuk është një numër urgjence." - "Nuk mund të telefonohet. Formo një numër urgjence." - "Përdor tastierën për të formuar numrin" - "Vendose në pritje telefonatën" - "Rifillo telefonatën" - "Mbylle telefonatën" - "Shfaq bllokun e formimit të numrit" - "Fshih bllokun e formimit të numrit" - "Çaktivizo audion" - "Aktivizo audion" - "Shto telefonatë" - "Shkri telefonatat" - "Shkëmbe" - "Menaxho telefonatat" - "Menaxho telefonatën konferencë" - "Telefonatë konferencë" - "Menaxho" - "Audioja" - "Telefonatë me video" - "Ndërro në telefonatë me video" - "Ndërro kamerën" - "Aktivizo kamerën" - "Çaktivizo kamerën" - "Opsione të tjera" - "Luajtësi filloi" - "Luajtësi ndaloi" - "Kamera nuk është gati" - "Kamera është gati" - "Ngjarje e panjohur në sesionin e telefonatës" - "Shërbimi" - "Konfigurimi" - "<I pavendosur>" - "Cilësime të tjera të telefonatës" - "Telefonatë nëpërmjet %s" - "Hyrëse nëpërmjet %s" - "fotografia e kontaktit" - "bëje private" - "përzgjidh kontaktin" - "Shkruaj përgjigjen tënde..." - "Anulo" - "Dërgo" - "Përgjigju" - "Dërgo SMS" - "Refuzo" - "Përgjigju si telefonatë me video" - "Përgjigju si telefonatë me audio" - "Prano kërkesën për video" - "Refuzo kërkesën për video" - "Prano kërkesën për transmetimin e videos" - "Refuzo kërkesën për transmetimin e videos" - "Prano kërkesën për marrjen e videos" - "Refuzo kërkesën për marrjen e videos" - "Rrëshqit lart për %s." - "Rrëshqit majtas për %s" - "Rrëshqit djathtas për %s" - "Rrëshqit poshtë për %s." - "Dridhja" - "Dridhja" - "Tingulli" - "Tingulli i parazgjedhur (%1$s)" - "Toni i ziles i telefonit" - "Dridhje edhe kur bie zilja" - "Me zile dhe me dridhje" - "Menaxho telefonatën konferencë" - "Numri i urgjencës" - "Fotografia e profilit" - "Kamera joaktive" - "përmes %s" - "Shënimi u dërgua" - "Mesazhet e fundit" - "Informacioni i biznesit" - "%.1f milje larg" - "%.1f km larg" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "Hapet nesër në %s" - "Hapet sot në %s" - "Mbyllet në %s" - "Mbyllur sot në %s" - "Tani është hapur" - "Tani është mbyllur" - "I padëshirueshëm" - "Telefonata përfundoi %1$s" - "Kjo është hera e parë që ky numër ka telefonuar." - "Ne dyshojmë që kjo telefonatë të jetë e padëshirueshme." - "Blloko/raporto të padëshiruar" - "Shto një kontakt" - "Jo i padëshiruar" - diff --git a/InCallUI/res/values-sr/strings.xml b/InCallUI/res/values-sr/strings.xml deleted file mode 100644 index 3a3820d8c..000000000 --- a/InCallUI/res/values-sr/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Телефон" - "На чекању" - "Непознат" - "Приватан број" - "Телефонска говорница" - "Конференцијски позив" - "Позив је прекинут" - "Звучник" - "Слушалица телефона" - "Жичане наглавне слушалице" - "Bluetooth" - "Желите ли да пошаљете следеће тонове?\n" - "Тонови се шаљу\n" - "Пошаљи" - "Да" - "Не" - "Замените џокер знак са" - "Конференцијски позив %s" - "Број говорне поште" - "Позива се" - "Поново се бира" - "Конференцијски позив" - "Долазни позив" - "Долазни позив за Work" - "Позив је завршен" - "На чекању" - "Веза се прекида" - "Позив је у току" - "Мој број је %s" - "Повезује се видео позив" - "Видео позив" - "Захтева се видео позив" - "Повезивање видео позива није успело" - "Захтев за видео позив је одбијен" - "Број за повратни позив\n %1$s" - "Број за хитан повратни позив\n %1$s" - "Позива се" - "Пропуштен позив" - "Пропуштени позиви" - "Број пропуштених позива: %s" - "Пропуштен позив од: %s" - "Текући позив" - "Текући позив за Work" - "Текући Wi-Fi позив" - "Текући позив за Work преко Wi-Fi-ја" - "На чекању" - "Долазни позив" - "Долазни позив за Work" - "Долазни Wi-Fi позив" - "Долазни позив за Work преко Wi-Fi-ја" - "Долазни видео позив" - "Сумња на непожељан долазни позив" - "Захтев за долазни видео позив" - "Нова порука говорне поште" - "Нова порука говорне поште (%d)" - "Позови %s" - "Непознат број говорне поште" - "Мобилна мрежа није доступна" - "Изабрана мрежа (%s) није доступна" - "Одговори" - "Прекини везу" - "Видео" - "Гласовни" - "Прихватам" - "Одбаци" - "Узврати позив" - "Пошаљи SMS" - "Позив је у току на другом уређају" - "Пребаци позив" - "Да бисте упутили позив, прво искључите режим рада у авиону." - "Није регистровано на мрежи." - "Мобилна мрежа није доступна." - "Да бисте упутили позив, унесите важећи број." - "Позив није успео." - "Покреће се MMI секвенца..." - "Услуга није подржана." - "Замена позива није успела." - "Раздвајање позива није успело." - "Пребацивање није успело." - "Конференцијски позив није успео." - "Одбијање позива није успело." - "Успостављање позива није успело." - "SIP позив" - "Хитни позив" - "Укључује се радио…" - "Мобилна мрежа није доступна. Покушавамо поново…" - "Позив није успео. %s није број за хитне случајеве." - "Позив није успео. Позовите број за хитне случајеве." - "Користите тастатуру за позивање" - "Стави позив на чекање" - "Настави позив" - "Заврши позив" - "Прикажи нумеричку тастатуру" - "Сакриј нумеричку тастатуру" - "Искључи звук" - "Укључи звук" - "Додај позив" - "Обједини позиве" - "Замени" - "Управљај позивима" - "Управљај конференцијским позивом" - "Конференцијски позив" - "Управљај" - "Аудио" - "Видео позив" - "Промени у гласовни позив" - "Промени камеру" - "Укључи камеру" - "Искључи камеру" - "Још опција" - "Плејер је покренут" - "Плејер је заустављен" - "Камера није спремна" - "Камера је спремна" - "Непознат догађај сесије позива" - "Услуга" - "Подешавање" - "<Није подешено>" - "Друга подешавања позива" - "Позива се преко добављача %s" - "Долазни позив преко %s" - "слика контакта" - "иди на приватно" - "изаберите контакт" - "Напишите сами…" - "Откажи" - "Пошаљи" - "Одговори" - "Пошаљи SMS" - "Одбиј" - "Одговори видео позивом" - "Одговори аудио-позивом" - "Прихвати захтев за видео" - "Одбиј захтев за видео" - "Прихвати захтев за одлазни видео позив" - "Одбиј захтев за одлазни видео позив" - "Прихвати захтев за долазни видео позив" - "Одбиј захтев за долазни видео позив" - "Превуците нагоре за %s." - "Превуците улево за %s." - "Превуците удесно за %s." - "Превуците надоле за %s." - "Вибрација" - "Вибрација" - "Звук" - "Подразумевани звук (%1$s)" - "Мелодија звона телефона" - "Вибрирај када звони" - "Мелодија звона и вибрација" - "Управљај конференцијским позивом" - "Број за хитне случајеве" - "Слика профила" - "Камера је искључена" - "на %s" - "Белешка је послата" - "Недавне поруке" - "Информације о предузећу" - "Удаљеност је %.1f mi" - "Удаљеност је %.1f km" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "Отвара се сутра у %s" - "Отвара се данас у %s" - "Затвара се у %s" - "Затворило се данас у %s" - "Тренутно отворено" - "Тренутно затворено" - "Непожељан позивалац" - "Позив се завршио у %1$s" - "Ово је био први позив са овог броја." - "Сумњамо да је овај позив непожељан." - "Блокирај/пријави" - "Додај контакт" - "Није непожељан" - diff --git a/InCallUI/res/values-sv/strings.xml b/InCallUI/res/values-sv/strings.xml deleted file mode 100644 index 980ecdb07..000000000 --- a/InCallUI/res/values-sv/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefon" - "Parkerat" - "Okänd" - "Privat nummer" - "Telefonautomat" - "Konferenssamtal" - "Samtalet avbröts" - "Högtalare" - "Telefonlur" - "Trådanslutet headset" - "Bluetooth" - "Ska följande toner skickas?\nBREAK" - "Skickar signaler\n" - "Skicka" - "Ja" - "Nej" - "Ersätt jokertecknet med" - "Konferenssamtal %s" - "Nummer till röstbrevlåda" - "Ringer" - "Ringer upp igen" - "Konferenssamtal" - "Inkommande samtal" - "Inkommande jobbsamtal" - "Samtal avslutat" - "Parkerat" - "Lägger på" - "I samtal" - "Mitt telefonnummer är %s" - "Ansluter video" - "Videosamtal" - "Begär video" - "Det gick inte att ansluta till videosamtalet" - "Videobegäran avslogs" - "Ditt nummer för återuppringning\n %1$s" - "Ditt nummer för återuppringning vid nödsamtal\n %1$s" - "Ringer" - "Missat samtal" - "Missade samtal" - "%s missade samtal" - "Missat samtal från %s" - "Pågående samtal" - "Pågående jobbsamtal" - "Pågående Wi-Fi-samtal" - "Pågående jobbsamtal via Wi-Fi" - "Parkerat" - "Inkommande samtal" - "Inkommande jobbsamtal" - "Inkommande Wi-Fi-samtal" - "Inkommande jobbsamtal via Wi-Fi" - "Inkommande videosamtal" - "Inkommande misstänkt spamsamtal" - "Inkommande begäran om videosamtal" - "Nytt röstmeddelande" - "Nytt röstmeddelande (%d)" - "Ring %s" - "Nummer till röstbrevlåda okänt" - "Ingen tjänst" - "Det valda nätverket (%s) är inte tillgängligt" - "Svara" - "Lägg på" - "Video" - "Röstsamtal" - "Godkänn" - "Ignorera" - "Ring upp" - "Meddelande" - "Pågående samtal på en annan enhet" - "Överför samtal" - "Om du vill ringa måste du först inaktivera flygplansläge." - "Inte registrerat på nätverk." - "Det finns inget mobilnät tillgängligt." - "Ange ett giltigt nummer om du vill ringa ett samtal." - "Det gick inte att ringa." - "Startar sekvens för MMI-kod …" - "Tjänsten stöds inte." - "Det gick inte att växla mellan samtal." - "Det gick inte att koppla isär samtalen." - "Det gick inte att överföra." - "Det gick inte att starta ett konferenssamtal." - "Det gick inte att avvisa samtalet." - "Det gick inte att släppa samtal." - "SIP-samtal" - "Nödsamtal" - "Sätter på radion …" - "Ingen tjänst. Försöker igen …" - "Det gick inte att ringa. %s är inget nödnummer." - "Det gick inte att ringa. Slå ett nödnummer." - "Använd tangentbordet om du vill ringa" - "Parkera samtal" - "Återuppta samtal" - "Avsluta samtal" - "Visa knappsats" - "Dölj knappsats" - "Ljud av" - "Sluta ignorera" - "Lägg till samtal" - "Koppla ihop samtal" - "Byt" - "Hantera samtal" - "Hantera konferenssamtal" - "Konferenssamtal" - "Hantera" - "Ljud" - "Videosamt." - "Byt till röstsamtal" - "Byt kamera" - "Slå på kameran" - "Stäng av kameran" - "Fler alternativ" - "Videospelaren har startats" - "Videospelaren har stoppats" - "Kameran är inte klar" - "Kameran är klar" - "Okänd händelse vid samtalssession" - "Tjänst" - "Konfiguration" - "<Har inte angetts>" - "Övriga samtalsinställningar" - "Ringer via %s" - "Inkommande via %s" - "kontaktbild" - "gör privat" - "välj kontakt" - "Skriv ett eget svar …" - "Avbryt" - "Skicka" - "Svara" - "Skicka sms" - "Avvisa" - "Svara som videosamtal" - "Svara som röstsamtal" - "Godkänn videobegäran" - "Avvisa videobegäran" - "Godkänn begäran om att skicka video" - "Avvisa begäran om att skicka video" - "Godkänn begäran om att ta emot video" - "Avvisa begäran om att ta emot video" - "%s genom att dra uppåt." - "%s genom att dra åt vänster." - "%s genom att dra åt höger." - "%s genom att dra nedåt." - "Vibrera" - "Vibrera" - "Ljud" - "Standardsignal (%1$s)" - "Ringsignal" - "Enheten vibrerar vid samtal" - "Ringsignal och vibration" - "Hantera konferenssamtal" - "Nödsamtalsnummer" - "Profilbild" - "Kamera av" - "via %s" - "Anteckningen har skickats" - "Senaste meddelandena" - "Företagsuppgifter" - "%.1f miles bort" - "%.1f km bort" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "Öppnar i morgon kl. %s" - "Öppnar i dag kl. %s" - "Stänger kl. %s" - "Stängde i dag kl. %s" - "Öppet" - "Stängt" - "Misstänkt spamsamtal" - "Samtalet avslutat %1$s" - "Det här är första gången det här numret ringde dig." - "Vi misstänkte att det här samtalet var en spammare." - "Blockera/rapp. spam" - "Lägg till kontakt" - "Inte spam" - diff --git a/InCallUI/res/values-sw/strings.xml b/InCallUI/res/values-sw/strings.xml deleted file mode 100644 index d60c4901a..000000000 --- a/InCallUI/res/values-sw/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Simu" - "Inangoja" - "Isiyojulikana" - "Nambari ya faragha" - "Simu ya kulipia" - "Simu ya mkutano" - "Simu imekatwa" - "Spika" - "Kipaza sauti cha kichwani" - "Vifaa vya sauti visivyo na waya" - "Bluetooth" - "Ungependa kutuma milio ya sauti inayofuata? \n" - "Inatuma milio ya simu\n" - "Tuma" - "Ndiyo" - "Hapana" - "Badilisha herufi inayojitegemea kwa" - "Simu ya mkutano %s" - "Nambari ya ujumbe wa sauti" - "Inapiga" - "Inapiga simu tena" - "Simu ya mkutano" - "Unapigiwa simu" - "Simu ya kazi inayoingia" - "Simu imekamilika" - "Inangoja" - "Kukata simu" - "Katika simu" - "Nambari yangu ni %s" - "Inaunganisha video" - "Hangout ya Video" - "Inaomba video" - "Haiwezi kuunganisha Hangout ya video" - "Ombi la video limekataliwa" - "Nambari yako ya kupigiwa simu\n%1$s" - "Nambari yako ya dharura ya kupigiwa simu\n%1$s" - "Inapiga" - "Simu ambayo hukujibu" - "Simu ambazo hukujibu" - "Simu %s ambazo hukujibu" - "Simu ambayo hukujibu kutoka %s" - "Simu inayoendelea" - "Simu ya kazi inayoendelea" - "Simu ya Wi-Fi inayoendelea" - "Simu ya Wi-Fi ya kazi inayoendelea" - "Inangoja" - "Unapigiwa simu" - "Unapigiwa simu ya kazi" - "Unapigiwa simu kupitia Wi-Fi" - "Unapigiwa simu ya kazini kupitia Wi-Fi" - "Hangout ya Video inayoingia" - "Simu inayoingia inashukiwa kuwa taka" - "Ombi linaloingia la video" - "Ujumbe mpya wa sauti" - "Ujumbe (%d) mpya wa sauti" - "Piga %s" - "Nambari ya ujumbe wa sauti haijulikani." - "Hakuna huduma" - "Mtandao uliochaguliwa (%s) haupatikani" - "Jibu" - "Kata simu" - "Video" - "Sauti" - "Kubali" - "Ondoa" - "Mpigie" - "Ujumbe" - "Una Hangout inayoendelea kwenye kifaa kingine" - "Hamisha Hangout" - "Ili upige simu kwanza, zima Hali ya ndegeni." - "Haijasajiliwa kwenye mtandao." - "Mitandao ya simu za mkononi haipatikani." - "Ili upige simu, weka nambari sahihi." - "Haiwezi kupiga simu." - "Inaanzisha msururu wa MMI…" - "Huduma haitumiki." - "Haiwezi kubadili simu." - "Haiwezi kutenganisha simu." - "Haiwezi kuhamisha." - "Haiwezi kushiriki katika simu ya mkutano." - "Haiwezi kukataa simu." - "Haiwezi kutoa simu." - "Simu ya SIP" - "Simu ya dharura" - "Inawasha redio..." - "Hakuna huduma. Inajaribu tena..." - "Haiwezi kupiga simu. %s si nambari ya dharura." - "Haiwezi kupiga simu. Piga simu nambari ya dharura." - "Tumia kibodi kubonyeza" - "Shikilia Simu" - "Endelea na Simu" - "Kata Simu" - "Onyesha Vitufe vya Kupiga Simu" - "Ficha Vitufe vya Kupiga Simu" - "Zima Sauti" - "Rejesha sauti" - "Ongeza simu" - "Unganisha simu" - "Badili" - "Dhibiti simu" - "Dhibiti simu ya mkutano" - "Mkutano kwenye simu" - "Dhibiti" - "Sauti" - "Hangout ya Video" - "Badilisha iwe simu ya sauti" - "Badilisha kamera" - "Washa kamera" - "Zima kamera" - "Chaguo zaidi" - "Kichezaji Kimeanzishwa" - "Kichezaji Kimekomeshwa" - "Kamera haiko tayari" - "Kamera iko tayari" - "Tukio lisilojulikana la kipindi cha simu" - "Huduma" - "Weka mipangilio" - "<Haijawekwa>" - "Mipangilio mingine ya simu" - "Kupiga simu kupitia %s" - "Simu zinazoingia kupitia %s" - "picha ya anwani" - "tumia kwa faragha" - "chagua anwani" - "Andika yako binafsi..." - "Ghairi" - "Tuma" - "Jibu" - "Tuma SMS" - "Kataa" - "Pokea kama Hangout ya Video" - "Pokea kama simu ya sauti" - "Kubali ombi la video" - "Kataa ombi la video" - "Kubali ombi la kutuma kupitia hangout ya video" - "Kataa ombi la kutuma kupitia hangout ya video" - "Kubali ombi la kupokea kupitia hangout ya video" - "Kataa ombi la kupokea kupitia hangout ya video" - "Telezesha kidole juu ili %s ." - "Telezesha kidole kushoto ili %s." - "Telezesha kidole kulia ili %s." - "Telezesha kidole chini ili %s." - "Mtetemo" - "Mtetemo" - "Mlio" - "Sauti chaguo-msingi (%1$s)" - "Mlio wa simu" - "Tetema wakati wa kuita" - "Mlio wa simu na Mtetemo" - "Dhibiti simu ya mkutano" - "Nambari ya dharura" - "Picha ya wasifu" - "Kamera imezimwa" - "kupitia %s" - "Dokezo limetumwa" - "Ujumbe wa hivi majuzi" - "Maelezo ya biashara" - "Umbali wa maili %.1f" - "Umbali wa kilomita %.1f" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "Itafunguliwa kesho saa %s" - "Itafunguliwa leo saa %s" - "Hufungwa saa %s" - "Imefungwa leo saa %s" - "Sasa imefunguliwa" - "Sasa imefungwa" - "Mpiga simu taka" - "Simu imekatwa %1$s" - "Hii ndiyo mara ya kwanza nambari hii imekupigia." - "Tunashuku kwamba simu hii ni taka." - "Zuia/ripoti taka" - "Ongeza anwani" - "Si barua taka" - diff --git a/InCallUI/res/values-sw360dp/dimens.xml b/InCallUI/res/values-sw360dp/dimens.xml deleted file mode 100644 index 9fbcd605b..000000000 --- a/InCallUI/res/values-sw360dp/dimens.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - 22sp - 18sp - 45dp - 34sp - 18sp - - - @dimen/dialpad_key_number_default_margin_bottom - - @dimen/dialpad_zero_key_number_default_margin_bottom - @dimen/dialpad_digits_text_size - @dimen/dialpad_digits_height - @dimen/dialpad_key_numbers_default_size - diff --git a/InCallUI/res/values-sw410dp/config.xml b/InCallUI/res/values-sw410dp/config.xml deleted file mode 100644 index a57f86784..000000000 --- a/InCallUI/res/values-sw410dp/config.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - 6 - \ No newline at end of file diff --git a/InCallUI/res/values-ta/strings.xml b/InCallUI/res/values-ta/strings.xml deleted file mode 100644 index 1ee57b4ba..000000000 --- a/InCallUI/res/values-ta/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "ஃபோன்" - "ஹோல்டில் உள்ளது" - "தெரியாத எண்" - "தனிப்பட்ட எண்" - "கட்டணத் தொலைபேசி" - "குழு அழைப்பு" - "அழைப்பு நிறுத்தப்பட்டது" - "ஸ்பீக்கர்" - "ஹேண்ட்செட் இயர்ஃபீஸ்" - "வயருள்ள ஹெட்செட்" - "புளூடூத்" - "பின்வரும் டோன்களை அனுப்பவா?\n" - "டோன்களை அனுப்புகிறது\n" - "அனுப்பு" - "ஆம்" - "வேண்டாம்" - "சிறப்புக்குறியை இதன் மூலம் மாற்றியமை" - "குழு அழைப்பு: %s" - "குரலஞ்சல் எண்" - "அழைக்கிறது" - "மீண்டும் டயல் செய்கிறது" - "குழு அழைப்பு" - "உள்வரும் அழைப்பு" - "உள்வரும் அழைப்பு (பணி)" - "அழைப்பு முடிந்தது" - "ஹோல்டில் உள்ளது" - "துண்டிக்கிறது" - "அழைப்பில்" - "எனது எண்: %s" - "வீடியோவை இணைக்கிறது" - "வீடியோ அழைப்பு" - "வீடியோவைக் கோருகிறது" - "வீடியோ அழைப்பை இணைக்க முடியவில்லை" - "வீடியோ கோரிக்கை நிராகரிக்கப்பட்டது" - "உங்களைத் திரும்ப அழைப்பதற்கான எண்\n %1$s" - "அவசர அழைப்பு எண்\n %1$s" - "அழைக்கிறது" - "தவறிய அழைப்பு" - "தவறிய அழைப்புகள்" - "%s தவறிய அழைப்புகள்" - "%s இடமிருந்து தவறிய அழைப்பு" - "செயலில் இருக்கும் அழைப்பு" - "செயலில் இருக்கும் அழைப்பு (பணி)" - "செயலில் இருக்கும் வைஃபை அழைப்பு" - "செயலில் இருக்கும் வைஃபை அழைப்பு" - "ஹோல்டில் உள்ளது" - "உள்வரும் அழைப்பு" - "உள்வரும் அழைப்பு (பணி)" - "உள்வரும் வைஃபை அழைப்பு" - "உள்வரும் வைஃபை அழைப்பு (பணி)" - "உள்வரும் வீடியோ அழைப்பு" - "உள்வரும் சந்தேகத்திற்குரிய ஸ்பேம் அழைப்பு" - "உள்வரும் வீடியோ கோரிக்கை" - "புதிய குரலஞ்சல்" - "புதிய குரலஞ்சல் (%d)" - "%sஐ அழை" - "குரலஞ்சல் எண் அறியப்படவில்லை" - "சேவை இல்லை" - "தேர்ந்தெடுத்த நெட்வொர்க் (%s) கிடைக்கவில்லை" - "பதிலளி" - "துண்டி" - "வீடியோ" - "குரல்" - "ஏற்கிறேன்" - "நிராகரி" - "திரும்ப அழை" - "செய்தி அனுப்பு" - "மற்றொரு சாதனத்தில் செயலில் இருக்கும் அழைப்பு" - "அழைப்பை இடமாற்று" - "அழைப்பதற்கு, முதலில் விமானப் பயன்முறையை முடக்கவும்." - "நெட்வொர்க்கில் பதிவுசெய்யப்படவில்லை." - "செல்லுலார் நெட்வொர்க் கிடைக்கவில்லை." - "அழைக்க, சரியான எண்ணை உள்ளிடவும்." - "அழைக்க முடியாது." - "MMI வரிசையைத் தொடங்குகிறது..." - "சேவை ஆதரிக்கப்படவில்லை." - "அழைப்புகளில் மாற முடியாது." - "அழைப்பைப் பிரிக்க முடியாது." - "மாற்ற முடியாது." - "குழு அழைப்பு செய்ய முடியாது." - "அழைப்பை நிராகரிக்க முடியாது." - "அழைப்பை(அழைப்புகளை) விடுவிக்க முடியாது." - "SIP அழைப்பு" - "அவசர அழைப்பு" - "ரேடியோவை இயக்குகிறது…" - "சேவை இல்லை. மீண்டும் முயல்கிறது…" - "%s என்பது அவசர அழைப்பு எண் இல்லை என்பதால் அழைக்க முடியாது." - "அழைக்க முடியாது. அவசர அழைப்பு எண்ணை அழைக்கவும்." - "டயல் செய்ய, விசைப்பலகையைப் பயன்படுத்தவும்" - "அழைப்பை ஹோல்டில் வை" - "அழைப்பை மீண்டும் தொடங்கு" - "அழைப்பை முடி" - "டயல்பேடைக் காட்டு" - "டயல்பேடை மறை" - "ஒலியடக்கு" - "ஒலி இயக்கு" - "அழைப்பைச் சேர்" - "அழைப்புகளை இணை" - "மாற்று" - "அழைப்புகளை நிர்வகி" - "குழு அழைப்பை நிர்வகி" - "குழு அழைப்பு" - "நிர்வகி" - "ஆடியோ" - "வீடியோ அழைப்பு" - "குரல் அழைப்பிற்கு மாறு" - "கேமராவை மாற்று" - "கேமராவை இயக்கு" - "கேமராவை முடக்கு" - "கூடுதல் விருப்பங்கள்" - "வீடியோ பிளேயர் துவங்கியது" - "வீடியோ பிளேயர் நிறுத்தப்பட்டது" - "கேமரா தயாராக இல்லை" - "கேமரா தயார்" - "தெரியாத அழைப்பு நேர நிகழ்வு" - "சேவை" - "அமை" - "<அமைக்கப்படவில்லை>" - "பிற அழைப்பு அமைப்புகள்" - "%s வழியாக அழைக்கிறது" - "%s மூலம் உள்வரும் அழைப்புகள்" - "தொடர்புப் படம்" - "தனிப்பட்டதிற்குச் செல்" - "தொடர்பைத் தேர்ந்தெடுக்கவும்" - "சொந்தமாக எழுதவும்..." - "ரத்துசெய்" - "அனுப்பு" - "பதிலளி" - "SMS அனுப்பு" - "நிராகரி" - "வீடியோ அழைப்பில் பதிலளி" - "ஆடியோ அழைப்பில் பதிலளி" - "வீடியோ கோரிக்கையை அனுமதி" - "வீடியோ கோரிக்கையை நிராகரி" - "வீடியோவைப் பரிமாற்றும் கோரிக்கையை அனுமதி" - "வீடியோவைப் பரிமாற்றும் கோரிக்கையை நிராகரி" - "வீடியோவைப் பெறும் கோரிக்கையை அனுமதி" - "வீடியோவைப் பெறும் கோரிக்கையை நிராகரி" - "%s, மேலே ஸ்லைடு செய்க." - "%s, இடப்புறம் ஸ்லைடு செய்க." - "%s, வலப்புறம் ஸ்லைடு செய்க." - "%s, கீழே ஸ்லைடு செய்க." - "அதிர்வுறு" - "அதிர்வுறு" - "ஒலி" - "இயல்பு ஒலி (%1$s)" - "ஃபோனின் ரிங்டோன்" - "அழைக்கும் போது அதிர்வுறு" - "ரிங்டோன் & அதிர்வு" - "குழு அழைப்பை நிர்வகி" - "அவசர அழைப்பு எண்" - "சுயவிவரப் படம்" - "கேமரா முடக்கப்பட்டது" - "%s வழியாக" - "குறிப்பு அனுப்பப்பட்டது" - "சமீபத்திய செய்திகள்" - "வணிகத் தகவல்" - "%.1f மைல் தொலைவில்" - "%.1f கிமீ தொலைவில்" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "நாளை %sக்குத் திறக்கப்படும்" - "இன்று %sக்குத் திறக்கப்படும்" - "%sக்கு மூடப்படும்" - "இன்று %sக்கு மூடப்பட்டது" - "இப்போது திறக்கப்பட்டுள்ளது" - "இப்போது மூடப்பட்டுள்ளது" - "சந்தேகத்திற்குரிய ஸ்பேம் அழைப்பாளர்" - "அழைப்பு துண்டிக்கப்பட்டது %1$s" - "இந்த எண்ணிலிருந்து உங்களுக்கு அழைப்பு வந்தது இதுவே முதல் முறை." - "இந்த அழைப்பு ஸ்பேமராக இருக்கக்கூடும் என சந்தேகிக்கிறோம்." - "தடு/ஸ்பேமென புகாரளி" - "தொடர்பைச் சேர்" - "ஸ்பேமில்லை" - diff --git a/InCallUI/res/values-te/strings.xml b/InCallUI/res/values-te/strings.xml deleted file mode 100644 index 936f1be7c..000000000 --- a/InCallUI/res/values-te/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "ఫోన్" - "హోల్డ్‌లో ఉంది" - "తెలియదు" - "ప్రైవేట్ నంబర్" - "పే ఫోన్" - "కాన్ఫరెన్స్ కాల్" - "కాల్ కట్ అయింది" - "స్పీకర్" - "హ్యాండ్‌సెట్ ఇయర్‌పీస్" - "వైర్ గల హెడ్‌సెట్" - "బ్లూటూత్" - "కింది టోన్‌లను పంపాలా?\n" - "టోన్‌లు పంపుతోంది\n" - "పంపు" - "అవును" - "వద్దు" - "దీనితో వైల్డ అక్షరాన్ని భర్తీ చేయండి" - "కాన్ఫరెన్స్ కాల్ %s" - "వాయిస్ మెయిల్ నంబర్" - "డయల్ చేస్తోంది" - "మళ్లీ డయల్ చేస్తోంది" - "కాన్ఫరెన్స్ కాల్" - "ఇన్‌కమింగ్ కాల్" - "ఇన్‌కమింగ్ కార్యాలయ కాల్" - "కాల్ ముగిసింది" - "హోల్డ్‌లో ఉంది" - "ముగిస్తోంది" - "కాల్‌లో ఉంది" - "నా నంబర్ %s" - "వీడియోను కనెక్ట్ చేస్తోంది" - "వీడియో కాల్" - "వీడియో కోసం అభ్యర్థిస్తోంది" - "వీడియో కాల్‌ను కనెక్ట్ చేయలేరు" - "వీడియో అభ్యర్థన తిరస్కరించబడింది" - "మీ కాల్‌బ్యాక్ నంబర్\n %1$s" - "మీ అత్యవసర కాల్‌బ్యాక్ నంబర్\n %1$s" - "డయల్ చేస్తోంది" - "సమాధానం ఇవ్వని కాల్" - "సమాధానం ఇవ్వని కాల్‌లు" - "%s సమాధానం ఇవ్వని కాల్‌లు" - "%s నుండి సమాధానం ఇవ్వని కాల్" - "కాల్ కొనసాగుతోంది" - "కార్యాలయ కాల్ కొనసాగుతోంది" - "Wi-Fi కాల్ కొనసాగుతోంది" - "Wi-Fi కార్యాలయ కాల్ కొనసాగుతోంది" - "హోల్డ్‌లో ఉంది" - "ఇన్‌కమింగ్ కాల్" - "ఇన్‌కమింగ్ కార్యాలయ కాల్" - "ఇన్‌కమింగ్ Wi-Fi కాల్" - "ఇన్‌కమింగ్ Wi-Fi కార్యాలయ కాల్" - "ఇన్‌కమింగ్ వీడియో కాల్" - "అనుమానాస్పద స్పామ్ కాల్ వస్తోంది" - "ఇన్‌కమింగ్ వీడియో అభ్యర్థన" - "కొత్త వాయిస్ మెయిల్" - "కొత్త వాయిస్ మెయిల్ (%d)" - "%sకు డయల్ చేయండి" - "వాయిస్ మెయిల్ నంబర్ తెలియదు" - "సేవ లేదు" - "ఎంచుకున్న నెట్‌వర్క్ (%s) అందుబాటులో లేదు" - "సమాధానం" - "కాల్ ముగించు" - "వీడియో" - "వాయిస్" - "ఆమోదిస్తున్నాను" - "తీసివేయి" - "తిరిగి కాల్ చేయి" - "సందేశం పంపు" - "మరో పరికరంలో కాల్ జరుగుతోంది" - "కాల్‌ను బదిలీ చేయి" - "కాల్ చేయడానికి, ముందుగా ఎయిర్‌ప్లైన్ మోడ్‌ను ఆపివేయండి." - "నెట్‌వర్క్‌లో నమోదు కాలేదు." - "సెల్యులార్ నెట్‌వర్క్ అందుబాటులో లేదు." - "కాల్ చేయడానికి, చెల్లుబాటు అయ్యే నంబర్‌ను నమోదు చేయండి." - "కాల్ చేయలేరు." - "MMI శ్రేణిని ప్రారంభిస్తోంది…" - "సేవకు మద్దతు లేదు." - "కాల్‌లను మార్చలేరు." - "కాల్‌ను వేరు చేయలేరు." - "బదిలీ చేయలేరు." - "కాన్ఫరెన్స్ కాల్ కుదరదు." - "కాల్‌ను తిరస్కరించలేరు." - "కాల్(ల)ను విడిచిపెట్టలేరు." - "SIP కాల్" - "అత్యవసర కాల్" - "రేడియోను ఆన్ చేస్తోంది…" - "సేవ లేదు. మళ్లీ ప్రయత్నిస్తోంది…" - "కాల్ చేయలేరు. %s అత్యవసర నంబర్ కాదు." - "కాల్ చేయలేరు. అత్యవసర నంబర్‌కు డయల్ చేయండి." - "డయల్ చేయడానికి కీబోర్డ్‌ను ఉపయోగించండి" - "కాల్‌ను హోల్డ్‌లో ఉంచు" - "కాల్‌ను పునఃప్రారంభించు" - "కాల్‌ని ముగించు" - "డయల్‌ప్యాడ్‌ను చూపు" - "డయల్‌ప్యాడ్‌ను దాచు" - "మ్యూట్ చేయి" - "అన్‌మ్యూట్ చేయి" - "కాల్‌ను జోడించు" - "కాల్‌లను విలీనం చేయి" - "స్వాప్ చేయి" - "కాల్‌లను నిర్వహించు" - "కాన్ఫరెన్స్ కాల్‌ను నిర్వహించు" - "కాన్ఫరెన్స్ కాల్" - "నిర్వహించు" - "ఆడియో" - "వీడియో కాల్" - "వాయిస్ కాల్‌కి మార్చు" - "కెమెరాను మార్చు" - "కెమెరాను ఆన్ చేయి" - "కెమెరాను ఆఫ్ చేయి" - "మరిన్ని ఎంపికలు" - "ప్లేయర్ ప్రారంభమైంది" - "ప్లేయర్ ఆపివేయబడింది" - "కెమెరా సిద్ధంగా లేదు" - "కెమెరా సిద్ధంగా ఉంది" - "తెలియని కాల్ సెషన్ ఉదంతం" - "సేవ" - "సెటప్ చేయండి" - "<సెట్ చేయలేదు>" - "ఇతర కాల్ సెట్టింగ్‌లు" - "%s ద్వారా కాల్ చేయబడుతోంది" - "%s ద్వారా ఇన్‌కమింగ్" - "పరిచయం ఫోటో" - "ప్రైవేట్‌గా వెళ్లు" - "పరిచయాన్ని ఎంచుకోండి" - "మీ స్వంతంగా వ్రాయండి…" - "రద్దు చేయి" - "పంపు" - "సమాధానం" - "SMSని పంపుతుంది" - "తిరస్కరిస్తుంది" - "వీడియో కాల్ రూపంలో సమాధానం" - "ఆడియో కాల్ రూపంలో సమాధానం" - "వీడియో అభ్యర్థనను ఆమోదిస్తుంది" - "వీడియో అభ్యర్థనను తిరస్కరిస్తుంది" - "వీడియో ప్రసరణ అభ్యర్థనను ఆమోదిస్తుంది" - "వీడియో ప్రసరణ అభ్యర్థనను తిరస్కరిస్తుంది" - "వీడియో స్వీకరణ అభ్యర్థనను ఆమోదిస్తుంది" - "వీడియో స్వీకరణ అభ్యర్థనను తిరస్కరిస్తుంది" - "%s కోసం పైకి స్లైడ్ చేయండి." - "%s కోసం ఎడమవైపుకు స్లైడ్ చేయండి." - "%s కోసం కుడివైపుకు స్లైడ్ చేయండి." - "%s కోసం కిందికి స్లైడ్ చేయండి." - "వైబ్రేట్" - "వైబ్రేట్" - "ధ్వని" - "డిఫాల్ట్ ధ్వని (%1$s)" - "ఫోన్ రింగ్‌టోన్" - "రింగ్ అయ్యేప్పుడు వైబ్రేషన్" - "రింగ్‌టోన్ & వైబ్రేట్" - "కాన్ఫరెన్స్ కాల్‌ను నిర్వహించు" - "అత్యవసర నంబర్" - "ప్రొఫైల్ ఫోటో" - "కెమెరా ఆఫ్‌లో ఉంది" - "%s ద్వారా" - "గమనిక పంపబడింది" - "ఇటీవలి సందేశాలు" - "వ్యాపార సంస్థ సమాచారం" - "%.1f మై దూరంలో ఉంది" - "%.1f కి.మీ దూరంలో ఉంది" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "రేపు %sకి తెరవబడుతుంది" - "ఈరోజు %sకి తెరవబడుతుంది" - "%sకి మూసివేయబడుతుంది" - "ఈరోజు %sకి మూసివేయబడి ఉంటుంది" - "ఇప్పుడు తెరిచి ఉంది" - "ఇప్పుడు మూసివేయబడింది" - "అనుమానిత స్పామ్ కాలర్" - "కాల్ ముగిసింది %1$s" - "ఈ నంబర్ నుండి మీకు కాల్ రావడం ఇదే మొదటిసారి." - "ఈ కాల్ స్పామర్ కావచ్చని మేము అనుమానిస్తున్నాము." - "బ్లాక్/స్పామ్ నివే." - "పరిచయాన్ని జోడించు" - "స్పామ్ కాదు" - diff --git a/InCallUI/res/values-th/strings.xml b/InCallUI/res/values-th/strings.xml deleted file mode 100644 index 69ae44d07..000000000 --- a/InCallUI/res/values-th/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "โทรศัพท์" - "พักสาย" - "ไม่ทราบ" - "หมายเลขส่วนตัว" - "โทรศัพท์สาธารณะ" - "การประชุมสาย" - "สายหลุด" - "ลำโพง" - "ชุดหูฟังโทรศัพท์" - "ชุดหูฟังแบบมีสาย" - "บลูทูธ" - "ส่งหมายเลขต่อไปไหม\n" - "กำลังส่งหมายเลข\n" - "ส่ง" - "ใช่" - "ไม่" - "แทนที่อักขระแทนด้วย" - "การประชุมสาย %s" - "หมายเลขข้อความเสียง" - "กำลังโทรออก" - "โทรใหม่" - "การประชุมสาย" - "สายเรียกเข้า" - "มีสายเรียกเข้าจากที่ทำงาน" - "วางสายแล้ว" - "พักสาย" - "กำลังวางสาย" - "กำลังใช้สาย" - "หมายเลขของฉันคือ %s" - "กำลังเชื่อมต่อวิดีโอ" - "แฮงเอาท์วิดีโอ" - "กำลังขอวิดีโอ" - "ไม่สามารถเชื่อมต่อแฮงเอาท์วิดีโอได้" - "คำขอวิดีโอถูกปฏิเสธ" - "หมายเลขโทรกลับของคุณ\n %1$s" - "หมายเลขโทรกลับฉุกเฉินของคุณ\n %1$s" - "กำลังโทรออก" - "สายที่ไม่ได้รับ" - "สายที่ไม่ได้รับ" - "ไม่ได้รับ %s สาย" - "สายที่ไม่ได้รับจาก %s" - "โทรต่อเนื่อง" - "กำลังอยู่ในสายจากที่ทำงาน" - "กำลังโทรผ่าน Wi-Fi" - "กำลังอยู่ในสายจากที่ทำงานผ่าน Wi-Fi" - "พักสาย" - "สายเรียกเข้า" - "มีสายเรียกเข้าจากที่ทำงาน" - "สายโทรเข้าผ่าน Wi-Fi" - "มีสายเรียกเข้าจากที่ทำงานผ่าน Wi-Fi" - "แฮงเอาท์วิดีโอเรียกเข้า" - "สายเรียกเข้าที่สงสัยว่าเป็นสแปม" - "คำขอโทรเข้าเป็นวิดีโอ" - "ข้อความเสียงใหม่" - "ข้อความเสียงใหม่ (%d)" - "หมุนหมายเลข %s" - "ไม่ทราบหมายเลขข้อความเสียง" - "ไม่มีบริการ" - "เครือข่ายที่เลือกไว้ (%s) ไม่พร้อมใช้งาน" - "รับสาย" - "วางสาย" - "วิดีโอ" - "เสียง" - "ยอมรับ" - "ปิด" - "โทรกลับ" - "ข้อความ" - "กำลังใช้สายบนอุปกรณ์อื่น" - "โอนสาย" - "หากต้องการโทรออก ให้ปิดโหมดบนเครื่องบินก่อน" - "ยังไม่ได้ลงทะเบียนบนเครือข่าย" - "เครือข่ายมือถือใช้งานไม่ได้" - "หากต้องการโทรออก โปรดป้อนหมายเลขที่ถูกต้อง" - "ไม่สามารถโทรได้" - "กำลังเริ่มต้นลำดับ MMI..." - "ไม่สนับสนุนบริการนี้" - "ไม่สามารถสลับสายได้" - "ไม่สามารถแยกสายได้" - "ไม่สามารถโอนได้" - "ไม่สามารถประชุมได้" - "ไม่สามารถปฏิเสธสายได้" - "ไม่สามารถเริ่มการโทรได้" - "โทรแบบ SIP" - "หมายเลขฉุกเฉิน" - "กำลังเปิดวิทยุ…" - "ไม่มีบริการ โปรดลองอีกครั้ง…" - "โทรออกไม่ได้ %s ไม่ใช่หมายเลขฉุกเฉิน" - "ไม่สามารถโทรออก โทรหมายเลขฉุกเฉิน" - "ใช้แป้นพิมพ์กดหมายเลขโทรศัพท์" - "พักสาย" - "โทรต่อ" - "วางสาย" - "แสดงแป้นหมายเลข" - "ซ่อนแป้นหมายเลข" - "ปิดเสียง" - "เปิดเสียง" - "เพิ่มการโทร" - "รวมสาย" - "สลับ" - "จัดการการโทร" - "จัดการการประชุมสาย" - "การประชุมสาย" - "จัดการ" - "เสียง" - "แฮงเอาท์วิดีโอ" - "เปลี่ยนเป็นการโทรด้วยเสียง" - "เปลี่ยนกล้อง" - "เปิดกล้อง" - "ปิดกล้อง" - "ตัวเลือกเพิ่มเติม" - "โปรแกรมเล่นเริ่มทำงานแล้ว" - "โปรแกรมเล่นหยุดแล้ว" - "กล้องไม่พร้อมทำงาน" - "กล้องพร้อมทำงาน" - "เหตุการณ์เซสชันการโทรที่ไม่รู้จัก" - "บริการ" - "ตั้งค่า" - "<ไม่ได้ตั้งค่า>" - "การตั้งค่าการโทรอื่นๆ" - "โทรผ่าน %s" - "สายเรียกเข้าผ่าน %s" - "ภาพของรายชื่อติดต่อ" - "เข้าสู่โหมดส่วนตัว" - "เลือกรายชื่อติดต่อ" - "เขียนคำตอบของคุณเอง..." - "ยกเลิก" - "ส่ง" - "รับสาย" - "ส่ง SMS" - "ปฏิเสธ" - "รับสายเป็นแฮงเอาท์วิดีโอ" - "รับสายเป็นการโทรด้วยเสียง" - "ยอมรับคำขอวิดีโอ" - "ปฏิเสธคำขอวิดีโอ" - "ยอมรับคำขอให้ส่งวิดีโอ" - "ปฏิเสธคำขอให้ส่งวิดีโอ" - "ยอมรับคำขอให้รับวิดีโอ" - "ปฏิเสธคำขอให้รับวิดีโอ" - "เลื่อนไปข้างบนเพื่อ %s" - "เลื่อนไปทางซ้ายเพื่อ %s" - "เลื่อนไปทางขวาเพื่อ %s" - "เลื่อนลงเพื่อ %s" - "สั่น" - "สั่น" - "เสียง" - "เสียงเริ่มต้น (%1$s)" - "เสียงเรียกเข้าโทรศัพท์" - "สั่นเมื่อมีสายเข้า" - "เสียงเรียกเข้าและสั่น" - "จัดการการประชุมสาย" - "หมายเลขฉุกเฉิน" - "รูปโปรไฟล์" - "ปิดกล้อง" - "ผ่านหมายเลข %s" - "ส่งโน้ตแล้ว" - "ข้อความล่าสุด" - "ข้อมูลธุรกิจ" - "อยู่ห่างออกไป %.1f ไมล์" - "อยู่ห่างออกไป %.1f กม." - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "เปิดให้บริการพรุ่งนี้เวลา %s" - "เปิดให้บริการวันนี้เวลา %s" - "ปิดให้บริการเวลา %s" - "ปิดให้บริการแล้ววันนี้เวลา %s" - "ขณะนี้เปิดทำการ" - "ขณะนี้ปิดทำการ" - "ผู้โทรที่สงสัยว่าเป็นสแปม" - "วางสายแล้ว %1$s" - "หมายเลขนี้โทรหาคุณเป็นครั้งแรก" - "เราสงสัยว่าสายนี้จะเป็นนักส่งสแปม" - "บล็อก/รายงานสแปม" - "เพิ่มผู้ติดต่อ" - "ไม่ใช่สแปม" - diff --git a/InCallUI/res/values-tl/strings.xml b/InCallUI/res/values-tl/strings.xml deleted file mode 100644 index b5658d705..000000000 --- a/InCallUI/res/values-tl/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telepono" - "Naka-hold" - "Hindi alam" - "Pribadong numero" - "Payphone" - "Conference call" - "Naputol ang tawag" - "Speaker" - "Handset earpiece" - "Wired na headset" - "Bluetooth" - "Ipapadala ba ang mga sumusunod na tono?\n" - "Nagpapadala ng mga tono\n" - "Ipadala" - "Oo" - "Hindi" - "Palitan ang wild character ng" - "Conference call %s" - "Numero ng voicemail" - "Dina-dial" - "Muling dina-dial" - "Conference call" - "Papasok na tawag" - "Papasok na tawag sa trabaho" - "Ibinaba ang tawag" - "Naka-hold" - "Binababa" - "Nasa tawag" - "Ang aking numero ay %s" - "Ikinokonekta ang video" - "Mag-video call" - "Humihiling ng video" - "Hindi makakonekta sa video call" - "Tinanggihan ang kahilingan sa video" - "Ang iyong numero ng callback\n %1$s" - "Ang iyong emergency na numero ng callback\n %1$s" - "Dina-dial" - "Hindi nasagot na tawag" - "Mga hindi nasagot na tawag" - "%s (na) hindi nasagot na tawag" - "Hindi nasagot ang tawag mula kay %s" - "Kasalukuyang tawag" - "Kasalukuyang tawag sa trabaho" - "Kasalukuyang tawag sa Wi-Fi" - "Kasalukuyang tawag sa trabaho sa pamamagitan ng Wi-Fi" - "Naka-hold" - "Papasok na tawag" - "Papasok na tawag sa trabaho" - "Papasok na tawag sa Wi-Fi" - "Papasok na tawag sa trabaho sa pamamagitan ng Wi-Fi" - "Papasok na video call" - "Papasok na pinaghihinalaang spam na tawag" - "Papasok na kahilingan ng video" - "Bagong voicemail" - "Bagong voicemail (%d)" - "I-dial ang %s" - "Hindi kilala ang numero ng voicemail" - "Walang serbisyo" - "Hindi available ang piniling network (%s)" - "Sagutin" - "Ibaba" - "Video" - "Boses" - "Tanggapin" - "I-dismiss" - "Tawagan" - "Mensahe" - "Kasalukuyang tawag sa isa pang device" - "Ilipat ang Tawag" - "Upang tumawag, paki-off muna ang Airplane mode." - "Hindi nakarehistro sa network." - "Hindi available ang cellular network." - "Upang tumawag, maglagay ng wastong numero." - "Hindi makatawag." - "Sinisimulan ang pagkakasunud-sunod ng MMI…" - "Hindi sinusuportahan ang serbisyo." - "Hindi mailipat ang mga tawag." - "Hindi mapaghiwalay ang tawag." - "Hindi mailipat." - "Hindi makapag-conference." - "Hindi matanggihan ang tawag." - "Hindi mailabas ang (mga) tawag." - "Tawag sa SIP" - "Emergency na tawag" - "Ino-on ang radyo…" - "Walang serbisyo. Sinusubukang muli…" - "Hindi makatawag. Ang %s ay hindi isang pang-emergency na numero." - "Hindi makatawag. Mag-dial ng pang-emergency na numero." - "Gamitin ang keyboard upang mag-dial" - "I-hold ang Tawag" - "Ituloy ang Tawag" - "Ibaba ang Tawag" - "Ipakita ang Dialpad" - "Itago ang Dialpad" - "I-mute" - "Alisin sa pagkaka-mute" - "Magdagdag ng tawag" - "Pagsamahin ang mga tawag" - "Pagpalitin" - "Pamahalaan ang mga tawag" - "Pamahalaan ang conference call" - "Conference call" - "Pamahalaan" - "Audio" - "Mag-video call" - "Gawing voice call" - "Lumipat ng camera" - "I-on ang camera" - "I-off ang camera" - "Higit pang mga opsyon" - "Nagsimula na ang Player" - "Huminto ang Player" - "Hindi pa handa ang camera" - "Handa na ang camera" - "Hindi alam na kaganapan ng session ng tawag" - "Serbisyo" - "I-setup" - "<Hindi nakatakda>" - "Iba pang mga setting ng tawag" - "Tumatawag sa pamamagitan ng %s" - "Papasok sa pamamagitan ng %s" - "larawan ng contact" - "maging pribado" - "pumili ng contact" - "Sumulat ng sarili mong tugon…" - "Kanselahin" - "Ipadala" - "Sagutin" - "Magpadala ng SMS" - "Tanggihan" - "Sagutin bilang video call" - "Sagutin bilang audio call" - "Tanggapin ang kahilingan sa video" - "Tanggihan ang kahilingan sa video" - "Tanggapin ang kahilingan sa pagpapadala ng video" - "Tanggihan ang kahilingan sa pagpapadala ng video" - "Tanggapin ang kahilingan sa pagtanggap ng video" - "Tanggihan ang kahilingan sa pagtanggap ng video" - "Mag-slide pataas para sa %s." - "Mag-slide pakaliwa para sa %s." - "Mag-slide pakanan para sa %s." - "Mag-slide pababa para sa %s." - "Mag-vibrate" - "Mag-vibrate" - "Tunog" - "Default na tunog (%1$s)" - "Ringtone ng telepono" - "Mag-vibrate kapag nagri-ring" - "Ringtone at Pag-vibrate" - "Pamahalaan ang conference call" - "Pang-emergency na numero" - "Larawan sa profile" - "Naka-off ang camera" - "sa pamamagitan ng %s" - "Naipadala ang tala" - "Mga kamakailang mensahe" - "Impormasyon ng negosyo" - "%.1f (na) milya ang layo" - "%.1f (na) kilometro ang layo" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "Magbubukas bukas nang %s" - "Magbubukas ngayon nang %s" - "Magsasara nang %s" - "Sarado ngayon nang %s" - "Bukas ngayon" - "Sarado ngayon" - "Hinihinalang spam na tumatawag" - "Natapos ang tawag %1$s" - "Ito ang unang beses na tinawagan ka ng numerong ito." - "Pinaghihinalaan naming isang spammer ang tawag na ito." - "I-block/iulat na spam" - "Magdagdag ng contact" - "Hindi spam" - diff --git a/InCallUI/res/values-tr/strings.xml b/InCallUI/res/values-tr/strings.xml deleted file mode 100644 index 405bab8db..000000000 --- a/InCallUI/res/values-tr/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefon" - "Beklemede" - "Bilinmiyor" - "Gizli numara" - "Ankesörlü telefon" - "Konferans görüşmesi" - "Çağrı kesildi" - "Hoparlör" - "Mobil cihaz kulaklığı" - "Kablolu kulaklık" - "Bluetooth" - "Aşağıdaki zil sesleri gönderilsin mi?\n" - "Numara tonları gönderiliyor\n" - "Gönder" - "Evet" - "Hayır" - "Joker karakteri şununla değiştir:" - "Konferans görüşmesi %s" - "Sesli mesaj numarası" - "Numara çevriliyor" - "Yeniden çevriliyor" - "Konferans görüşmesi" - "Gelen çağrı" - "İşle ilgili gelen çağrı" - "Çağrı sonlandırıldı" - "Beklemede" - "Sonlandırılıyor" - "Görüşmede" - "Numaram: %s" - "Video bağlanıyor" - "Video görüşmesi" - "Video isteniyor" - "Video görüşmesi bağlantısı yapılamıyor" - "Video isteği reddedildi" - "Geri aranacağınız numara\n %1$s" - "Acil durumda geri aranacağınız numara\n %1$s" - "Numara çevriliyor" - "Cevapsız çağrı" - "Cevapsız çağrılar" - "%s cevapsız çağrı" - "Cevapsız çağrı: %s" - "Devam eden çağrı" - "İşle ilgili devam eden çağrı" - "Devam eden kablosuz çağrı" - "İşle ilgili devam eden kablosuz çağrı" - "Beklemede" - "Gelen çağrı" - "İşle ilgili gelen çağrı" - "Gelen kablosuz çağrı" - "İşle ilgili gelen kablosuz çağrı" - "Gelen video görüşmesi isteği" - "Spam olabilecek gelen arama" - "Gelen video isteği" - "Yeni sesli mesaj" - "Yeni sesli mesaj (%d)" - "Çevir: %s" - "Sesli mesaj numarası bilinmiyor" - "Servis yok" - "Seçili ağ (%s) kullanılamıyor" - "Yanıtla" - "Kapat" - "Video" - "Ses" - "Kabul et" - "Yok say" - "Geri ara" - "İleti" - "Başka bir cihazda devam eden çağrı" - "Çağrıyı Aktar" - "Çağrı yapmak için öncelikle Uçak modunu kapatın." - "Ağda kayıtlı değil." - "Hücresel ağ kullanılamıyor." - "Çağrı yapmak için geçerli bir numara girin." - "Çağrı yapılamıyor." - "MMI dizisi başlatılıyor…" - "Service desteklenmiyor" - "Çağrı geçişi yapılamıyor." - "Çağrı ayrılamıyor." - "Aktarılamıyor." - "Konferans çağrısı yapılamıyor." - "Çağrı reddedilemiyor." - "Çağrılar bırakılamıyor." - "SIP çağrısı" - "Acil durum çağrısı" - "Radyo açılıyor…" - "Servis yok. Tekrar deneniyor…" - "Çağrı yapılamıyor. %s bir acil durum numarası değil." - "Çağrı yapılamıyor. Acil durum numarasını çevirin." - "Çevirmek için klavyeyi kullan" - "Çağrıyı Beklet" - "Çağrıyı Devam Ettir" - "Çağrıyı Sonlandır" - "Tuş Takımını Göster" - "Tuş Takımını Gizle" - "Sesi kapat" - "Sesi aç" - "Çağrı ekle" - "Çağrıları birleştir" - "Değiştir" - "Çağrıları yönet" - "Konferans çağrısını yönet" - "Konferans çağrısı" - "Yönet" - "Ses" - "Vid. görşm" - "Sesli çağrıya geç" - "Kamerayı değiştir" - "Kamerayı aç" - "Kamerayı kapat" - "Diğer seçenekler" - "Oynatıcı Başlatıldı" - "Oynatıcı Durduruldu" - "Kamera hazır değil" - "Kamera hazır" - "Bilinmeyen çağrı oturumu etkinliği" - "Hizmet" - "Kurulum" - "<Ayarlanmadı>" - "Diğer çağrı ayarları" - "%s üzerinden çağrı yapılıyor" - "%s adlı sağlayıcı üzerinden gelen çağrı" - "kişi fotoğrafı" - "özel görüşmeye geç" - "kişi seçin" - "Kendi yanıtınızı oluşturun…" - "İptal" - "Gönder" - "Yanıtla" - "SMS gönder" - "Reddet" - "Video görüşmesi olarak yanıtla" - "Sesli görüşme olarak yanıtla" - "Video isteğini kabul et" - "Video isteğini reddet" - "Video aktarma isteğini kabul et" - "Video aktarma isteğini reddet" - "Video alma isteğini kabul et" - "Video alma isteğini reddet" - "%s için yukarı kaydırın." - "%s için sola kaydırın." - "%s için sağa kaydırın." - "%s için aşağı kaydırın." - "Titreşim" - "Titreşim" - "Ses" - "Varsayılan ses (%1$s)" - "Telefon zil sesi" - "Çalarken titret" - "Zil Sesi ve Titreşim" - "Konferans çağrısını yönetin" - "Acil durum numarası" - "Profil fotoğrafı" - "Kamera kapalı" - "%s üzerinden" - "Not gönderildi" - "Son iletiler" - "İşletme bilgileri" - "%.1f mil uzakta" - "%.1f km uzakta" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "Yarın açılış saati: %s" - "Bugün açılış saati: %s" - "Kapanış saati: %s" - "Bugün kapanış saati: %s" - "Şu an açık" - "Şu an kapalı" - "Spam olabilck arayan" - "Arama sona erdi %1$s" - "Bu, bu numaradan ilk aranışınız." - "Bu aramanın spam olduğundan şüpheleniliyor." - "Engelle/spam bildir" - "Kişi ekle" - "Spam değil" - diff --git a/InCallUI/res/values-uk/strings.xml b/InCallUI/res/values-uk/strings.xml deleted file mode 100644 index b323acd04..000000000 --- a/InCallUI/res/values-uk/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Номер телефону" - "Очікування" - "Невідомо" - "Приватний номер" - "Таксофон" - "Конференц-зв’язок" - "Виклик перервано" - "Динамік" - "Динамік гарнітури" - "Дротова гарнітура" - "Bluetooth" - "Надіслати вказані нижче сигнали?\n" - "Надсилання сигналів\n" - "Надіслати" - "Так" - "Ні" - "Замінити довільний символ на" - "Конференц-зв’язок %s" - "Номер голосової пошти" - "Набір номера" - "Повторний набір" - "Конференц-зв’язок" - "Вхідний виклик" - "Вхідний робочий виклик" - "Виклик завершено" - "Очікування" - "Завершення виклику" - "Триває виклик" - "Мій номер: %s" - "Відеодзвінок: з’єднання" - "Відеодзвінок" - "Надсилання запиту на відеодзвінок" - "Не вдалося здійснити відеодзвінок" - "Запрошення на відеодзвінок відхилено" - "Номер для зв’язку:\n%1$s" - "Екстрений номер:\n%1$s" - "Набір номера" - "Пропущений виклик" - "Пропущені виклики" - "Пропущено викликів: %s" - "Пропущений виклик: %s" - "Поточний виклик" - "Поточний виклик на робочий телефон" - "Поточний виклик через Wi-Fi" - "Поточний виклик на робочий телефон через Wi-Fi" - "Очікування" - "Вхідний виклик" - "Вхідний виклик на робочий телефон" - "Вхідний виклик через Wi-Fi" - "Вхідний виклик на робочий телефон через Wi-Fi" - "Вхідний відеодзвінок" - "Цей дзвінок може бути спамом" - "Запит на вхідний відеодзвінок" - "Нові голосові повідомлення" - "Нові голосові повідомлення (%d)" - "Набрати %s" - "Невідомий номер голосової пошти" - "Немає зв’язку" - "Вибрана мережа (%s) недоступна" - "Відповісти" - "Завершити" - "Відео" - "Гол. виклик" - "Прийняти" - "Відхилити" - "Передзвонити" - "Написати SMS" - "Поточний виклик на іншому пристрої" - "Передати виклик" - "Щоб зателефонувати, вимкніть режим польоту." - "Не зареєстровано в мережі." - "Мобільна мережа недоступна." - "Щоб зателефонувати, введіть дійсний номер." - "Не вдається зателефонувати." - "Запуск ряду MMI…" - "Служба не підтримується." - "Неможливо переключитися між викликами." - "Неможливо розділити виклик." - "Неможливо перенести." - "Конференц-зв’язок недоступний." - "Неможливо відхилити виклик." - "Неможливо телефонувати." - "Виклик через протокол SIP" - "Екстрений виклик" - "Увімкнення радіо…" - "Немає зв’язку. Повторна спроба…" - "Не вдається зателефонувати. %s не є екстреним номером." - "Не вдається зателефонувати. Наберіть екстрений номер." - "Використовуйте для набору клавіатуру" - "Призупинити виклик" - "Відновити виклик" - "Завершити виклик" - "Показати цифрову клавіатуру" - "Сховати цифрову клавіатуру" - "Ігнорувати" - "Не ігнорувати" - "Додати виклик" - "Об’єднати виклики" - "Поміняти виклики" - "Керувати викликами" - "Керувати конференц-зв’язком" - "Конференц-зв’язок" - "Керувати" - "Аудіо" - "Відеодзвінок" - "Перейти в режим голосового виклику" - "Вибрати камеру" - "Увімкнути камеру" - "Вимкнути камеру" - "Інші опції" - "Програвач запущено" - "Програвач зупинено" - "Камера неготова" - "Камера готова" - "Невідомий сеанс виклику" - "Служба" - "Налаштування" - "<Не налаштовано>" - "Інші налаштування виклику" - "Виклик здійснюється через оператора %s" - "Вхідні виклики через оператора %s" - "фото контакта" - "приватна розмова" - "вибрати контакт" - "Напишіть власну відповідь…" - "Скасувати" - "Надіслати" - "Відповісти" - "Надіслати SMS" - "Відхилити" - "Відповісти в режимі відеодзвінка" - "Відповісти в режимі аудіодзвінка" - "Прийняти запит на відео" - "Відхилити запит на відео" - "Прийняти запит на передавання відео" - "Відхилити запит на передавання відео" - "Прийняти запит на отримання відео" - "Відхилити запит на отримання відео" - "Проведіть пальцем угору, щоб %s." - "Проведіть пальцем ліворуч, щоб %s." - "Проведіть пальцем праворуч, щоб %s." - "Проведіть пальцем донизу, щоб %s." - "Вібросигнал" - "Вібросигнал" - "Звук" - "Звук за умовчанням (%1$s)" - "Сигнал дзвінка телефона" - "Вібрувати під час виклику" - "Сигнал дзвінка та вібросигнал" - "Керування конференц-зв’язком" - "Екстрений номер" - "Фотографія профілю" - "Камеру вимкнено" - "на номер %s" - "Нотатку надіслано" - "Нещодавні повідомлення" - "Інформація про компанію" - "За %.1f мил." - "За %.1f км" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "Відчиняється завтра о %s" - "Відчиняється сьогодні о %s" - "Зачиняється о %s" - "Зачинено сьогодні о %s" - "Відчинено" - "Зачинено" - "Може бути спамом" - "Дзвінок завершено %1$s" - "З цього номера вам телефонують уперше." - "Ми підозрюємо, що телефонував спамер." - "Заблокувати/це спам" - "Додати контакт" - "Не спам" - diff --git a/InCallUI/res/values-ur/strings.xml b/InCallUI/res/values-ur/strings.xml deleted file mode 100644 index 32e0d45aa..000000000 --- a/InCallUI/res/values-ur/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "فون" - "ہولڈ پر ہے" - "نامعلوم" - "نجی نمبر" - "پے فون" - "کانفرنس کال" - "کال ختم ہو گئی" - "اسپیکر" - "ہینڈسیٹ ایئرپیس" - "تار والا ہیڈسیٹ" - "بلوٹوتھ" - "درج ذیل ٹونز بھیجیں؟\n" - "ٹونز بھیج رہا ہے\n" - "بھیجیں" - "ہاں" - "نہیں" - "وائلڈ کریکٹر کو اس کے ساتھ بدلیں" - "کانفرنس کال %s" - "صوتی میل نمبر" - "ڈائل ہو رہا ہے" - "دوبارہ ڈائل ہو رہا ہے" - "کانفرنس کال" - "آنے والی کال" - "کام سے متعلق آنے والی کال" - "کال ختم ہوگئی" - "ہولڈ پر ہے" - "کال منقطع ہو رہی ہے" - "کال میں" - "میرا نمبر ہے %s" - "ویڈیو منسلک ہو رہی ہے" - "ویڈیو کال" - "ویڈیو کی درخواست کی جا رہی ہے" - "ویڈیو کال منسلک نہیں ہو سکتی" - "ویڈیو کی درخواست مسترد ہو گئی" - "‏آپ کا کال بیک نمبر‎\n %1$s" - "‏آپ کا ہنگامی کال بیک نمبر‎\n %1$s" - "ڈائل ہو رہا ہے" - "چھوٹی ہوئی کال" - "چھوٹی ہوئی کالیں" - "%s چھوٹی ہوئی کالیں" - "%s کی جانب سے چھوٹی ہوئی کال" - "جاری کال" - "کام سے متعلق جاری کال" - "‏Wi-Fi کال جاری ہے" - "‏کام سے متعلق جاری Wi-Fi کال" - "ہولڈ پر ہے" - "آنے والی کال" - "کام سے متعلق آنے والی کال" - "‏آنے والی Wi-Fi کال" - "‏کام سے متعلق آنے والی Wi-Fi کال" - "آنے والی ویڈیو کال" - "آنے والی مشتبہ سپام کال" - "آنے والی ویڈیو کی درخواست" - "نیا صوتی میل" - "نیا صوتی میل (%d)" - "%s ڈائل کریں" - "صوتی میل نمبر نامعلوم ہے" - "کوئی سروس نہیں ہے" - "منتخب کردہ نیٹ ورک (%s) دستیاب نہیں ہے" - "جواب" - "کال منقطع کریں" - "ویڈیو" - "آواز" - "قبول کریں" - "برخاست کریں" - "واپس کال کریں" - "پیغام" - "ایک اور آلے پر جاری کال" - "کال منتقل کریں" - "کال کرنے کیلئے، پہلے ہوائی جہاز طرز کو آف کریں۔" - "نیٹ ورک پر رجسٹرڈ نہیں ہے۔" - "سیلولر نیٹ ورک دستیاب نہیں ہے۔" - "کال کرنے کیلئے، ایک درست نمبر درج کریں۔" - "کال نہیں ہو سکتی۔" - "‏MMI ترتیب شروع ہو رہی ہے…" - "سروس تعاون یافتہ نہیں ہے۔" - "کالز سوئچ نہیں ہو سکتیں۔" - "کال الگ نہیں ہو سکتی۔" - "منتقل نہیں ہو سکتی۔" - "کانفرنس نہیں ہو سکتی۔" - "کال مسترد نہیں ہو سکتی۔" - "کال(ز) ریلیز نہیں ہو سکتیں۔" - "‏SIP کال" - "ہنگامی کال" - "ریڈیو آن ہو رہا ہے…" - "کوئی سروس نہیں ہے۔ دوبارہ کوشش کی جا رہی ہے…" - "کال نہیں کی جا سکتی۔ %s ایک ہنگامی نمبر نہیں ہے۔" - "کال نہیں کی جا سکتی۔ ایک ہنگامی نمبر ڈائل کریں۔" - "ڈائل کرنے کیلئے کی بورڈ استعمال کریں" - "کال کو ہولڈ کریں" - "کال کو دوبارہ شروع کریں" - "کال ختم کریں" - "ڈائل پیڈ دکھائیں" - "ڈائل پیڈ چھپائیں" - "خاموش کریں" - "آواز چالو کریں" - "کال شامل کریں" - "کالز کو ضم کریں" - "تبادلہ کریں" - "کالز کا نظم کریں" - "کانفرنس کال کا نظم کریں" - "کانفرنس کال" - "نظم کریں" - "آڈیو" - "ویڈیو کال" - "صوتی کال میں تبدیل کریں" - "کیمرا سوئچ کریں" - "کیمرا آن کریں" - "کیمرا آف کریں" - "مزید اختیارات" - "پلیئر شروع ہوگیا" - "پلیئر بند ہوگیا" - "کیمرا تیار نہیں ہے" - "کیمرا تیار ہے" - "نامعلوم کال سیشن ایونٹ" - "سروس" - "ترتیب دیں" - "‏‎<سیٹ نہیں ہے>‎" - "کال کی دیگر ترتیبات" - "کالنگ بذریعہ %s" - "%s کے ذریعے آنے والی" - "رابطہ کی تصویر" - "نجی ہوجائیں" - "رابطہ منتخب کریں" - "اپنا ذاتی تحریر کریں…" - "منسوخ کریں" - "بھیجیں" - "جواب دیں" - "‏SMS بھیجیں" - "مسترد کریں" - "ویڈیو کال کے بطور جواب دیں" - "آڈیو کال کے بطور جواب دیں" - "ویڈیو کی درخواست قبول کریں" - "ویڈیو کی درخواست مسترد کریں" - "ویڈیو منتقل کرنے کی درخواست قبول کریں" - "ویڈیو منتقل کرنے کی درخواست مسترد کریں" - "ویڈیو موصول کرنے کی درخواست قبول کریں" - "ویڈیو موصول کرنے کی درخواست مسترد کریں" - "%s کیلئے اوپر سلائیڈ کریں۔" - "%s کیلئے بائیں سلائیڈ کریں۔" - "%s کیلئے دائیں سلائیڈ کریں۔" - "%s کیلئے نیچے سلائیڈ کریں۔" - "ارتعاش" - "ارتعاش" - "آواز" - "ڈیفالٹ آواز (%1$s)" - "فون رِنگ ٹون" - "رِنگ کے وقت مرتعش کریں" - "رنگ ٹون اور ارتعاش" - "کانفرنس کال کا نظم کریں" - "ہنگامی نمبر" - "پروفائل کی تصویر" - "کیمرا آف ہے" - "بذریعہ %s" - "نوٹ بھیج دیا گیا" - "حالیہ پیغامات" - "کاروباری معلومات" - "%.1f میل دور" - "%.1f کلومیٹر دور" - "%1$s، %2$s" - "%1$s - %2$s" - "%1$s، %2$s" - "کل %s بجے کھلے گا" - "آج %s بجے کھلے گا" - "%s بجے بند ہوگا" - "آج %s بجے بند ہوا" - "ابھی کھلا ہے" - "اب بند ہے" - "مشتبہ سپام کالر" - "‏کال ختم ہو گئی ‎%1$s" - "اس نمبر سے پہلی بار آپ کو کال آئی ہے۔" - "ہمیں شک ہے کہ یہ کال سپامر تھی۔" - "مسدود کریں/سپام کی اطلاع دیں" - "رابطہ شامل کریں" - "سپام نہیں ہے" - diff --git a/InCallUI/res/values-uz/strings.xml b/InCallUI/res/values-uz/strings.xml deleted file mode 100644 index 15d534d25..000000000 --- a/InCallUI/res/values-uz/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Telefon" - "Kutilmoqda" - "Noma’lum" - "Maxfiy raqam" - "Taksofon" - "Konferens-aloqa" - "Chaqiruv uzilib qoldi" - "Karnay" - "Telefon quloqchini" - "Simli garnitura" - "Bluetooth" - "Quyidagi tovush signallari yuborilsinmi?\n" - "Tovush signallari yuborilmoqda\n" - "Yuborish" - "Ha" - "Yo‘q" - "Universal belgini bunga almashtirish" - "Konferens-aloqa: %s" - "Ovozli pochta raqami" - "Chaqiruv" - "Qayta terilmoqda" - "Konferens-aloqa" - "Kiruvchi qo‘ng‘iroq" - "Kiruvchi qo‘ng‘iroq (ish)" - "Chaqiruv tugadi" - "Kutilmoqda" - "Suhbat tugatilmoqda" - "Suhbat" - "Mening raqamim – %s" - "Videoga ulanmoqda" - "Video qo‘ng‘iroq" - "Video so‘ralmoqda" - "Video qo‘ng‘iroqqa ulanib bo‘lmadi" - "Video qo‘ng‘iroq so‘rovi rad etildi" - "Teskari qo‘ng‘iroq raqamingiz\n %1$s" - "Favqulodda holatlar uchun teskari qo‘ng‘iroq raqamingiz\n %1$s" - "Chaqiruv" - "Javobsiz qo‘ng‘iroq" - "Javobsiz qo‘ng‘iroqlar" - "%s ta javobsiz qo‘ng‘iroq" - "%s qo‘ng‘irog‘i javobsiz qoldirildi" - "Joriy qo‘ng‘iroq" - "Joriy qo‘ng‘iroq (ish)" - "Joriy Wi-Fi qo‘ng‘iroq" - "Joriy Wi-Fi qo‘ng‘iroq (ish)" - "Kutilmoqda" - "Kiruvchi qo‘ng‘iroq" - "Kiruvchi qo‘ng‘iroq (ish)" - "Kiruvchi Wi-Fi qo‘ng‘iroq" - "Kiruvchi Wi-Fi qo‘ng‘iroq (ish)" - "Kiruvchi video qo‘ng‘iroq" - "Shubhali kiruvchi qo‘ng‘iroq" - "Kiruvchi video qo‘ng‘iroq" - "Yangi ovozli xabar" - "Yangi ovozli xabar (%d)" - "%s raqamini terish" - "Ovozli pochta raqami noma’lum" - "Xizmat mavjud emas" - "Tanlangan tarmoq (%s) mavjud emas" - "Javob berish" - "Tugatish" - "Video aloqa" - "Ovozli aloqa" - "Qabul qilish" - "Rad etish" - "Telefon qilish" - "SMS yuborish" - "Boshqa qurilmada hozir qo‘ng‘iroq amalga oshirilmoqda." - "Qo‘ng‘iroqni o‘tkazish" - "Parvoz rejimini o‘chirib qo‘ying." - "Tarmoqda ro‘yxatdan o‘tmagan." - "Uyali tarmoq mavjud emas." - "Raqam noto‘g‘ri." - "Qo‘ng‘iroq qilib bo‘lmadi." - "MMI tartibi ishga tushmoqda…" - "Xizmat qo‘llab-quvvatlanmaydi." - "Qo‘ng‘iroqlarni almashtirib bo‘lmadi." - "Qo‘ng‘iroqni ajratib bo‘lmadi." - "O‘tkazib bo‘lmadi." - "Konferens-aloqa o‘rnatib bo‘lmadi." - "Qo‘ng‘iroqni rad qilib bo‘lmadi." - "Qo‘ng‘iroq(lar)ni chiqarib bo‘lmadi." - "SIP qo‘ng‘iroq" - "Favqulodda chaqiruv" - "Radio yoqilmoqda…" - "Aloqa yo‘q. Qayta urinilmoqda…" - "Qo‘ng‘iroq qilib bo‘lmadi. %s favqulodda raqam emas." - "Qo‘ng‘iroq qilib bo‘lmadi. Favqulodda raqamga tering." - "Terish uchun klaviaturadan foydalaning" - "Qo‘ng‘iroqni ushlab turish" - "Qo‘ng‘iroqni davom ettirish" - "Chaqiruvni tugatish" - "Raqam terish panelini ochish" - "Raqam terish panelini yopish" - "Ovozni o‘chirish" - "Ovozni yoqish" - "Chaqiruv qo‘shish" - "Qo‘ng‘iroqlarni birlashtirish" - "Almashtirish" - "Qo‘ng‘iroqlarni boshqarish" - "Konferens-aloqani sozlash" - "Konferens-aloqa" - "Boshqarish" - "Audio" - "Video qo‘ng‘iroq" - "Ovozli qo‘ng‘iroqqa o‘zgartirish" - "Kamerani almashtirish" - "Kamerani yoqish" - "Kamerani o‘chirish" - "Boshqa sozlamalar" - "Pleyer ishga tushirildi" - "Pleyer to‘xtatildi" - "Kamera tayyor emas" - "Kamera tayyor" - "Aloqa seansining noma’lum hodisasi" - "Xizmat" - "Sozlash" - "<Ko‘rsatilmagan>" - "Boshqa qo‘ng‘iroq sozlamalari" - "%s orqali qo‘ng‘rioq qilinmoqda" - "%s orqali kiruvchi qo‘ng‘iroqlar" - "kontakt rasmi" - "alohida suhbatga o‘tish" - "kontaktni tanlash" - "O‘z javobingizni yozing…" - "Bekor qilish" - "Yuborish" - "Javob berish" - "SMS yuborish" - "Rad etish" - "Video qo‘ng‘iroqqa javob berish" - "Ovozli qo‘ng‘iroqqa javob berish" - "Video qo‘ng‘iroq so‘rovini qabul qilish" - "Video qo‘ng‘iroq so‘rovini rad etish" - "Video uzatishga ruxsat berish" - "Video uzatishga ruxsat bermaslik" - "Kiruvchi video qo‘ng‘iroqni qabul qilish" - "Kiruvchi video qo‘ng‘iroqni rad etish" - "%s uchun tepaga suring." - "%s uchun chapga suring." - "%s uchun o‘ngga suring." - "%s uchun pastga suring." - "Tebranish" - "Tebranish" - "Ovoz" - "Standart ovoz (%1$s)" - "Telefon ringtoni" - "Jiringlash vaqtida tebranish" - "Qo‘ng‘iroq ohangi va tebranish" - "Konferens-aloqani sozlash" - "Favqulodda qo‘ng‘iroq raqami" - "Profil rasmi" - "Kamera o‘chiq" - "%s orqali" - "Xabar yuborildi" - "So‘nggi xabarlar" - "Kompaniya haqida ma’lumot" - "%.1f mil masofada" - "%.1f km masofada" - "%1$s, %2$s" - "%1$s%2$s" - "%1$s, %2$s" - "Ertaga %s da ochiladi" - "Bugun %s da ochiladi" - "%s da yopiladi" - "Bugun %s da yopiladi" - "Ochiq" - "Yopiq" - "Shubhali abonent" - "Qo‘ng‘iroq yakunlandi (%1$s)" - "Sizga bu raqamdan birinchi marta qo‘ng‘iroq qilishdi." - "Bu spam-qo‘ng‘iroqqa o‘xshayapti." - "Bloklash/spamga" - "Kontaktni qo‘shish" - "Spam emas" - diff --git a/InCallUI/res/values-vi/strings.xml b/InCallUI/res/values-vi/strings.xml deleted file mode 100644 index 58a50c278..000000000 --- a/InCallUI/res/values-vi/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Điện thoại" - "Đang chờ" - "Không xác định" - "Số cá nhân" - "Điện thoại trả tiền" - "Cuộc gọi nhiều bên" - "Cuộc gọi bị gián đoạn" - "Loa" - "Tai nghe ĐTDĐ" - "Tai nghe có dây" - "Bluetooth" - "Gửi các âm sau?\n" - "Đang gửi âm\n" - "Gửi" - "Có" - "Không" - "Thay thế ký tự tự do bằng" - "Cuộc gọi nhiều bên %s" - "Số thư thoại" - "Đang gọi" - "Đang quay số lại" - "Cuộc gọi nhiều bên" - "Cuộc gọi đến" - "Cuộc gọi đến về công việc" - "Cuộc gọi đã kết thúc" - "Đang chờ" - "Kết thúc cuộc gọi" - "Trong cuộc gọi" - "Số điện thoại của tôi là %s" - "Đang kết nối video" - "Cuộc gọi điện video" - "Đang yêu cầu video" - "Không kết nối được cuộc gọi điện video" - "Đã từ chối yêu cầu video" - "Số gọi lại của bạn\n %1$s" - "Số gọi lại khẩn cấp của bạn\n %1$s" - "Đang gọi" - "Cuộc gọi nhỡ" - "Cuộc gọi nhỡ" - "%s cuộc gọi nhỡ" - "Cuộc gọi nhỡ từ %s" - "Cuộc gọi đang thực hiện" - "Cuộc gọi đang diễn ra về công việc" - "Cuộc gọi đang diễn ra qua Wi-Fi" - "Cuộc gọi đang diễn ra qua Wi-Fi về công việc" - "Đang chờ" - "Cuộc gọi đến" - "Cuộc gọi đến về công việc" - "Cuộc gọi đến qua Wi-Fi" - "Cuộc gọi đến qua Wi-Fi về công việc" - "Cuộc gọi điện video đến" - "Cuộc gọi spam đến bị nghi ngờ" - "Yêu cầu video đến" - "Thư thoại mới" - "Thư thoại mới (%d)" - "Quay số %s" - "Số thư thoại không xác định" - "Không có dịch vụ" - "Mạng được chọn (%s) không khả dụng" - "Trả lời" - "Gác máy" - "Video" - "Thoại" - "Chấp nhận" - "Loại bỏ" - "Gọi lại" - "Tin nhắn" - "Cuộc gọi đang diễn ra trên một thiết bị khác" - "Chuyển cuộc gọi" - "Để thực hiện cuộc gọi, trước tiên, hãy tắt chế độ trên Máy bay." - "Chưa được đăng ký trên mạng." - "Không có mạng di động." - "Để thực hiện cuộc gọi, hãy nhập một số hợp lệ." - "Không thực hiện được cuộc gọi." - "Đang khởi động chuỗi MMI…" - "Dịch vụ không được hỗ trợ." - "Không chuyển đổi được cuộc gọi." - "Không tách được cuộc gọi." - "Không chuyển được cuộc gọi." - "Không thực hiện được cuộc gọi nhiều bên." - "Không từ chối được cuộc gọi." - "Không thực hiện được cuộc gọi." - "Cuộc gọi qua SIP" - "Cuộc gọi khẩn cấp" - "Đang bật radio..." - "Không có dịch vụ nào. Đang thử lại…" - "Không thực hiện được cuộc gọi. %s không phải là số khẩn cấp." - "Không thực hiện được cuộc gọi. Hãy quay số khẩn cấp." - "Sử dụng bàn phím để quay số" - "Giữ cuộc gọi" - "Tiếp tục cuộc gọi" - "Kết thúc cuộc gọi" - "Hiển thị bàn phím số" - "Ẩn bàn phím số" - "Tắt tiếng" - "Bật tiếng" - "Thêm cuộc gọi" - "Hợp nhất cuộc gọi" - "Hoán đổi" - "Quản lý cuộc gọi" - "Quản lý cuộc gọi nhiều bên" - "Cuộc gọi nhiều bên" - "Quản lý" - "Âm thanh" - "Cuộc gọi điện video" - "Thay đổi thành cuộc gọi thoại" - "Chuyển máy ảnh" - "Bật máy ảnh" - "Tắt máy ảnh" - "Tùy chọn khác" - "Đã khởi động trình phát" - "Đã dừng trình phát" - "Máy ảnh chưa sẵn sàng" - "Máy ảnh đã sẵn sàng" - "Sự kiện phiên cuộc gọi không xác định" - "Dịch vụ" - "Thiết lập" - "<Chưa được đặt>" - "Cài đặt cuộc gọi khác" - "Gọi điện qua %s" - "Cuộc gọi đến qua %s" - "ảnh liên hệ" - "chuyển thành riêng tư" - "chọn địa chỉ liên hệ" - "Viết trả lời của riêng bạn..." - "Hủy" - "Gửi" - "Trả lời" - "Gửi SMS" - "Từ chối" - "Trả lời là cuộc gọi điện video" - "Trả lời là cuộc gọi âm thanh" - "Chấp nhận yêu cầu cuộc gọi video" - "Từ chối yêu cầu cuộc gọi video" - "Chấp nhận yêu cầu truyền video" - "Từ chối yêu cầu truyền video" - "Chấp nhận yêu cầu nhận video" - "Từ chối yêu cầu nhận video" - "Trượt lên để %s." - "Trượt sang trái để %s." - "Trượt sang phải để %s." - "Trượt xuống để %s." - "Rung" - "Rung" - "Âm thanh" - "Âm thanh mặc định (%1$s)" - "Nhạc chuông điện thoại" - "Rung khi đổ chuông" - "Nhạc chuông và rung" - "Quản lý cuộc gọi nhiều bên" - "Số khẩn cấp" - "Ảnh hồ sơ" - "Tắt máy ảnh" - "qua %s" - "Đã gửi ghi chú" - "Tin nhắn gần đây" - "Thông tin doanh nghiệp" - "Cách %.1f dặm" - "Cách %.1f km" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "Mở cửa lúc %s ngày mai" - "Mở cửa lúc %s hôm nay" - "Đóng cửa lúc %s" - "Đã đóng cửa lúc %s hôm nay" - "Mở ngay bây giờ" - "Hiện đã đóng cửa" - "Người gọi spam bị nghi ngờ" - "Đã kết thúc cuộc gọi %1$s" - "Đây là lần đầu tiên số điện thoại này gọi điện cho bạn." - "Chúng tôi đã nghi ngờ cuộc gọi này là người gửi spam." - "Chặn/báo cáo spam" - "Thêm liên hệ" - "Không phải là spam" - diff --git a/InCallUI/res/values-w500dp-land/colors.xml b/InCallUI/res/values-w500dp-land/colors.xml deleted file mode 100644 index 77eea2e68..000000000 --- a/InCallUI/res/values-w500dp-land/colors.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - #000000 - diff --git a/InCallUI/res/values-w500dp-land/dimens.xml b/InCallUI/res/values-w500dp-land/dimens.xml deleted file mode 100644 index 112ec5f09..000000000 --- a/InCallUI/res/values-w500dp-land/dimens.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - true - - - true - - - 40dp - - - 30dp - - 2dp - - 20dp - diff --git a/InCallUI/res/values-zh-rCN/strings.xml b/InCallUI/res/values-zh-rCN/strings.xml deleted file mode 100644 index f9c43764d..000000000 --- a/InCallUI/res/values-zh-rCN/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "电话" - "保持" - "未知" - "私密号码" - "公用电话" - "电话会议" - "通话中断" - "扬声器" - "手机听筒" - "有线耳机" - "蓝牙" - "发送以下信号音?\n" - "正在发送信号音\n" - "发送" - "是" - "否" - "将通配符替换为" - "电话会议(%s)" - "语音信箱号码" - "正在拨号" - "正在重拨" - "电话会议" - "来电" - "工作来电" - "通话已结束" - "保持" - "正在挂断" - "正在通话" - "我的电话号码:%s" - "正在接通视频" - "视频通话" - "正在发出视频请求" - "无法接通视频通话" - "视频请求遭拒" - "您的回拨号码如下:\n%1$s" - "您的紧急回拨号码如下:\n%1$s" - "正在拨号" - "未接电话" - "未接电话" - "%s 个未接电话" - "来自%s的未接电话" - "通话进行中" - "工作通话进行中" - "WLAN 通话进行中" - "WLAN 工作通话进行中" - "保持" - "来电" - "工作来电" - "WLAN 来电" - "WLAN 工作来电" - "视频通话来电" - "有疑似骚扰来电" - "收到视频通话请求" - "新语音邮件" - "新语音邮件 (%d)" - "拨打 %s" - "语音信箱号码未知" - "没有服务" - "所选网络(%s)不可用" - "接听" - "挂断" - "视频" - "语音" - "接受" - "拒绝" - "回电" - "发短信" - "其他设备上有正在进行的通话" - "转接通话" - "要拨打电话,请先关闭飞行模式。" - "尚未注册网络。" - "无法连接到移动网络。" - "要拨打电话,请输入有效的电话号码。" - "无法拨打该电话。" - "正在启动 MMI 序列…" - "服务不受支持。" - "无法切换通话。" - "无法单独通话。" - "无法转移呼叫。" - "无法进行电话会议。" - "无法拒接来电。" - "无法挂断。" - "SIP 通话" - "紧急呼救" - "正在开启无线装置…" - "找不到服务信号,正在重试…" - "无法拨打该电话。%s 不是紧急呼救号码。" - "无法拨打该电话。请拨打紧急呼救号码。" - "使用键盘拨号" - "保持通话" - "恢复通话" - "结束通话" - "显示拨号键盘" - "隐藏拨号键盘" - "静音" - "取消静音" - "添加通话" - "合并通话" - "切换" - "管理通话" - "管理电话会议" - "电话会议" - "管理" - "音频" - "视频通话" - "改为语音通话" - "切换摄像头" - "开启摄像头" - "关闭摄像头" - "更多选项" - "播放器已启动" - "播放器已停止" - "摄像头尚未准备就绪" - "摄像头已准备就绪" - "未知通话事件" - "服务" - "设置" - "<未设置>" - "其他通话设置" - "正在通过%s进行通话" - "有人通过%s来电" - "联系人照片" - "单独通话" - "选择联系人" - "自行撰写回复…" - "取消" - "发送" - "接听" - "发送短信" - "拒绝" - "以视频通话的形式接听" - "以音频通话的形式接听" - "接受视频请求" - "拒绝视频请求" - "接受视频传输请求" - "拒绝视频传输请求" - "接受视频接收请求" - "拒绝视频接收请求" - "向上滑动即可%s。" - "向左滑动即可%s。" - "向右滑动即可%s。" - "向下滑动即可%s。" - "振动" - "振动" - "提示音" - "默认提示音(%1$s)" - "手机铃声" - "响铃时振动" - "铃声和振动" - "管理电话会议" - "紧急呼救号码" - "个人资料照片" - "摄像头已关闭" - "通过 %s" - "已发送备注" - "最近的信息" - "商家信息" - "%.1f 英里远" - "%.1f 公里远" - "%2$s%1$s" - "%1$s - %2$s" - "%1$s%2$s" - "将于明天%s开始营业" - "将于今天%s开始营业" - "将于%s结束营业" - "已于今天%s结束营业" - "营业中" - "现已结束营业" - "疑似骚扰电话号码" - "通话已结束 %1$s" - "这是此号码的第一次来电。" - "我们怀疑这是骚扰电话。" - "屏蔽/举报骚扰电话号码" - "添加联系人" - "非骚扰电话号码" - diff --git a/InCallUI/res/values-zh-rHK/strings.xml b/InCallUI/res/values-zh-rHK/strings.xml deleted file mode 100644 index bf6f016cb..000000000 --- a/InCallUI/res/values-zh-rHK/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "電話" - "保留通話" - "不明" - "私人號碼" - "公共電話" - "會議通話" - "通話已中斷" - "喇叭" - "免提聽筒" - "有線耳機" - "藍牙" - "要傳送以下訊號音嗎?\n" - "正在傳送訊號音\n" - "傳送" - "是" - "否" - "將萬用字元改為" - "會議通話:%s" - "留言號碼" - "正在撥號" - "正在重撥" - "會議通話" - "來電" - "工作來電" - "通話已結束" - "保留通話" - "正在掛斷電話" - "正在通話" - "我的電話號碼:%s" - "正在建立視像通話連線" - "視像通話" - "正在提出視像通話要求" - "無法建立視像通話連線" - "視像通話要求被拒" - "您的回撥號碼:\n%1$s" - "您的緊急回撥號碼:\n%1$s" - "正在撥號" - "未接來電" - "未接來電" - "%s 個未接來電" - "來自 %s 的未接來電" - "通話中" - "正在進行工作通話" - "正在進行 Wi-Fi 通話" - "正在進行 Wi-Fi 工作通話" - "保留通話" - "來電" - "工作來電" - "Wi-Fi 來電" - "Wi-Fi 工作來電" - "視像通話來電" - "疑似收到垃圾來電" - "收到視像通話要求" - "新留言" - "新留言 (%d 個)" - "撥打 %s" - "留言號碼不明" - "沒有服務" - "您選取的網絡 (%s) 無法使用" - "接聽" - "掛斷電話" - "視像通話" - "語音通話" - "接受" - "拒絕" - "回電" - "短訊" - "其他裝置上有正在進行的通話" - "轉接來電" - "如要撥打電話,請先關閉飛行模式。" - "未在網絡上註冊。" - "無法連線至流動網絡。" - "如要撥打電話,請輸入有效的號碼。" - "無法通話。" - "開始 MMI 序列…" - "不支援此服務。" - "無法切換通話。" - "無法分開通話。" - "無法轉接。" - "無法進行會議通話。" - "無法拒接來電。" - "無法結束通話。" - "SIP 通話" - "緊急電話" - "正在開啟無線電…" - "找不到服務,請再試一次…" - "無法通話。%s 不是緊急電話號碼。" - "無法通話。請撥打緊急電話號碼。" - "使用鍵盤撥號" - "保留通話" - "恢復通話" - "結束通話" - "顯示撥號鍵盤" - "隱藏撥號鍵盤" - "略過" - "取消靜音" - "新增通話" - "合併通話" - "切換" - "管理通話" - "管理會議通話" - "會議通話" - "管理" - "音訊" - "視像通話" - "變更為語音通話" - "切換鏡頭" - "開啟攝影機" - "關閉攝影機" - "更多選項" - "已啟動播放器" - "已停止播放器" - "相機未準備好" - "相機已準備就緒" - "不明的通話工作階段活動" - "服務" - "設定" - "<未設定>" - "其他通話設定" - "正在透過 %s 撥號" - "有人透過 %s 來電" - "聯絡人相片" - "私人通話" - "選取聯絡人" - "自行撰寫回覆..." - "取消" - "傳送" - "接聽" - "傳送短訊" - "拒絕" - "接聽視像通話" - "接聽語音通話" - "接受視像通話要求" - "拒絕視像通話要求" - "接受視像通話轉駁要求" - "拒絕視像通話轉駁要求" - "接受視像接收要求" - "拒絕視像接收要求" - "向上滑動即可%s。" - "向左滑動即可%s。" - "向右滑動即可%s。" - "向下滑動即可%s。" - "震動" - "震動" - "音效" - "預設音效 (%1$s)" - "手機鈴聲" - "響鈴時震動" - "鈴聲和震動" - "管理會議通話" - "緊急電話號碼" - "個人檔案相片" - "相機已關閉" - "透過 %s" - "已傳送筆記" - "最近的訊息" - "公司資料" - "%.1f 英里外" - "%.1f 公里外" - "%2$s%1$s" - "%1$s - %2$s" - "%1$s%2$s" - "將於明天%s開始營業" - "將於今天%s開始營業" - "將於%s關門" - "已於今天%s關門" - "營業中" - "目前休息" - "疑似垃圾來電者" - "通話結束 %1$s" - "這是此號碼的第一次來電。" - "我們懷疑此來電為垃圾來電。" - "封鎖/舉報為垃圾來電" - "新增聯絡人" - "非垃圾來電" - diff --git a/InCallUI/res/values-zh-rTW/strings.xml b/InCallUI/res/values-zh-rTW/strings.xml deleted file mode 100644 index e316c7d40..000000000 --- a/InCallUI/res/values-zh-rTW/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "電話" - "保留" - "不明" - "私人號碼" - "公用電話" - "電話會議" - "通話已中斷" - "喇叭" - "手機聽筒" - "有線耳機" - "藍牙" - "要傳送以下的信號音嗎?\n" - "正在傳送信號音\n" - "傳送" - "是" - "否" - "將萬用字元替換為" - "電話會議 %s" - "語音留言號碼" - "撥號中" - "重撥中" - "電話會議" - "來電" - "公司來電" - "通話已結束" - "保留" - "掛斷中" - "通話中" - "我的電話號碼:%s" - "正在建立視訊通話連線" - "視訊通話" - "正在提出視訊通話要求" - "無法建立視訊通話連線" - "視訊通話要求遭拒" - "您的回撥號碼如下\n %1$s" - "您的緊急回撥號碼如下\n %1$s" - "撥號中" - "未接來電" - "未接來電" - "%s 通未接來電" - "來自 %s 的未接來電" - "進行中的通話" - "進行中的公司通話" - "進行中的通話 (透過 Wi-Fi)" - "進行中的公司通話 (透過 Wi-Fi)" - "保留" - "來電" - "公司來電" - "來電 (透過 Wi-Fi)" - "公司來電 (透過 Wi-Fi)" - "視訊通話來電" - "可疑的騷擾/廣告來電" - "收到視訊通話要求" - "新的語音留言" - "新的語音留言 (%d)" - "撥打 %s" - "語音留言號碼不明" - "沒有服務" - "您所選取的網路 (%s) 無法使用" - "接聽" - "掛斷" - "視訊通話" - "語音通話" - "接受" - "拒絕" - "回撥" - "傳送簡訊" - "其他裝置上有進行中的通話" - "轉接來電" - "撥號前,請先關閉飛航模式。" - "尚未註冊網路。" - "無法連線到行動網路。" - "如要撥打電話,請輸入有效的號碼。" - "無法通話。" - "開始 MMI 序列…" - "不支援的服務。" - "無法切換通話。" - "無法分割通話。" - "無法轉接。" - "無法進行電話會議。" - "無法拒接來電。" - "無法掛斷電話。" - "SIP 通話" - "緊急電話" - "正在開啟無線通訊…" - "找不到服務訊號,重試中…" - "無法通話。%s 不是緊急電話號碼。" - "無法通話。只能撥打緊急號碼。" - "使用鍵盤撥號" - "保留通話" - "恢復通話" - "結束通話" - "顯示撥號鍵盤" - "隱藏撥號鍵盤" - "忽略" - "取消忽略" - "新增通話" - "合併通話" - "切換" - "管理通話" - "管理電話會議" - "電話會議" - "管理" - "音訊" - "視訊通話" - "變更為語音通話" - "切換鏡頭" - "開啟攝影機" - "關閉攝影機" - "更多選項" - "已啟動播放器" - "已停止播放器" - "相機尚未就緒" - "相機已準備就緒" - "不明的通話工作階段事件" - "服務" - "設定" - "<未設定>" - "其他通話設定" - "正在透過 %s 撥號" - "有人透過 %s 來電" - "聯絡人相片" - "私人通話" - "選取聯絡人" - "自行撰寫回應…" - "取消" - "傳送" - "接聽" - "傳送簡訊" - "拒絕" - "接聽視訊通話" - "接聽語音通話" - "接受視訊通話要求" - "拒絕視訊通話要求" - "接受視訊傳送要求" - "拒絕視訊傳送要求" - "接受視訊接收要求" - "拒絕視訊接收要求" - "向上滑動即可%s。" - "向左滑動即可%s。" - "向右滑動即可%s。" - "向下滑動即可%s。" - "震動" - "震動" - "音效" - "預設音效 (%1$s)" - "手機鈴聲" - "鈴響時震動" - "鈴聲與震動" - "管理電話會議" - "緊急電話號碼" - "個人資料相片" - "相機已停用" - "透過 %s" - "備註已送出" - "最近的訊息" - "商家資訊" - "%.1f 英里遠" - "%.1f 公里遠" - "%2$s%1$s" - "%1$s - %2$s" - "%1$s%2$s" - "將於明日%s開始營業" - "將於本日%s開始營業" - "將於%s結束營業" - "已於本日%s結束營業" - "營業中" - "本日已結束營業" - "可疑的騷擾/廣告來電者" - "通話結束 %1$s" - "這組號碼首次致電給您。" - "我們懷疑這通來電是騷擾/廣告電話。" - "封鎖/回報為騷擾/廣告電話" - "新增聯絡人" - "非騷擾/廣告電話" - diff --git a/InCallUI/res/values-zu/strings.xml b/InCallUI/res/values-zu/strings.xml deleted file mode 100644 index 46bf5afbb..000000000 --- a/InCallUI/res/values-zu/strings.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - "Ifoni" - "Ibanjiwe" - "Akwaziwa" - "Inombolo eyimfihlo" - "Ucingo olufakwa imali" - "Ikholi yenkomfa" - "Ikholi ivaliwe" - "Isipikha" - "Isipikha sendlebe sama-ear phone" - "Ama-earphone anezintambo" - "I-Bluetooth" - "Thumela amathoni alandelayo?\n" - "Ithumela amathoni\n" - "Thumela" - "Yebo" - "Cha" - "Miselela uhlamvu lwasendle nge" - "Ikholi yenkomfa engu-%s" - "Inombolo yevoyisimeyili" - "Iyadayela" - "Iphinda iyadayela" - "Ikholi yenkomfa" - "Ikholi engenayo" - "Ikholi engenayo yomsebenzi" - "Ikholi iqediwe" - "Ibanjiwe" - "Iyavala" - "Ukukholi" - "Inombolo yami ngu-%s" - "Ixhuma ividiyo" - "Ikholi yevidiyo" - "Icela ividiyo" - "Ayikwazi ukuxhuma ikholi yevidiyo" - "Isicelo sevidiyo sinqatshelwe" - "Inombolo yakho yokuphinda ushaye\n%1$s" - "Inombolo yakho yokuphinda ushayelwe yesimo esiphuthumayo\n%1$s" - "Iyadayela" - "Ikholi ephuthelwe" - "Amakholi akuphuthele" - "%s amakholi akulahlekele" - "Uphuthelwe ikholi kusukela ku-%s" - "Ikholi eqhubekayo" - "Ikholi yomsebenzi eqhubekayo" - "Ikholi ye-Wi-Fi eqhubekayo" - "Ikholi yomsebenzi eqhubekayo ye-Wi-Fi" - "Ibanjiwe" - "Ikholi engenayo" - "Ikholi engenayo yomsebenzi" - "Ikholi ye-Wi-Fi engenayo" - "Ikholi engenayo yomsebenzi ye-Wi-Fi" - "Ikholi yevidiyo engenayo" - "Ikholi engenayo osolisayo kagaxekile" - "Isicelo sevidiyo engenayo" - "Ivoyisimeyili entsha" - "Ivoyisimeyili entsha (%d)" - "Dayela u-%s" - "Inombolo yevoyisimeyili ayaziwa" - "Ayikho isevisi" - "Inethiwekhi ekhethiwe (%s) ayitholakali" - "Phendula" - "Vala ikholi" - "Ividiyo" - "Izwi" - "Yamukela" - "Cashisa" - "Phinda ushayele" - "Umlayezo" - "Ikholi eqhubekayo kwenye idivayisi" - "Dlulisela ikholi" - "Ukwenza ikholi, vala kuqala imodi Yendiza." - "Ayibhalisiwe kwinethiwekhi." - "Inethiwekhi yeselula ayitholakali." - "Ukuze wenze ikholi, faka inombolo evumelekile." - "Ayikwazi ukushaya." - "Iqalisa ukulandelana kwe-MMI..." - "Isevisi ayisekelwe." - "Ayikwazi ukushintsha amakholi." - "Ayikwazi ukuhlukanisa ikholi." - "Ayikwazi ukudlulisela." - "Ayikwazi ukwenza inkomfa." - "Ayikwazi ukunqabela ikholi." - "Ayikwazi ukukhipha amakholi." - "Ikholi ye-SIP" - "Ikholi ephuthumayo" - "Ivula irediyo..." - "Ayikho isevisi. Iyazama futhi…" - "Ayikwazi ukushaya. U-%s akuyona inombolo yesimo esiphuthumayo." - "Ayikwazi ukushaya. Shayela inombolo yesimo esiphuthumayo." - "Sebenzisa ikhibhodi ukudayela" - "Bamba ikholi" - "Qalisa kabusha ikholi" - "Qeda ikholi" - "Bonisa iphedi yokudayela" - "Fihla iphedi yokudayela" - "Thulisa" - "Susa ukuthula" - "Engeza ikholi" - "Hlanganisa amakholi" - "Shintsha" - "Phatha amakholi" - "Phatha ucingo lwengqungquthela" - "Ikholi yenkomfa" - "Phatha" - "Umsindo" - "Ikholi yevidiyo" - "Shintshela kukholi yezwi" - "Shintsha Ikhamera" - "Vula ikhamera" - "Vala ikhamera" - "Izinketho eziningi" - "Umdlali uqalile" - "Umdlali umisiwe" - "Ikhamera ayilungile" - "Ikhamera ilungile" - "Umcimbi wesikhathi sekholi ongaziwa" - "Isevisi" - "Ukusetha" - "<Ayisethiwe>" - "Ezinye izilungiselelo zekholi" - "Ishaya nge-%s" - "Ingena nge-%s" - "isithombe soxhumana naye" - "yenza kube imfihlo" - "khetha othintana naye" - "Bhala okwakho…" - "Khansela" - "Thumela" - "Phendula" - "Thumela i-SMS" - "Yenqaba" - "Phendula njengekholi yevidiyo" - "Phendula njengekholi yomsindo" - "Yamukela isicelo sevidiyo" - "Yenqaba isicelo sevidiyo" - "Yamukela isicelo sokudlulisa ividiyo" - "Yenqaba isicelo sokudlulisa ividiyo" - "Yamukela isicelo sokwamukela ividiyo" - "Yenqaba isicelo sokwamukela ividiyo" - "Slayidela phezulu ku-%s." - "Slayida ngakwesokunxele ku-%s." - "Slayida ngakwesokudla ku-%s." - "Slayida ngezansi ku-%s." - "Dlidlizela" - "Dlidlizela" - "Umsindo" - "Umsindo ozenzakalelayo (%1$s)" - "Ithoni yokukhala yefoni" - "Dlidlizisa uma ikhala" - "Ithoni yokukhala nokudlidliza" - "Phatha ucingo lwengqungquthela" - "Inombolo ephuthumayo" - "Isithombe sephrofayela" - "Ikhamera ivaliwe" - "nge-%s" - "Inothi lithunyelwe" - "Imilayezo yakamuva" - "Ulwazi lwebhizinisi" - "%.1f amamitha kude" - "%.1f amakhilomitha kude" - "%1$s, %2$s" - "%1$s - %2$s" - "%1$s, %2$s" - "Kuvulwa kusasa ngo-%s" - "Kuvulwa namuhla ngo-%s" - "Kuvalwa ngo-%s" - "Kuvalwe namuhla ngo-%s" - "Kuvuliwe manje" - "Kuvaliwe manje" - "Ofonayo osolisayo wogaxekile" - "Ikholi iphelile %1$s" - "Lesi isikhathi sokuqala le nombolo ikushayela." - "Sisolele le kholi ukuthi ugaxekile." - "Vimba/bika ugaxekile" - "Engeza oxhumana naye" - "Akusiko okugaxekile" - diff --git a/InCallUI/res/values/animation_constants.xml b/InCallUI/res/values/animation_constants.xml deleted file mode 100644 index 8df6a7281..000000000 --- a/InCallUI/res/values/animation_constants.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - 333 - 333 - 257 - diff --git a/InCallUI/res/values/array.xml b/InCallUI/res/values/array.xml deleted file mode 100644 index 7877ec8f3..000000000 --- a/InCallUI/res/values/array.xml +++ /dev/null @@ -1,135 +0,0 @@ - - - - - - - - - - @drawable/ic_lockscreen_answer - @null - @drawable/ic_lockscreen_decline - @null" - - - @string/description_target_answer - @null - @string/description_target_decline - @null" - - - @string/description_direction_right - @null - @string/description_direction_left - @null - - - - - @drawable/ic_lockscreen_answer - @drawable/ic_lockscreen_text - @drawable/ic_lockscreen_decline - @null" - - - @string/description_target_answer - @string/description_target_send_sms - @string/description_target_decline - @null" - - - @string/description_direction_right - @string/description_direction_up - @string/description_direction_left - @null - - - - - @drawable/ic_lockscreen_answer - @null - @drawable/ic_lockscreen_decline - @drawable/ic_lockscreen_answer_video - - - @string/description_target_answer_video_call - @null - @string/description_target_decline - @string/description_target_answer_audio_call - - - @string/description_direction_right - @null - @string/description_direction_left - @string/description_direction_down - - - - - @drawable/ic_lockscreen_answer_video - @drawable/ic_lockscreen_text - @drawable/ic_lockscreen_decline - @drawable/ic_lockscreen_answer - - - @string/description_target_answer_video_call - @string/description_target_send_sms - @string/description_target_decline - @string/description_target_answer_audio_call - - - @string/description_direction_right - @string/description_direction_up - @string/description_direction_left - @string/description_direction_down - - - - - @drawable/ic_lockscreen_answer_video - @drawable/ic_lockscreen_decline_video - - - - @string/description_target_accept_upgrade_to_video_request - @null - @string/description_target_decline_upgrade_to_video_request - @null" - - - @string/description_direction_right - @null - @string/description_direction_left - @null - - diff --git a/InCallUI/res/values/attrs.xml b/InCallUI/res/values/attrs.xml deleted file mode 100644 index e135fb72d..000000000 --- a/InCallUI/res/values/attrs.xml +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/InCallUI/res/values/colors.xml b/InCallUI/res/values/colors.xml deleted file mode 100644 index 238d36033..000000000 --- a/InCallUI/res/values/colors.xml +++ /dev/null @@ -1,133 +0,0 @@ - - - - - - - @color/dialer_theme_color - #ffffff - - - @color/incall_background_color - #ffffff - - #ffffff - #f5f5f5 - #333333 - - @color/incall_background_color - @color/incall_call_banner_text_color - - #545454 - - - #cc000000 - - #f8f8f8 - #4d4d4d - #999999 - - #999999 - #ffffff - - #dddddd - - - #333 - - #ffffff - #ccaaaaaa - - @color/incall_background_color - @color/dialer_theme_color_dark - - #b3ffffff - - #33ffffff - - - @color/dialer_theme_color - - @color/dialer_theme_color - - #33999999 - - - #b3000000 - - #26ffffff - #ffffff - #ffffff - #cccccc - #00c853 - #ff1744 - #a3a3a3 - #ffffff - - - #B2FFFFFF - - - #330288d1 - - - - #00796B - #3367D6 - #303F9F - #7B1FA2 - #C2185B - #C53929 - #A52714 - - - - - #00695C - #2A56C6 - #283593 - #6A1B9A - #AD1457 - #B93221 - #841F10 - - - - #A52714 - - - #40000000 - - @color/incall_call_banner_subtext_color - @color/dialer_theme_color - @color/incall_call_banner_subtext_color - @color/incall_call_banner_subtext_color - @color/incall_call_banner_subtext_color - - - #ffffff - - - #919191 - diff --git a/InCallUI/res/values/config.xml b/InCallUI/res/values/config.xml deleted file mode 100644 index b81ba3ca0..000000000 --- a/InCallUI/res/values/config.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - 5 - - - true - - 5000 - \ No newline at end of file diff --git a/InCallUI/res/values/dimens.xml b/InCallUI/res/values/dimens.xml deleted file mode 100644 index 59da7860a..000000000 --- a/InCallUI/res/values/dimens.xml +++ /dev/null @@ -1,150 +0,0 @@ - - - - - - false - - - false - - - - 0dp - 20dp - - 3dp - - - 2dp - - - 24dp - - 16dp - 24dp - 32dp - 16sp - - - 8dp - - - 16sp - 12sp - 34dp - 28sp - 16sp - - 50sp - - - 48dp - - 0dp - 2dp - - - 1dp - - 0dp - 20sp - 50dp - 36dp - - -10dp - - 8dp - 6dp - - - 10dp - 5dp - 10dp - - - 64dp - - - 56dp - - - 250dp - - 125dp - - - 70dip - - - 40dip - - - 15dip - - -48dip - 0dip - - 2dp - - 2dp - - 50dp - - - 90dp - - 0dp - - 72dp - 56dp - - 64dp - 46dp - - 14sp - 19dp - 23dp - 13dp - 13dp - - 30dp - 16sp - 7dp - 12dp - 15dp - 2dp - 7dp - 14sp - - 10dp - 25dp - 20dp - 16sp - 12sp - - 40dp - diff --git a/InCallUI/res/values/ids.xml b/InCallUI/res/values/ids.xml deleted file mode 100644 index accb8fb73..000000000 --- a/InCallUI/res/values/ids.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - diff --git a/InCallUI/res/values/strings.xml b/InCallUI/res/values/strings.xml deleted file mode 100644 index 92de14042..000000000 --- a/InCallUI/res/values/strings.xml +++ /dev/null @@ -1,540 +0,0 @@ - - - - - - - Phone - - - InCallUI - - - - - On hold - - - Unknown - - Private number - - Payphone - - - Conference call - - Call dropped - - - - - - Speaker - - Handset earpiece - - Wired headset - - Bluetooth - - - - Send the following tones?\n - - Sending tones\n - - Send - - Yes - - No - - Replace wild character with - - - Conference call %s - - - (650) 555-1234 - - Incoming phone number - - Fake Incoming Call - - - Voicemail number - - - - Dialing - - Redialing - - Conference call - - Incoming call - - Incoming work call - - Call ended - - On hold - - Hanging up - - In call - - My number is %s - - Connecting video - - Video call - - Requesting video - - Can\'t connect video call - - Video request rejected - - - Your callback number\n - %1$s - - - - Your emergency callback number\n - %1$s - - - - - Dialing - - Missed call - - Missed calls - - %s missed calls - - Missed call from %s - - Ongoing call - - Ongoing work call - - Ongoing Wi-Fi call - - Ongoing Wi-Fi work call - - On hold - - Incoming call - - Incoming work call - - Incoming Wi-Fi call - - Incoming Wi-Fi work call - - Incoming video call - - Incoming suspected spam call - - Incoming video request - - New voicemail - - New voicemail (%d) - - Dial %s - - Voicemail number unknown - - No service - - Selected network (%s) unavailable - - Answer - - Hang up - - Video - - Voice - - Accept - - Dismiss - - - Call back - - Message - - Ongoing call on another device - - Transfer Call - - - To place a call, first turn off Airplane mode. - - Not registered on network. - - Cellular network not available. - - To place a call, enter a valid number. - - Can\'t call. - - Starting MMI sequence\u2026 - - Service not supported. - - Can\'t switch calls. - - Can\'t separate call. - - Can\'t transfer. - - Can\'t conference. - - Can\'t reject call. - - Can\'t release call(s). - - - SIP call - - - Emergency call - - Turning on radio\u2026 - - No service. Trying again\u2026 - - - - Can\'t call. %s is not an emergency number. - - Can\'t call. Dial an emergency number. - - - Use keyboard to dial - - - Hold Call - - Resume Call - - End Call - - Show Dialpad - - Hide Dialpad - - Mute - - Unmute - - Add call - - Merge calls - - Swap - - Manage calls - - Manage conference call - - Conference call - - Manage - - Audio - - Video call - - Change to voice call - - Switch camera - - Turn on camera - - Turn off camera - - More options - - - Player Started - - Player Stopped - - Camera not ready - - Camera ready - - "Unkown call session event" - - - - ABSENT NUMBER - ABSENTNUMBER - - - - Service - - - Setup - - - <Not set> - - - Other call settings - - - Calling via %s - - Incoming via %s - - - contact photo - - go private - - select contact - - - Write your own... - - Cancel - - Send - - - Answer - - Send SMS - - Decline - - Answer as video call - - Answer as audio call - - Accept video request - - Decline video request - - Accept video transmit request - - Decline video transmit request - - Accept video receive request - - Decline video receive request - - - Slide up for %s. - - "Slide left for %s. - - Slide right for %s. - - Slide down for %s. - - - Vibrate - - Vibrate - - - Sound - - - Default sound (%1$s) - - - never - - - - always - silent - never - - - - Phone ringtone - - - Vibrate when ringing - - - Ringtone & Vibrate - - - Manage conference call - - - Emergency number - - - Profile photo - - - Camera off - - - via %s - - - Note sent - - - Recent messages - - - Business info - - - - - %.1f mi away - - %.1f km away - - %1$s, %2$s - - %1$s - %2$s - - %1$s, %2$s - - Opens tomorrow at %s - - Opens today at %s - - Closes at %s - - Closed today at %s - - Open now - - Closed now - - Suspected spam caller - - - Call ended %1$s - - This is the first time this number called you. - - We suspected this call to be a spammer. - - Block/report spam - - Add contact - - Not spam - diff --git a/InCallUI/res/values/styles.xml b/InCallUI/res/values/styles.xml deleted file mode 100644 index 11d636261..000000000 --- a/InCallUI/res/values/styles.xml +++ /dev/null @@ -1,100 +0,0 @@ - - - - - #FF333333 - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/InCallUI/src/com/android/incallui/AccelerometerListener.java b/InCallUI/src/com/android/incallui/AccelerometerListener.java deleted file mode 100644 index b5ad29675..000000000 --- a/InCallUI/src/com/android/incallui/AccelerometerListener.java +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Copyright (C) 2009 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui; - -import android.content.Context; -import android.hardware.Sensor; -import android.hardware.SensorEvent; -import android.hardware.SensorEventListener; -import android.hardware.SensorManager; -import android.os.Handler; -import android.os.Message; -import android.util.Log; - -/** - * This class is used to listen to the accelerometer to monitor the - * orientation of the phone. The client of this class is notified when - * the orientation changes between horizontal and vertical. - */ -public class AccelerometerListener { - private static final String TAG = "AccelerometerListener"; - private static final boolean DEBUG = true; - private static final boolean VDEBUG = false; - - private SensorManager mSensorManager; - private Sensor mSensor; - - // mOrientation is the orientation value most recently reported to the client. - private int mOrientation; - - // mPendingOrientation is the latest orientation computed based on the sensor value. - // This is sent to the client after a rebounce delay, at which point it is copied to - // mOrientation. - private int mPendingOrientation; - - private OrientationListener mListener; - - // Device orientation - public static final int ORIENTATION_UNKNOWN = 0; - public static final int ORIENTATION_VERTICAL = 1; - public static final int ORIENTATION_HORIZONTAL = 2; - - private static final int ORIENTATION_CHANGED = 1234; - - private static final int VERTICAL_DEBOUNCE = 100; - private static final int HORIZONTAL_DEBOUNCE = 500; - private static final double VERTICAL_ANGLE = 50.0; - - public interface OrientationListener { - public void orientationChanged(int orientation); - } - - public AccelerometerListener(Context context) { - mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE); - mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); - } - - public void setListener(OrientationListener listener) { - mListener = listener; - } - - public void enable(boolean enable) { - if (DEBUG) Log.d(TAG, "enable(" + enable + ")"); - synchronized (this) { - if (enable) { - mOrientation = ORIENTATION_UNKNOWN; - mPendingOrientation = ORIENTATION_UNKNOWN; - mSensorManager.registerListener(mSensorListener, mSensor, - SensorManager.SENSOR_DELAY_NORMAL); - } else { - mSensorManager.unregisterListener(mSensorListener); - mHandler.removeMessages(ORIENTATION_CHANGED); - } - } - } - - private void setOrientation(int orientation) { - synchronized (this) { - if (mPendingOrientation == orientation) { - // Pending orientation has not changed, so do nothing. - return; - } - - // Cancel any pending messages. - // We will either start a new timer or cancel alltogether - // if the orientation has not changed. - mHandler.removeMessages(ORIENTATION_CHANGED); - - if (mOrientation != orientation) { - // Set timer to send an event if the orientation has changed since its - // previously reported value. - mPendingOrientation = orientation; - final Message m = mHandler.obtainMessage(ORIENTATION_CHANGED); - // set delay to our debounce timeout - int delay = (orientation == ORIENTATION_VERTICAL ? VERTICAL_DEBOUNCE - : HORIZONTAL_DEBOUNCE); - mHandler.sendMessageDelayed(m, delay); - } else { - // no message is pending - mPendingOrientation = ORIENTATION_UNKNOWN; - } - } - } - - private void onSensorEvent(double x, double y, double z) { - if (VDEBUG) Log.d(TAG, "onSensorEvent(" + x + ", " + y + ", " + z + ")"); - - // If some values are exactly zero, then likely the sensor is not powered up yet. - // ignore these events to avoid false horizontal positives. - if (x == 0.0 || y == 0.0 || z == 0.0) return; - - // magnitude of the acceleration vector projected onto XY plane - final double xy = Math.hypot(x, y); - // compute the vertical angle - double angle = Math.atan2(xy, z); - // convert to degrees - angle = angle * 180.0 / Math.PI; - final int orientation = (angle > VERTICAL_ANGLE ? ORIENTATION_VERTICAL : ORIENTATION_HORIZONTAL); - if (VDEBUG) Log.d(TAG, "angle: " + angle + " orientation: " + orientation); - setOrientation(orientation); - } - - SensorEventListener mSensorListener = new SensorEventListener() { - @Override - public void onSensorChanged(SensorEvent event) { - onSensorEvent(event.values[0], event.values[1], event.values[2]); - } - - @Override - public void onAccuracyChanged(Sensor sensor, int accuracy) { - // ignore - } - }; - - Handler mHandler = new Handler() { - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case ORIENTATION_CHANGED: - synchronized (this) { - mOrientation = mPendingOrientation; - if (DEBUG) { - Log.d(TAG, "orientation: " + - (mOrientation == ORIENTATION_HORIZONTAL ? "horizontal" - : (mOrientation == ORIENTATION_VERTICAL ? "vertical" - : "unknown"))); - } - if (mListener != null) { - mListener.orientationChanged(mOrientation); - } - } - break; - } - } - }; -} diff --git a/InCallUI/src/com/android/incallui/AccessibleAnswerFragment.java b/InCallUI/src/com/android/incallui/AccessibleAnswerFragment.java deleted file mode 100644 index 89c78ec61..000000000 --- a/InCallUI/src/com/android/incallui/AccessibleAnswerFragment.java +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.os.Bundle; -import android.telecom.VideoProfile; -import android.view.GestureDetector; -import android.view.GestureDetector.SimpleOnGestureListener; -import android.view.LayoutInflater; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; - -import com.android.dialer.R; - -/** - * AnswerFragment to use when touch exploration is enabled in accessibility. - */ -public class AccessibleAnswerFragment extends AnswerFragment { - - private static final String TAG = AccessibleAnswerFragment.class.getSimpleName(); - private static final int SWIPE_THRESHOLD = 100; - - private View mAnswer; - private View mDecline; - private View mText; - - private TouchListener mTouchListener; - private GestureDetector mGestureDetector; - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - ViewGroup group = (ViewGroup) inflater.inflate(R.layout.accessible_answer_fragment, - container, false); - - mTouchListener = new TouchListener(); - mGestureDetector = new GestureDetector(getContext(), new SimpleOnGestureListener() { - @Override - public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, - float velocityY) { - return AccessibleAnswerFragment.this.onFling(e1, e2, velocityX, velocityX); - } - }); - - mAnswer = group.findViewById(R.id.accessible_answer_fragment_answer); - mAnswer.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Log.d(TAG, "Answer Button Clicked"); - onAnswer(VideoProfile.STATE_AUDIO_ONLY, getContext()); - } - }); - mDecline = group.findViewById(R.id.accessible_answer_fragment_decline); - mDecline.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Log.d(TAG, "Decline Button Clicked"); - onDecline(getContext()); - } - }); - - mText = group.findViewById(R.id.accessible_answer_fragment_text); - mText.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Log.d(TAG, "Text Button Clicked"); - onText(); - } - }); - return group; - } - - @Override - public void onResume() { - super.onResume(); - // Intercept all touch events for full screen swiping gesture. - InCallActivity activity = (InCallActivity) getActivity(); - activity.setDispatchTouchEventListener(mTouchListener); - } - - @Override - public void onPause() { - super.onPause(); - InCallActivity activity = (InCallActivity) getActivity(); - activity.setDispatchTouchEventListener(null); - } - - private class TouchListener implements View.OnTouchListener { - @Override - public boolean onTouch(View v, MotionEvent event) { - return mGestureDetector.onTouchEvent(event); - } - } - - private boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, - float velocityY) { - if (hasPendingDialogs()) { - return false; - } - - float diffY = e2.getY() - e1.getY(); - float diffX = e2.getX() - e1.getX(); - if (Math.abs(diffX) > Math.abs(diffY)) { - if (Math.abs(diffX) > SWIPE_THRESHOLD) { - if (diffX > 0) { - onSwipeRight(); - } else { - onSwipeLeft(); - } - } - return true; - } else if (Math.abs(diffY) > SWIPE_THRESHOLD) { - if (diffY > 0) { - onSwipeDown(); - } else { - onSwipeUp(); - } - return true; - } - - return false; - } - - private void onSwipeUp() { - Log.d(TAG, "onSwipeUp"); - onText(); - } - - private void onSwipeDown() { - Log.d(TAG, "onSwipeDown"); - } - - private void onSwipeLeft() { - Log.d(TAG, "onSwipeLeft"); - onDecline(getContext()); - } - - private void onSwipeRight() { - Log.d(TAG, "onSwipeRight"); - onAnswer(VideoProfile.STATE_AUDIO_ONLY, getContext()); - } -} diff --git a/InCallUI/src/com/android/incallui/AnswerFragment.java b/InCallUI/src/com/android/incallui/AnswerFragment.java deleted file mode 100644 index 44ddfcd49..000000000 --- a/InCallUI/src/com/android/incallui/AnswerFragment.java +++ /dev/null @@ -1,307 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.app.AlertDialog; -import android.app.Dialog; -import android.content.Context; -import android.content.DialogInterface; -import android.os.Bundle; -import android.text.Editable; -import android.text.TextWatcher; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.WindowManager; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.Button; -import android.widget.EditText; -import android.widget.ListView; - -import com.android.dialer.R; - -import java.util.ArrayList; -import java.util.List; - - -/** - * Provides only common interface and functions. Should be derived to implement the actual UI. - */ -public abstract class AnswerFragment extends BaseFragment - implements AnswerPresenter.AnswerUi { - - public static final int TARGET_SET_FOR_AUDIO_WITHOUT_SMS = 0; - public static final int TARGET_SET_FOR_AUDIO_WITH_SMS = 1; - public static final int TARGET_SET_FOR_VIDEO_WITHOUT_SMS = 2; - public static final int TARGET_SET_FOR_VIDEO_WITH_SMS = 3; - public static final int TARGET_SET_FOR_VIDEO_ACCEPT_REJECT_REQUEST = 4; - - /** - * This fragment implement no UI at all. Derived class should do it. - */ - @Override - public abstract View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState); - - /** - * The popup showing the list of canned responses. - * - * This is an AlertDialog containing a ListView showing the possible choices. This may be null - * if the InCallScreen hasn't ever called showRespondViaSmsPopup() yet, or if the popup was - * visible once but then got dismissed. - */ - private Dialog mCannedResponsePopup = null; - - /** - * The popup showing a text field for users to type in their custom message. - */ - private AlertDialog mCustomMessagePopup = null; - - private ArrayAdapter mSmsResponsesAdapter; - - private final List mSmsResponses = new ArrayList<>(); - - @Override - public AnswerPresenter createPresenter() { - return InCallPresenter.getInstance().getAnswerPresenter(); - } - - @Override - public AnswerPresenter.AnswerUi getUi() { - return this; - } - - @Override - public void showMessageDialog() { - final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - - mSmsResponsesAdapter = new ArrayAdapter<>(builder.getContext(), - android.R.layout.simple_list_item_1, android.R.id.text1, mSmsResponses); - - final ListView lv = new ListView(getActivity()); - lv.setAdapter(mSmsResponsesAdapter); - lv.setOnItemClickListener(new RespondViaSmsItemClickListener()); - - builder.setCancelable(true).setView(lv).setOnCancelListener( - new DialogInterface.OnCancelListener() { - @Override - public void onCancel(DialogInterface dialogInterface) { - onMessageDialogCancel(); - dismissCannedResponsePopup(); - getPresenter().onDismissDialog(); - } - }); - mCannedResponsePopup = builder.create(); - mCannedResponsePopup.getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); - mCannedResponsePopup.show(); - } - - private boolean isCannedResponsePopupShowing() { - if (mCannedResponsePopup != null) { - return mCannedResponsePopup.isShowing(); - } - return false; - } - - private boolean isCustomMessagePopupShowing() { - if (mCustomMessagePopup != null) { - return mCustomMessagePopup.isShowing(); - } - return false; - } - - /** - * Dismiss the canned response list popup. - * - * This is safe to call even if the popup is already dismissed, and even if you never called - * showRespondViaSmsPopup() in the first place. - */ - protected void dismissCannedResponsePopup() { - if (mCannedResponsePopup != null) { - mCannedResponsePopup.dismiss(); // safe even if already dismissed - mCannedResponsePopup = null; - } - } - - /** - * Dismiss the custom compose message popup. - */ - private void dismissCustomMessagePopup() { - if (mCustomMessagePopup != null) { - mCustomMessagePopup.dismiss(); - mCustomMessagePopup = null; - } - } - - public void dismissPendingDialogs() { - if (isCannedResponsePopupShowing()) { - dismissCannedResponsePopup(); - } - - if (isCustomMessagePopupShowing()) { - dismissCustomMessagePopup(); - } - } - - public boolean hasPendingDialogs() { - return !(mCannedResponsePopup == null && mCustomMessagePopup == null); - } - - /** - * Shows the custom message entry dialog. - */ - public void showCustomMessageDialog() { - // Create an alert dialog containing an EditText - final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - final EditText et = new EditText(builder.getContext()); - builder.setCancelable(true).setView(et) - .setPositiveButton(R.string.custom_message_send, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - // The order is arranged in a way that the popup will be destroyed - // when the InCallActivity is about to finish. - final String textMessage = et.getText().toString().trim(); - dismissCustomMessagePopup(); - getPresenter().rejectCallWithMessage(textMessage); - } - }) - .setNegativeButton(R.string.custom_message_cancel, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - dismissCustomMessagePopup(); - getPresenter().onDismissDialog(); - } - }) - .setOnCancelListener(new DialogInterface.OnCancelListener() { - @Override - public void onCancel(DialogInterface dialogInterface) { - dismissCustomMessagePopup(); - getPresenter().onDismissDialog(); - } - }) - .setTitle(R.string.respond_via_sms_custom_message); - mCustomMessagePopup = builder.create(); - - // Enable/disable the send button based on whether there is a message in the EditText - et.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - } - - @Override - public void afterTextChanged(Editable s) { - final Button sendButton = mCustomMessagePopup.getButton( - DialogInterface.BUTTON_POSITIVE); - sendButton.setEnabled(s != null && s.toString().trim().length() != 0); - } - }); - - // Keyboard up, show the dialog - mCustomMessagePopup.getWindow().setSoftInputMode( - WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); - mCustomMessagePopup.getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); - mCustomMessagePopup.show(); - - // Send button starts out disabled - final Button sendButton = mCustomMessagePopup.getButton(DialogInterface.BUTTON_POSITIVE); - sendButton.setEnabled(false); - } - - @Override - public void configureMessageDialog(List textResponses) { - mSmsResponses.clear(); - mSmsResponses.addAll(textResponses); - mSmsResponses.add(getResources().getString( - R.string.respond_via_sms_custom_message)); - if (mSmsResponsesAdapter != null) { - mSmsResponsesAdapter.notifyDataSetChanged(); - } - } - - @Override - public Context getContext() { - return getActivity(); - } - - public void onAnswer(int videoState, Context context) { - Log.d(this, "onAnswer videoState=" + videoState + " context=" + context); - getPresenter().onAnswer(videoState, context); - } - - public void onDecline(Context context) { - getPresenter().onDecline(context); - } - - public void onDeclineUpgradeRequest(Context context) { - InCallPresenter.getInstance().declineUpgradeRequest(context); - } - - public void onText() { - getPresenter().onText(); - } - - /** - * OnItemClickListener for the "Respond via SMS" popup. - */ - public class RespondViaSmsItemClickListener implements AdapterView.OnItemClickListener { - - /** - * Handles the user selecting an item from the popup. - */ - @Override - public void onItemClick(AdapterView parent, // The ListView - View view, // The TextView that was clicked - int position, long id) { - Log.d(this, "RespondViaSmsItemClickListener.onItemClick(" + position + ")..."); - final String message = (String) parent.getItemAtPosition(position); - Log.v(this, "- message: '" + message + "'"); - dismissCannedResponsePopup(); - - // The "Custom" choice is a special case. - // (For now, it's guaranteed to be the last item.) - if (position == (parent.getCount() - 1)) { - // Show the custom message dialog - showCustomMessageDialog(); - } else { - getPresenter().rejectCallWithMessage(message); - } - } - } - - public void onShowAnswerUi(boolean shown) { - // Do Nothing - } - - public void showTargets(int targetSet) { - // Do Nothing - } - - public void showTargets(int targetSet, int videoState) { - // Do Nothing - } - - protected void onMessageDialogCancel() { - // Do nothing. - } -} diff --git a/InCallUI/src/com/android/incallui/AnswerPresenter.java b/InCallUI/src/com/android/incallui/AnswerPresenter.java deleted file mode 100644 index 883b54fed..000000000 --- a/InCallUI/src/com/android/incallui/AnswerPresenter.java +++ /dev/null @@ -1,312 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.content.Context; - -import com.android.dialer.compat.UserManagerCompat; -import com.android.dialer.util.TelecomUtil; -import com.android.incallui.InCallPresenter.InCallState; - -import java.util.List; - -/** - * Presenter for the Incoming call widget. The {@link AnswerPresenter} handles the logic during - * incoming calls. It is also in charge of responding to incoming calls, so there needs to be - * an instance alive so that it can receive onIncomingCall callbacks. - * - * An instance of {@link AnswerPresenter} is created by InCallPresenter at startup, registers - * for callbacks via InCallPresenter, and shows/hides the {@link AnswerFragment} via IncallActivity. - * - */ -public class AnswerPresenter extends Presenter - implements CallList.CallUpdateListener, InCallPresenter.InCallUiListener, - InCallPresenter.IncomingCallListener, - CallList.Listener { - - private static final String TAG = AnswerPresenter.class.getSimpleName(); - - private String mCallId; - private Call mCall = null; - private boolean mHasTextMessages = false; - - @Override - public void onUiShowing(boolean showing) { - if (showing) { - CallList.getInstance().addListener(this); - final CallList calls = CallList.getInstance(); - Call call; - call = calls.getIncomingCall(); - if (call != null) { - processIncomingCall(call); - } - call = calls.getVideoUpgradeRequestCall(); - Log.d(this, "getVideoUpgradeRequestCall call =" + call); - if (call != null) { - showAnswerUi(true); - processVideoUpgradeRequestCall(call); - } - } else { - CallList.getInstance().removeListener(this); - // This is necessary because the activity can be destroyed while an incoming call exists. - // This happens when back button is pressed while incoming call is still being shown. - if (mCallId != null) { - CallList.getInstance().removeCallUpdateListener(mCallId, this); - } - } - } - - @Override - public void onIncomingCall(InCallState oldState, InCallState newState, Call call) { - Log.d(this, "onIncomingCall: " + this); - Call modifyCall = CallList.getInstance().getVideoUpgradeRequestCall(); - if (modifyCall != null) { - showAnswerUi(false); - Log.d(this, "declining upgrade request id: "); - CallList.getInstance().removeCallUpdateListener(mCallId, this); - InCallPresenter.getInstance().declineUpgradeRequest(); - } - if (!call.getId().equals(mCallId)) { - // A new call is coming in. - processIncomingCall(call); - } - } - - @Override - public void onIncomingCall(Call call) { - } - - @Override - public void onCallListChange(CallList list) { - } - - @Override - public void onDisconnect(Call call) { - // no-op - } - - public void onSessionModificationStateChange(int sessionModificationState) { - boolean isUpgradePending = sessionModificationState == - Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST; - - if (!isUpgradePending) { - // Stop listening for updates. - CallList.getInstance().removeCallUpdateListener(mCallId, this); - showAnswerUi(false); - } - } - - @Override - public void onLastForwardedNumberChange() { - // no-op - } - - @Override - public void onChildNumberChange() { - // no-op - } - - private boolean isVideoUpgradePending(Call call) { - return call.getSessionModificationState() - == Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST; - } - - @Override - public void onUpgradeToVideo(Call call) { - Log.d(this, "onUpgradeToVideo: " + this + " call=" + call); - showAnswerUi(true); - boolean isUpgradePending = isVideoUpgradePending(call); - InCallPresenter inCallPresenter = InCallPresenter.getInstance(); - if (isUpgradePending - && inCallPresenter.getInCallState() == InCallPresenter.InCallState.INCOMING) { - Log.d(this, "declining upgrade request"); - //If there is incoming call reject upgrade request - inCallPresenter.declineUpgradeRequest(getUi().getContext()); - } else if (isUpgradePending) { - Log.d(this, "process upgrade request as no MT call"); - processVideoUpgradeRequestCall(call); - } - } - - private void processIncomingCall(Call call) { - mCallId = call.getId(); - mCall = call; - - // Listen for call updates for the current call. - CallList.getInstance().addCallUpdateListener(mCallId, this); - - Log.d(TAG, "Showing incoming for call id: " + mCallId + " " + this); - if (showAnswerUi(true)) { - final List textMsgs = CallList.getInstance().getTextResponses(call.getId()); - configureAnswerTargetsForSms(call, textMsgs); - } - } - - private boolean showAnswerUi(boolean show) { - final InCallActivity activity = InCallPresenter.getInstance().getActivity(); - if (activity != null) { - activity.showAnswerFragment(show); - if (getUi() != null) { - getUi().onShowAnswerUi(show); - } - return true; - } else { - return false; - } - } - - private void processVideoUpgradeRequestCall(Call call) { - Log.d(this, " processVideoUpgradeRequestCall call=" + call); - mCallId = call.getId(); - mCall = call; - - // Listen for call updates for the current call. - CallList.getInstance().addCallUpdateListener(mCallId, this); - - final int currentVideoState = call.getVideoState(); - final int modifyToVideoState = call.getRequestedVideoState(); - - if (currentVideoState == modifyToVideoState) { - Log.w(this, "processVideoUpgradeRequestCall: Video states are same. Return."); - return; - } - - AnswerUi ui = getUi(); - - if (ui == null) { - Log.e(this, "Ui is null. Can't process upgrade request"); - return; - } - showAnswerUi(true); - ui.showTargets(AnswerFragment.TARGET_SET_FOR_VIDEO_ACCEPT_REJECT_REQUEST, - modifyToVideoState); - } - - private boolean isEnabled(int videoState, int mask) { - return (videoState & mask) == mask; - } - - @Override - public void onCallChanged(Call call) { - Log.d(this, "onCallStateChange() " + call + " " + this); - if (call.getState() != Call.State.INCOMING) { - boolean isUpgradePending = isVideoUpgradePending(call); - if (!isUpgradePending) { - // Stop listening for updates. - CallList.getInstance().removeCallUpdateListener(mCallId, this); - } - - final Call incall = CallList.getInstance().getIncomingCall(); - if (incall != null || isUpgradePending) { - showAnswerUi(true); - } else { - showAnswerUi(false); - } - - mHasTextMessages = false; - } else if (!mHasTextMessages) { - final List textMsgs = CallList.getInstance().getTextResponses(call.getId()); - if (textMsgs != null) { - configureAnswerTargetsForSms(call, textMsgs); - } - } - } - - public void onAnswer(int videoState, Context context) { - if (mCallId == null) { - return; - } - - if (mCall.getSessionModificationState() - == Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) { - Log.d(this, "onAnswer (upgradeCall) mCallId=" + mCallId + " videoState=" + videoState); - InCallPresenter.getInstance().acceptUpgradeRequest(videoState, context); - } else { - Log.d(this, "onAnswer (answerCall) mCallId=" + mCallId + " videoState=" + videoState); - TelecomAdapter.getInstance().answerCall(mCall.getId(), videoState); - } - } - - /** - * TODO: We are using reject and decline interchangeably. We should settle on - * reject since it seems to be more prevalent. - */ - public void onDecline(Context context) { - Log.d(this, "onDecline " + mCallId); - if (mCall.getSessionModificationState() - == Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) { - InCallPresenter.getInstance().declineUpgradeRequest(context); - } else { - TelecomAdapter.getInstance().rejectCall(mCall.getId(), false, null); - } - } - - public void onText() { - if (getUi() != null) { - TelecomUtil.silenceRinger(getUi().getContext()); - getUi().showMessageDialog(); - } - } - - public void rejectCallWithMessage(String message) { - Log.d(this, "sendTextToDefaultActivity()..."); - TelecomAdapter.getInstance().rejectCall(mCall.getId(), true, message); - - onDismissDialog(); - } - - public void onDismissDialog() { - InCallPresenter.getInstance().onDismissDialog(); - } - - private void configureAnswerTargetsForSms(Call call, List textMsgs) { - if (getUi() == null) { - return; - } - mHasTextMessages = textMsgs != null; - boolean withSms = UserManagerCompat.isUserUnlocked(getUi().getContext()) - && call.can(android.telecom.Call.Details.CAPABILITY_RESPOND_VIA_TEXT) - && mHasTextMessages; - - // Only present the user with the option to answer as a video call if the incoming call is - // a bi-directional video call. - if (VideoUtils.isBidirectionalVideoCall(call)) { - if (withSms) { - getUi().showTargets(AnswerFragment.TARGET_SET_FOR_VIDEO_WITH_SMS); - getUi().configureMessageDialog(textMsgs); - } else { - getUi().showTargets(AnswerFragment.TARGET_SET_FOR_VIDEO_WITHOUT_SMS); - } - } else { - if (withSms) { - getUi().showTargets(AnswerFragment.TARGET_SET_FOR_AUDIO_WITH_SMS); - getUi().configureMessageDialog(textMsgs); - } else { - getUi().showTargets(AnswerFragment.TARGET_SET_FOR_AUDIO_WITHOUT_SMS); - } - } - } - - interface AnswerUi extends Ui { - public void onShowAnswerUi(boolean shown); - public void showTargets(int targetSet); - public void showTargets(int targetSet, int videoState); - public void showMessageDialog(); - public void configureMessageDialog(List textResponses); - public Context getContext(); - } -} diff --git a/InCallUI/src/com/android/incallui/AudioModeProvider.java b/InCallUI/src/com/android/incallui/AudioModeProvider.java deleted file mode 100644 index ea56dd624..000000000 --- a/InCallUI/src/com/android/incallui/AudioModeProvider.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.telecom.CallAudioState; - -import com.google.common.collect.Lists; - -import java.util.List; - -/** - * Proxy class for getting and setting the audio mode. - */ -public class AudioModeProvider { - - static final int AUDIO_MODE_INVALID = 0; - - private static AudioModeProvider sAudioModeProvider = new AudioModeProvider(); - private int mAudioMode = CallAudioState.ROUTE_EARPIECE; - private boolean mMuted = false; - private int mSupportedModes = CallAudioState.ROUTE_EARPIECE - | CallAudioState.ROUTE_BLUETOOTH | CallAudioState.ROUTE_WIRED_HEADSET - | CallAudioState.ROUTE_SPEAKER; - private final List mListeners = Lists.newArrayList(); - - public static AudioModeProvider getInstance() { - return sAudioModeProvider; - } - - public void onAudioStateChanged(boolean isMuted, int route, int supportedRouteMask) { - onAudioModeChange(route, isMuted); - onSupportedAudioModeChange(supportedRouteMask); - } - - public void onAudioModeChange(int newMode, boolean muted) { - if (mAudioMode != newMode) { - mAudioMode = newMode; - for (AudioModeListener l : mListeners) { - l.onAudioMode(mAudioMode); - } - } - - if (mMuted != muted) { - mMuted = muted; - for (AudioModeListener l : mListeners) { - l.onMute(mMuted); - } - } - } - - public void onSupportedAudioModeChange(int newModeMask) { - mSupportedModes = newModeMask; - - for (AudioModeListener l : mListeners) { - l.onSupportedAudioMode(mSupportedModes); - } - } - - public void addListener(AudioModeListener listener) { - if (!mListeners.contains(listener)) { - mListeners.add(listener); - listener.onSupportedAudioMode(mSupportedModes); - listener.onAudioMode(mAudioMode); - listener.onMute(mMuted); - } - } - - public void removeListener(AudioModeListener listener) { - if (mListeners.contains(listener)) { - mListeners.remove(listener); - } - } - - public int getSupportedModes() { - return mSupportedModes; - } - - public int getAudioMode() { - return mAudioMode; - } - - public boolean getMute() { - return mMuted; - } - - /* package */ interface AudioModeListener { - void onAudioMode(int newMode); - void onMute(boolean muted); - void onSupportedAudioMode(int modeMask); - } -} diff --git a/InCallUI/src/com/android/incallui/BaseFragment.java b/InCallUI/src/com/android/incallui/BaseFragment.java deleted file mode 100644 index 58d991acd..000000000 --- a/InCallUI/src/com/android/incallui/BaseFragment.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.app.Activity; -import android.app.Fragment; -import android.os.Bundle; - -/** - * Parent for all fragments that use Presenters and Ui design. - */ -public abstract class BaseFragment, U extends Ui> extends Fragment { - - private static final String KEY_FRAGMENT_HIDDEN = "key_fragment_hidden"; - - private T mPresenter; - - public abstract T createPresenter(); - - public abstract U getUi(); - - protected BaseFragment() { - mPresenter = createPresenter(); - } - - /** - * Presenter will be available after onActivityCreated(). - * - * @return The presenter associated with this fragment. - */ - public T getPresenter() { - return mPresenter; - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - mPresenter.onUiReady(getUi()); - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - if (savedInstanceState != null) { - mPresenter.onRestoreInstanceState(savedInstanceState); - if (savedInstanceState.getBoolean(KEY_FRAGMENT_HIDDEN)) { - getFragmentManager().beginTransaction().hide(this).commit(); - } - } - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - mPresenter.onUiDestroy(getUi()); - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - mPresenter.onSaveInstanceState(outState); - outState.putBoolean(KEY_FRAGMENT_HIDDEN, isHidden()); - } - - @Override - public void onAttach(Activity activity) { - super.onAttach(activity); - ((FragmentDisplayManager) activity).onFragmentAttached(this); - } -} diff --git a/InCallUI/src/com/android/incallui/Call.java b/InCallUI/src/com/android/incallui/Call.java deleted file mode 100644 index 1ad37e01a..000000000 --- a/InCallUI/src/com/android/incallui/Call.java +++ /dev/null @@ -1,1023 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui; - -import android.content.Context; -import android.hardware.camera2.CameraCharacteristics; -import android.net.Uri; -import android.os.Bundle; -import android.os.Trace; -import android.support.annotation.IntDef; -import android.telecom.Call.Details; -import android.telecom.Connection; -import android.telecom.DisconnectCause; -import android.telecom.GatewayInfo; -import android.telecom.InCallService.VideoCall; -import android.telecom.PhoneAccount; -import android.telecom.PhoneAccountHandle; -import android.telecom.TelecomManager; -import android.telecom.VideoProfile; -import android.text.TextUtils; - -import com.android.contacts.common.CallUtil; -import com.android.contacts.common.compat.CallSdkCompat; -import com.android.contacts.common.compat.CompatUtils; -import com.android.contacts.common.compat.SdkVersionOverride; -import com.android.contacts.common.compat.telecom.TelecomManagerCompat; -import com.android.contacts.common.testing.NeededForTesting; -import com.android.dialer.util.IntentUtil; -import com.android.incallui.util.TelecomCallUtil; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.Objects; - -/** - * Describes a single call and its state. - */ -@NeededForTesting -public class Call { - - /** - * Specifies whether a number is in the call history or not. - * {@link #CALL_HISTORY_STATUS_UNKNOWN} means there is no result. - */ - @IntDef({CALL_HISTORY_STATUS_UNKNOWN, CALL_HISTORY_STATUS_PRESENT, - CALL_HISTORY_STATUS_NOT_PRESENT}) - @Retention(RetentionPolicy.SOURCE) - public @interface CallHistoryStatus {} - public static final int CALL_HISTORY_STATUS_UNKNOWN = 0; - public static final int CALL_HISTORY_STATUS_PRESENT = 1; - public static final int CALL_HISTORY_STATUS_NOT_PRESENT = 2; - - /* Defines different states of this call */ - public static class State { - public static final int INVALID = 0; - public static final int NEW = 1; /* The call is new. */ - public static final int IDLE = 2; /* The call is idle. Nothing active */ - public static final int ACTIVE = 3; /* There is an active call */ - public static final int INCOMING = 4; /* A normal incoming phone call */ - public static final int CALL_WAITING = 5; /* Incoming call while another is active */ - public static final int DIALING = 6; /* An outgoing call during dial phase */ - public static final int REDIALING = 7; /* Subsequent dialing attempt after a failure */ - public static final int ONHOLD = 8; /* An active phone call placed on hold */ - public static final int DISCONNECTING = 9; /* A call is being ended. */ - public static final int DISCONNECTED = 10; /* State after a call disconnects */ - public static final int CONFERENCED = 11; /* Call part of a conference call */ - public static final int SELECT_PHONE_ACCOUNT = 12; /* Waiting for account selection */ - public static final int CONNECTING = 13; /* Waiting for Telecom broadcast to finish */ - public static final int BLOCKED = 14; /* The number was found on the block list */ - - - public static boolean isConnectingOrConnected(int state) { - switch(state) { - case ACTIVE: - case INCOMING: - case CALL_WAITING: - case CONNECTING: - case DIALING: - case REDIALING: - case ONHOLD: - case CONFERENCED: - return true; - default: - } - return false; - } - - public static boolean isDialing(int state) { - return state == DIALING || state == REDIALING; - } - - public static String toString(int state) { - switch (state) { - case INVALID: - return "INVALID"; - case NEW: - return "NEW"; - case IDLE: - return "IDLE"; - case ACTIVE: - return "ACTIVE"; - case INCOMING: - return "INCOMING"; - case CALL_WAITING: - return "CALL_WAITING"; - case DIALING: - return "DIALING"; - case REDIALING: - return "REDIALING"; - case ONHOLD: - return "ONHOLD"; - case DISCONNECTING: - return "DISCONNECTING"; - case DISCONNECTED: - return "DISCONNECTED"; - case CONFERENCED: - return "CONFERENCED"; - case SELECT_PHONE_ACCOUNT: - return "SELECT_PHONE_ACCOUNT"; - case CONNECTING: - return "CONNECTING"; - case BLOCKED: - return "BLOCKED"; - default: - return "UNKNOWN"; - } - } - } - - /** - * Defines different states of session modify requests, which are used to upgrade to video, or - * downgrade to audio. - */ - public static class SessionModificationState { - public static final int NO_REQUEST = 0; - public static final int WAITING_FOR_RESPONSE = 1; - public static final int REQUEST_FAILED = 2; - public static final int RECEIVED_UPGRADE_TO_VIDEO_REQUEST = 3; - public static final int UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT = 4; - public static final int REQUEST_REJECTED = 5; - } - - public static class VideoSettings { - public static final int CAMERA_DIRECTION_UNKNOWN = -1; - public static final int CAMERA_DIRECTION_FRONT_FACING = - CameraCharacteristics.LENS_FACING_FRONT; - public static final int CAMERA_DIRECTION_BACK_FACING = - CameraCharacteristics.LENS_FACING_BACK; - - private int mCameraDirection = CAMERA_DIRECTION_UNKNOWN; - - /** - * Sets the camera direction. if camera direction is set to CAMERA_DIRECTION_UNKNOWN, - * the video state of the call should be used to infer the camera direction. - * - * @see {@link CameraCharacteristics#LENS_FACING_FRONT} - * @see {@link CameraCharacteristics#LENS_FACING_BACK} - */ - public void setCameraDir(int cameraDirection) { - if (cameraDirection == CAMERA_DIRECTION_FRONT_FACING - || cameraDirection == CAMERA_DIRECTION_BACK_FACING) { - mCameraDirection = cameraDirection; - } else { - mCameraDirection = CAMERA_DIRECTION_UNKNOWN; - } - } - - /** - * Gets the camera direction. if camera direction is set to CAMERA_DIRECTION_UNKNOWN, - * the video state of the call should be used to infer the camera direction. - * - * @see {@link CameraCharacteristics#LENS_FACING_FRONT} - * @see {@link CameraCharacteristics#LENS_FACING_BACK} - */ - public int getCameraDir() { - return mCameraDirection; - } - - @Override - public String toString() { - return "(CameraDir:" + getCameraDir() + ")"; - } - } - - /** - * Tracks any state variables that is useful for logging. There is some amount of overlap with - * existing call member variables, but this duplication helps to ensure that none of these - * logging variables will interface with/and affect call logic. - */ - public static class LogState { - - // Contact lookup type constants - // Unknown lookup result (lookup not completed yet?) - public static final int LOOKUP_UNKNOWN = 0; - public static final int LOOKUP_NOT_FOUND = 1; - public static final int LOOKUP_LOCAL_CONTACT = 2; - public static final int LOOKUP_LOCAL_CACHE = 3; - public static final int LOOKUP_REMOTE_CONTACT = 4; - public static final int LOOKUP_EMERGENCY = 5; - public static final int LOOKUP_VOICEMAIL = 6; - - // Call initiation type constants - public static final int INITIATION_UNKNOWN = 0; - public static final int INITIATION_INCOMING = 1; - public static final int INITIATION_DIALPAD = 2; - public static final int INITIATION_SPEED_DIAL = 3; - public static final int INITIATION_REMOTE_DIRECTORY = 4; - public static final int INITIATION_SMART_DIAL = 5; - public static final int INITIATION_REGULAR_SEARCH = 6; - public static final int INITIATION_CALL_LOG = 7; - public static final int INITIATION_CALL_LOG_FILTER = 8; - public static final int INITIATION_VOICEMAIL_LOG = 9; - public static final int INITIATION_CALL_DETAILS = 10; - public static final int INITIATION_QUICK_CONTACTS = 11; - public static final int INITIATION_EXTERNAL = 12; - - public DisconnectCause disconnectCause; - public boolean isIncoming = false; - public int contactLookupResult = LOOKUP_UNKNOWN; - public int callInitiationMethod = INITIATION_EXTERNAL; - // If this was a conference call, the total number of calls involved in the conference. - public int conferencedCalls = 0; - public long duration = 0; - public boolean isLogged = false; - - @Override - public String toString() { - return String.format(Locale.US, "[" - + "%s, " // DisconnectCause toString already describes the object type - + "isIncoming: %s, " - + "contactLookup: %s, " - + "callInitiation: %s, " - + "duration: %s" - + "]", - disconnectCause, - isIncoming, - lookupToString(contactLookupResult), - initiationToString(callInitiationMethod), - duration); - } - - private static String lookupToString(int lookupType) { - switch (lookupType) { - case LOOKUP_LOCAL_CONTACT: - return "Local"; - case LOOKUP_LOCAL_CACHE: - return "Cache"; - case LOOKUP_REMOTE_CONTACT: - return "Remote"; - case LOOKUP_EMERGENCY: - return "Emergency"; - case LOOKUP_VOICEMAIL: - return "Voicemail"; - default: - return "Not found"; - } - } - - private static String initiationToString(int initiationType) { - switch (initiationType) { - case INITIATION_INCOMING: - return "Incoming"; - case INITIATION_DIALPAD: - return "Dialpad"; - case INITIATION_SPEED_DIAL: - return "Speed Dial"; - case INITIATION_REMOTE_DIRECTORY: - return "Remote Directory"; - case INITIATION_SMART_DIAL: - return "Smart Dial"; - case INITIATION_REGULAR_SEARCH: - return "Regular Search"; - case INITIATION_CALL_LOG: - return "Call Log"; - case INITIATION_CALL_LOG_FILTER: - return "Call Log Filter"; - case INITIATION_VOICEMAIL_LOG: - return "Voicemail Log"; - case INITIATION_CALL_DETAILS: - return "Call Details"; - case INITIATION_QUICK_CONTACTS: - return "Quick Contacts"; - default: - return "Unknown"; - } - } - } - - - private static final String ID_PREFIX = Call.class.getSimpleName() + "_"; - private static int sIdCounter = 0; - - private final android.telecom.Call.Callback mTelecomCallCallback = - new android.telecom.Call.Callback() { - @Override - public void onStateChanged(android.telecom.Call call, int newState) { - Log.d(this, "TelecomCallCallback onStateChanged call=" + call + " newState=" - + newState); - update(); - } - - @Override - public void onParentChanged(android.telecom.Call call, - android.telecom.Call newParent) { - Log.d(this, "TelecomCallCallback onParentChanged call=" + call + " newParent=" - + newParent); - update(); - } - - @Override - public void onChildrenChanged(android.telecom.Call call, - List children) { - update(); - } - - @Override - public void onDetailsChanged(android.telecom.Call call, - android.telecom.Call.Details details) { - Log.d(this, "TelecomCallCallback onStateChanged call=" + call + " details=" - + details); - update(); - } - - @Override - public void onCannedTextResponsesLoaded(android.telecom.Call call, - List cannedTextResponses) { - Log.d(this, "TelecomCallCallback onStateChanged call=" + call - + " cannedTextResponses=" + cannedTextResponses); - update(); - } - - @Override - public void onPostDialWait(android.telecom.Call call, - String remainingPostDialSequence) { - Log.d(this, "TelecomCallCallback onStateChanged call=" + call - + " remainingPostDialSequence=" + remainingPostDialSequence); - update(); - } - - @Override - public void onVideoCallChanged(android.telecom.Call call, - VideoCall videoCall) { - Log.d(this, "TelecomCallCallback onStateChanged call=" + call + " videoCall=" - + videoCall); - update(); - } - - @Override - public void onCallDestroyed(android.telecom.Call call) { - Log.d(this, "TelecomCallCallback onStateChanged call=" + call); - call.unregisterCallback(this); - } - - @Override - public void onConferenceableCallsChanged(android.telecom.Call call, - List conferenceableCalls) { - update(); - } - }; - - private final android.telecom.Call mTelecomCall; - private final LatencyReport mLatencyReport; - private boolean mIsEmergencyCall; - private Uri mHandle; - private final String mId; - private int mState = State.INVALID; - private DisconnectCause mDisconnectCause; - private int mSessionModificationState; - private final List mChildCallIds = new ArrayList<>(); - private final VideoSettings mVideoSettings = new VideoSettings(); - private int mVideoState; - - /** - * mRequestedVideoState is used to store requested upgrade / downgrade video state - */ - private int mRequestedVideoState = VideoProfile.STATE_AUDIO_ONLY; - - private InCallVideoCallCallback mVideoCallCallback; - private boolean mIsVideoCallCallbackRegistered; - private String mChildNumber; - private String mLastForwardedNumber; - private String mCallSubject; - private PhoneAccountHandle mPhoneAccountHandle; - @CallHistoryStatus private int mCallHistoryStatus = CALL_HISTORY_STATUS_UNKNOWN; - private boolean mIsSpam; - - /** - * Indicates whether the phone account associated with this call supports specifying a call - * subject. - */ - private boolean mIsCallSubjectSupported; - - private long mTimeAddedMs; - - private final LogState mLogState = new LogState(); - - /** - * Used only to create mock calls for testing - */ - @NeededForTesting - Call(int state) { - mTelecomCall = null; - mLatencyReport = new LatencyReport(); - mId = ID_PREFIX + Integer.toString(sIdCounter++); - setState(state); - } - - /** - * Creates a new instance of a {@link Call}. Registers a callback for - * {@link android.telecom.Call} events. - */ - public Call(android.telecom.Call telecomCall, LatencyReport latencyReport) { - this(telecomCall, latencyReport, true /* registerCallback */); - } - - /** - * Creates a new instance of a {@link Call}. Optionally registers a callback for - * {@link android.telecom.Call} events. - * - * Intended for use when creating a {@link Call} instance for use with the - * {@link ContactInfoCache}, where we do not want to register callbacks for the new call. - */ - public Call(android.telecom.Call telecomCall, LatencyReport latencyReport, - boolean registerCallback) { - mTelecomCall = telecomCall; - mLatencyReport = latencyReport; - mId = ID_PREFIX + Integer.toString(sIdCounter++); - - updateFromTelecomCall(registerCallback); - - if (registerCallback) { - mTelecomCall.registerCallback(mTelecomCallCallback); - } - - mTimeAddedMs = System.currentTimeMillis(); - } - - public android.telecom.Call getTelecomCall() { - return mTelecomCall; - } - - /** - * @return video settings of the call, null if the call is not a video call. - * @see VideoProfile - */ - public VideoSettings getVideoSettings() { - return mVideoSettings; - } - - private void update() { - Trace.beginSection("Update"); - int oldState = getState(); - // We want to potentially register a video call callback here. - updateFromTelecomCall(true /* registerCallback */); - if (oldState != getState() && getState() == Call.State.DISCONNECTED) { - CallList.getInstance().onDisconnect(this); - } else { - CallList.getInstance().onUpdate(this); - } - Trace.endSection(); - } - - private void updateFromTelecomCall(boolean registerCallback) { - Log.d(this, "updateFromTelecomCall: " + mTelecomCall.toString()); - final int translatedState = translateState(mTelecomCall.getState()); - if (mState != State.BLOCKED) { - setState(translatedState); - setDisconnectCause(mTelecomCall.getDetails().getDisconnectCause()); - maybeCancelVideoUpgrade(mTelecomCall.getDetails().getVideoState()); - } - - if (registerCallback && mTelecomCall.getVideoCall() != null) { - if (mVideoCallCallback == null) { - mVideoCallCallback = new InCallVideoCallCallback(this); - } - mTelecomCall.getVideoCall().registerCallback(mVideoCallCallback); - mIsVideoCallCallbackRegistered = true; - } - - mChildCallIds.clear(); - final int numChildCalls = mTelecomCall.getChildren().size(); - for (int i = 0; i < numChildCalls; i++) { - mChildCallIds.add( - CallList.getInstance().getCallByTelecomCall( - mTelecomCall.getChildren().get(i)).getId()); - } - - // The number of conferenced calls can change over the course of the call, so use the - // maximum number of conferenced child calls as the metric for conference call usage. - mLogState.conferencedCalls = Math.max(numChildCalls, mLogState.conferencedCalls); - - updateFromCallExtras(mTelecomCall.getDetails().getExtras()); - - // If the handle of the call has changed, update state for the call determining if it is an - // emergency call. - Uri newHandle = mTelecomCall.getDetails().getHandle(); - if (!Objects.equals(mHandle, newHandle)) { - mHandle = newHandle; - updateEmergencyCallState(); - } - - // If the phone account handle of the call is set, cache capability bit indicating whether - // the phone account supports call subjects. - PhoneAccountHandle newPhoneAccountHandle = mTelecomCall.getDetails().getAccountHandle(); - if (!Objects.equals(mPhoneAccountHandle, newPhoneAccountHandle)) { - mPhoneAccountHandle = newPhoneAccountHandle; - - if (mPhoneAccountHandle != null) { - TelecomManager mgr = InCallPresenter.getInstance().getTelecomManager(); - PhoneAccount phoneAccount = - TelecomManagerCompat.getPhoneAccount(mgr, mPhoneAccountHandle); - if (phoneAccount != null) { - mIsCallSubjectSupported = phoneAccount.hasCapabilities( - PhoneAccount.CAPABILITY_CALL_SUBJECT); - } - } - } - } - - /** - * Tests corruption of the {@code callExtras} bundle by calling {@link - * Bundle#containsKey(String)}. If the bundle is corrupted a {@link IllegalArgumentException} - * will be thrown and caught by this function. - * - * @param callExtras the bundle to verify - * @returns {@code true} if the bundle is corrupted, {@code false} otherwise. - */ - protected boolean areCallExtrasCorrupted(Bundle callExtras) { - /** - * There's currently a bug in Telephony service (b/25613098) that could corrupt the - * extras bundle, resulting in a IllegalArgumentException while validating data under - * {@link Bundle#containsKey(String)}. - */ - try { - callExtras.containsKey(Connection.EXTRA_CHILD_ADDRESS); - return false; - } catch (IllegalArgumentException e) { - Log.e(this, "CallExtras is corrupted, ignoring exception", e); - return true; - } - } - - protected void updateFromCallExtras(Bundle callExtras) { - if (callExtras == null || areCallExtrasCorrupted(callExtras)) { - /** - * If the bundle is corrupted, abandon information update as a work around. These are - * not critical for the dialer to function. - */ - return; - } - // Check for a change in the child address and notify any listeners. - if (callExtras.containsKey(Connection.EXTRA_CHILD_ADDRESS)) { - String childNumber = callExtras.getString(Connection.EXTRA_CHILD_ADDRESS); - if (!Objects.equals(childNumber, mChildNumber)) { - mChildNumber = childNumber; - CallList.getInstance().onChildNumberChange(this); - } - } - - // Last forwarded number comes in as an array of strings. We want to choose the - // last item in the array. The forwarding numbers arrive independently of when the - // call is originally set up, so we need to notify the the UI of the change. - if (callExtras.containsKey(Connection.EXTRA_LAST_FORWARDED_NUMBER)) { - ArrayList lastForwardedNumbers = - callExtras.getStringArrayList(Connection.EXTRA_LAST_FORWARDED_NUMBER); - - if (lastForwardedNumbers != null) { - String lastForwardedNumber = null; - if (!lastForwardedNumbers.isEmpty()) { - lastForwardedNumber = lastForwardedNumbers.get( - lastForwardedNumbers.size() - 1); - } - - if (!Objects.equals(lastForwardedNumber, mLastForwardedNumber)) { - mLastForwardedNumber = lastForwardedNumber; - CallList.getInstance().onLastForwardedNumberChange(this); - } - } - } - - // Call subject is present in the extras at the start of call, so we do not need to - // notify any other listeners of this. - if (callExtras.containsKey(Connection.EXTRA_CALL_SUBJECT)) { - String callSubject = callExtras.getString(Connection.EXTRA_CALL_SUBJECT); - if (!Objects.equals(mCallSubject, callSubject)) { - mCallSubject = callSubject; - } - } - } - - /** - * Determines if a received upgrade to video request should be cancelled. This can happen if - * another InCall UI responds to the upgrade to video request. - * - * @param newVideoState The new video state. - */ - private void maybeCancelVideoUpgrade(int newVideoState) { - boolean isVideoStateChanged = mVideoState != newVideoState; - - if (mSessionModificationState == SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST - && isVideoStateChanged) { - - Log.v(this, "maybeCancelVideoUpgrade : cancelling upgrade notification"); - setSessionModificationState(SessionModificationState.NO_REQUEST); - } - mVideoState = newVideoState; - } - private static int translateState(int state) { - switch (state) { - case android.telecom.Call.STATE_NEW: - case android.telecom.Call.STATE_CONNECTING: - return Call.State.CONNECTING; - case android.telecom.Call.STATE_SELECT_PHONE_ACCOUNT: - return Call.State.SELECT_PHONE_ACCOUNT; - case android.telecom.Call.STATE_DIALING: - return Call.State.DIALING; - case android.telecom.Call.STATE_RINGING: - return Call.State.INCOMING; - case android.telecom.Call.STATE_ACTIVE: - return Call.State.ACTIVE; - case android.telecom.Call.STATE_HOLDING: - return Call.State.ONHOLD; - case android.telecom.Call.STATE_DISCONNECTED: - return Call.State.DISCONNECTED; - case android.telecom.Call.STATE_DISCONNECTING: - return Call.State.DISCONNECTING; - default: - return Call.State.INVALID; - } - } - - public String getId() { - return mId; - } - - public long getTimeAddedMs() { - return mTimeAddedMs; - } - - public String getNumber() { - return TelecomCallUtil.getNumber(mTelecomCall); - } - - public void blockCall() { - mTelecomCall.reject(false, null); - setState(State.BLOCKED); - } - - public Uri getHandle() { - return mTelecomCall == null ? null : mTelecomCall.getDetails().getHandle(); - } - - public boolean isEmergencyCall() { - return mIsEmergencyCall; - } - - public int getState() { - if (mTelecomCall != null && mTelecomCall.getParent() != null) { - return State.CONFERENCED; - } else { - return mState; - } - } - - public void setState(int state) { - mState = state; - if (mState == State.INCOMING) { - mLogState.isIncoming = true; - } else if (mState == State.DISCONNECTED) { - mLogState.duration = getConnectTimeMillis() == 0 ? - 0: System.currentTimeMillis() - getConnectTimeMillis(); - } - } - - public int getNumberPresentation() { - return mTelecomCall == null ? null : mTelecomCall.getDetails().getHandlePresentation(); - } - - public int getCnapNamePresentation() { - return mTelecomCall == null ? null - : mTelecomCall.getDetails().getCallerDisplayNamePresentation(); - } - - public String getCnapName() { - return mTelecomCall == null ? null - : getTelecomCall().getDetails().getCallerDisplayName(); - } - - public Bundle getIntentExtras() { - return mTelecomCall.getDetails().getIntentExtras(); - } - - public Bundle getExtras() { - return mTelecomCall == null ? null : mTelecomCall.getDetails().getExtras(); - } - - /** - * @return The child number for the call, or {@code null} if none specified. - */ - public String getChildNumber() { - return mChildNumber; - } - - /** - * @return The last forwarded number for the call, or {@code null} if none specified. - */ - public String getLastForwardedNumber() { - return mLastForwardedNumber; - } - - /** - * @return The call subject, or {@code null} if none specified. - */ - public String getCallSubject() { - return mCallSubject; - } - - /** - * @return {@code true} if the call's phone account supports call subjects, {@code false} - * otherwise. - */ - public boolean isCallSubjectSupported() { - return mIsCallSubjectSupported; - } - - /** Returns call disconnect cause, defined by {@link DisconnectCause}. */ - public DisconnectCause getDisconnectCause() { - if (mState == State.DISCONNECTED || mState == State.IDLE) { - return mDisconnectCause; - } - - return new DisconnectCause(DisconnectCause.UNKNOWN); - } - - public void setDisconnectCause(DisconnectCause disconnectCause) { - mDisconnectCause = disconnectCause; - mLogState.disconnectCause = mDisconnectCause; - } - - /** Returns the possible text message responses. */ - public List getCannedSmsResponses() { - return mTelecomCall.getCannedTextResponses(); - } - - /** Checks if the call supports the given set of capabilities supplied as a bit mask. */ - public boolean can(int capabilities) { - int supportedCapabilities = mTelecomCall.getDetails().getCallCapabilities(); - - if ((capabilities & android.telecom.Call.Details.CAPABILITY_MERGE_CONFERENCE) != 0) { - // We allow you to merge if the capabilities allow it or if it is a call with - // conferenceable calls. - if (mTelecomCall.getConferenceableCalls().isEmpty() && - ((android.telecom.Call.Details.CAPABILITY_MERGE_CONFERENCE - & supportedCapabilities) == 0)) { - // Cannot merge calls if there are no calls to merge with. - return false; - } - capabilities &= ~android.telecom.Call.Details.CAPABILITY_MERGE_CONFERENCE; - } - return (capabilities == (capabilities & mTelecomCall.getDetails().getCallCapabilities())); - } - - public boolean hasProperty(int property) { - return mTelecomCall.getDetails().hasProperty(property); - } - - /** Gets the time when the call first became active. */ - public long getConnectTimeMillis() { - return mTelecomCall.getDetails().getConnectTimeMillis(); - } - - public boolean isConferenceCall() { - return hasProperty(android.telecom.Call.Details.PROPERTY_CONFERENCE); - } - - public GatewayInfo getGatewayInfo() { - return mTelecomCall == null ? null : mTelecomCall.getDetails().getGatewayInfo(); - } - - public PhoneAccountHandle getAccountHandle() { - return mTelecomCall == null ? null : mTelecomCall.getDetails().getAccountHandle(); - } - - /** - * @return The {@link VideoCall} instance associated with the {@link android.telecom.Call}. - * Will return {@code null} until {@link #updateFromTelecomCall()} has registered a valid - * callback on the {@link VideoCall}. - */ - public VideoCall getVideoCall() { - return mTelecomCall == null || !mIsVideoCallCallbackRegistered ? null - : mTelecomCall.getVideoCall(); - } - - public List getChildCallIds() { - return mChildCallIds; - } - - public String getParentId() { - android.telecom.Call parentCall = mTelecomCall.getParent(); - if (parentCall != null) { - return CallList.getInstance().getCallByTelecomCall(parentCall).getId(); - } - return null; - } - - public int getVideoState() { - return mTelecomCall.getDetails().getVideoState(); - } - - public boolean isVideoCall(Context context) { - return CallUtil.isVideoEnabled(context) && - VideoUtils.isVideoCall(getVideoState()); - } - - /** - * Handles incoming session modification requests. Stores the pending video request and sets - * the session modification state to - * {@link SessionModificationState#RECEIVED_UPGRADE_TO_VIDEO_REQUEST} so that we can keep track - * of the fact the request was received. Only upgrade requests require user confirmation and - * will be handled by this method. The remote user can turn off their own camera without - * confirmation. - * - * @param videoState The requested video state. - */ - public void setRequestedVideoState(int videoState) { - Log.d(this, "setRequestedVideoState - video state= " + videoState); - if (videoState == getVideoState()) { - mSessionModificationState = Call.SessionModificationState.NO_REQUEST; - Log.w(this,"setRequestedVideoState - Clearing session modification state"); - return; - } - - mSessionModificationState = Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST; - mRequestedVideoState = videoState; - CallList.getInstance().onUpgradeToVideo(this); - - Log.d(this, "setRequestedVideoState - mSessionModificationState=" - + mSessionModificationState + " video state= " + videoState); - update(); - } - - /** - * Set the session modification state. Used to keep track of pending video session modification - * operations and to inform listeners of these changes. - * - * @param state the new session modification state. - */ - public void setSessionModificationState(int state) { - boolean hasChanged = mSessionModificationState != state; - mSessionModificationState = state; - Log.d(this, "setSessionModificationState " + state + " mSessionModificationState=" - + mSessionModificationState); - if (hasChanged) { - CallList.getInstance().onSessionModificationStateChange(this, state); - } - } - - /** - * Determines if the call handle is an emergency number or not and caches the result to avoid - * repeated calls to isEmergencyNumber. - */ - private void updateEmergencyCallState() { - mIsEmergencyCall = TelecomCallUtil.isEmergencyCall(mTelecomCall); - } - - /** - * Gets the video state which was requested via a session modification request. - * - * @return The video state. - */ - public int getRequestedVideoState() { - return mRequestedVideoState; - } - - public static boolean areSame(Call call1, Call call2) { - if (call1 == null && call2 == null) { - return true; - } else if (call1 == null || call2 == null) { - return false; - } - - // otherwise compare call Ids - return call1.getId().equals(call2.getId()); - } - - public static boolean areSameNumber(Call call1, Call call2) { - if (call1 == null && call2 == null) { - return true; - } else if (call1 == null || call2 == null) { - return false; - } - - // otherwise compare call Numbers - return TextUtils.equals(call1.getNumber(), call2.getNumber()); - } - - /** - * Gets the current video session modification state. - * - * @return The session modification state. - */ - public int getSessionModificationState() { - return mSessionModificationState; - } - - public LogState getLogState() { - return mLogState; - } - - /** - * Determines if the call is an external call. - * - * An external call is one which does not exist locally for the - * {@link android.telecom.ConnectionService} it is associated with. - * - * External calls are only supported in N and higher. - * - * @return {@code true} if the call is an external call, {@code false} otherwise. - */ - public boolean isExternalCall() { - return CompatUtils.isNCompatible() && - hasProperty(CallSdkCompat.Details.PROPERTY_IS_EXTERNAL_CALL); - } - - /** - * Determines if the external call is pullable. - * - * An external call is one which does not exist locally for the - * {@link android.telecom.ConnectionService} it is associated with. An external call may be - * "pullable", which means that the user can request it be transferred to the current device. - * - * External calls are only supported in N and higher. - * - * @return {@code true} if the call is an external call, {@code false} otherwise. - */ - public boolean isPullableExternalCall() { - return CompatUtils.isNCompatible() && - (mTelecomCall.getDetails().getCallCapabilities() - & CallSdkCompat.Details.CAPABILITY_CAN_PULL_CALL) - == CallSdkCompat.Details.CAPABILITY_CAN_PULL_CALL; - } - - /** - * Logging utility methods - */ - public void logCallInitiationType() { - if (isExternalCall()) { - return; - } - - if (getState() == State.INCOMING) { - getLogState().callInitiationMethod = LogState.INITIATION_INCOMING; - } else if (getIntentExtras() != null) { - getLogState().callInitiationMethod = - getIntentExtras().getInt(IntentUtil.EXTRA_CALL_INITIATION_TYPE, - LogState.INITIATION_EXTERNAL); - } - } - - @Override - public String toString() { - if (mTelecomCall == null) { - // This should happen only in testing since otherwise we would never have a null - // Telecom call. - return String.valueOf(mId); - } - - return String.format(Locale.US, "[%s, %s, %s, %s, children:%s, parent:%s, " + - "conferenceable:%s, videoState:%s, mSessionModificationState:%d, VideoSettings:%s]", - mId, - State.toString(getState()), - Details.capabilitiesToString(mTelecomCall.getDetails().getCallCapabilities()), - Details.propertiesToString(mTelecomCall.getDetails().getCallProperties()), - mChildCallIds, - getParentId(), - this.mTelecomCall.getConferenceableCalls(), - VideoProfile.videoStateToString(mTelecomCall.getDetails().getVideoState()), - mSessionModificationState, - getVideoSettings()); - } - - public String toSimpleString() { - return super.toString(); - } - - public void setCallHistoryStatus(@CallHistoryStatus int callHistoryStatus) { - mCallHistoryStatus = callHistoryStatus; - } - - @CallHistoryStatus - public int getCallHistoryStatus() { - return mCallHistoryStatus; - } - - public void setSpam(boolean isSpam) { - mIsSpam = isSpam; - } - - public boolean isSpam() { - return mIsSpam; - } - - public LatencyReport getLatencyReport() { - return mLatencyReport; - } -} diff --git a/InCallUI/src/com/android/incallui/CallButtonFragment.java b/InCallUI/src/com/android/incallui/CallButtonFragment.java deleted file mode 100644 index 6b633eaf3..000000000 --- a/InCallUI/src/com/android/incallui/CallButtonFragment.java +++ /dev/null @@ -1,819 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_ADD_CALL; -import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_AUDIO; -import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_COUNT; -import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_DIALPAD; -import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_DOWNGRADE_TO_AUDIO; -import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_HOLD; -import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_MANAGE_VIDEO_CONFERENCE; -import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_MERGE; -import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_MUTE; -import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_PAUSE_VIDEO; -import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_SWAP; -import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_SWITCH_CAMERA; -import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_UPGRADE_TO_VIDEO; - -import android.content.Context; -import android.content.res.ColorStateList; -import android.content.res.Resources; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.GradientDrawable; -import android.graphics.drawable.LayerDrawable; -import android.graphics.drawable.RippleDrawable; -import android.graphics.drawable.StateListDrawable; -import android.os.Bundle; -import android.telecom.CallAudioState; -import android.util.SparseIntArray; -import android.view.ContextThemeWrapper; -import android.view.HapticFeedbackConstants; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.CompoundButton; -import android.widget.ImageButton; -import android.widget.PopupMenu; -import android.widget.PopupMenu.OnDismissListener; -import android.widget.PopupMenu.OnMenuItemClickListener; - -import com.android.contacts.common.util.MaterialColorMapUtils.MaterialPalette; -import com.android.dialer.R; - -/** - * Fragment for call control buttons - */ -public class CallButtonFragment - extends BaseFragment - implements CallButtonPresenter.CallButtonUi, OnMenuItemClickListener, OnDismissListener, - View.OnClickListener { - - private int mButtonMaxVisible; - // The button is currently visible in the UI - private static final int BUTTON_VISIBLE = 1; - // The button is hidden in the UI - private static final int BUTTON_HIDDEN = 2; - // The button has been collapsed into the overflow menu - private static final int BUTTON_MENU = 3; - - public interface Buttons { - - public static final int BUTTON_AUDIO = 0; - public static final int BUTTON_MUTE = 1; - public static final int BUTTON_DIALPAD = 2; - public static final int BUTTON_HOLD = 3; - public static final int BUTTON_SWAP = 4; - public static final int BUTTON_UPGRADE_TO_VIDEO = 5; - public static final int BUTTON_SWITCH_CAMERA = 6; - public static final int BUTTON_DOWNGRADE_TO_AUDIO = 7; - public static final int BUTTON_ADD_CALL = 8; - public static final int BUTTON_MERGE = 9; - public static final int BUTTON_PAUSE_VIDEO = 10; - public static final int BUTTON_MANAGE_VIDEO_CONFERENCE = 11; - public static final int BUTTON_COUNT = 12; - } - - private SparseIntArray mButtonVisibilityMap = new SparseIntArray(BUTTON_COUNT); - - private CompoundButton mAudioButton; - private CompoundButton mMuteButton; - private CompoundButton mShowDialpadButton; - private CompoundButton mHoldButton; - private ImageButton mSwapButton; - private ImageButton mChangeToVideoButton; - private ImageButton mChangeToVoiceButton; - private CompoundButton mSwitchCameraButton; - private ImageButton mAddCallButton; - private ImageButton mMergeButton; - private CompoundButton mPauseVideoButton; - private ImageButton mOverflowButton; - private ImageButton mManageVideoCallConferenceButton; - - private PopupMenu mAudioModePopup; - private boolean mAudioModePopupVisible; - private PopupMenu mOverflowPopup; - - private int mPrevAudioMode = 0; - - // Constants for Drawable.setAlpha() - private static final int HIDDEN = 0; - private static final int VISIBLE = 255; - - private boolean mIsEnabled; - private MaterialPalette mCurrentThemeColors; - - @Override - public CallButtonPresenter createPresenter() { - // TODO: find a cleaner way to include audio mode provider than having a singleton instance. - return new CallButtonPresenter(); - } - - @Override - public CallButtonPresenter.CallButtonUi getUi() { - return this; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - for (int i = 0; i < BUTTON_COUNT; i++) { - mButtonVisibilityMap.put(i, BUTTON_HIDDEN); - } - - mButtonMaxVisible = getResources().getInteger(R.integer.call_card_max_buttons); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - final View parent = inflater.inflate(R.layout.call_button_fragment, container, false); - - mAudioButton = (CompoundButton) parent.findViewById(R.id.audioButton); - mAudioButton.setOnClickListener(this); - mMuteButton = (CompoundButton) parent.findViewById(R.id.muteButton); - mMuteButton.setOnClickListener(this); - mShowDialpadButton = (CompoundButton) parent.findViewById(R.id.dialpadButton); - mShowDialpadButton.setOnClickListener(this); - mHoldButton = (CompoundButton) parent.findViewById(R.id.holdButton); - mHoldButton.setOnClickListener(this); - mSwapButton = (ImageButton) parent.findViewById(R.id.swapButton); - mSwapButton.setOnClickListener(this); - mChangeToVideoButton = (ImageButton) parent.findViewById(R.id.changeToVideoButton); - mChangeToVideoButton.setOnClickListener(this); - mChangeToVoiceButton = (ImageButton) parent.findViewById(R.id.changeToVoiceButton); - mChangeToVoiceButton.setOnClickListener(this); - mSwitchCameraButton = (CompoundButton) parent.findViewById(R.id.switchCameraButton); - mSwitchCameraButton.setOnClickListener(this); - mAddCallButton = (ImageButton) parent.findViewById(R.id.addButton); - mAddCallButton.setOnClickListener(this); - mMergeButton = (ImageButton) parent.findViewById(R.id.mergeButton); - mMergeButton.setOnClickListener(this); - mPauseVideoButton = (CompoundButton) parent.findViewById(R.id.pauseVideoButton); - mPauseVideoButton.setOnClickListener(this); - mOverflowButton = (ImageButton) parent.findViewById(R.id.overflowButton); - mOverflowButton.setOnClickListener(this); - mManageVideoCallConferenceButton = (ImageButton) parent.findViewById( - R.id.manageVideoCallConferenceButton); - mManageVideoCallConferenceButton.setOnClickListener(this); - return parent; - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - - // set the buttons - updateAudioButtons(); - } - - @Override - public void onResume() { - if (getPresenter() != null) { - getPresenter().refreshMuteState(); - } - super.onResume(); - - updateColors(); - } - - @Override - public void onClick(View view) { - int id = view.getId(); - Log.d(this, "onClick(View " + view + ", id " + id + ")..."); - - if (id == R.id.audioButton) { - onAudioButtonClicked(); - } else if (id == R.id.addButton) { - getPresenter().addCallClicked(); - } else if (id == R.id.muteButton) { - getPresenter().muteClicked(!mMuteButton.isSelected()); - } else if (id == R.id.mergeButton) { - getPresenter().mergeClicked(); - mMergeButton.setEnabled(false); - } else if (id == R.id.holdButton) { - getPresenter().holdClicked(!mHoldButton.isSelected()); - } else if (id == R.id.swapButton) { - getPresenter().swapClicked(); - } else if (id == R.id.dialpadButton) { - getPresenter().showDialpadClicked(!mShowDialpadButton.isSelected()); - } else if (id == R.id.changeToVideoButton) { - getPresenter().changeToVideoClicked(); - } else if (id == R.id.changeToVoiceButton) { - getPresenter().changeToVoiceClicked(); - } else if (id == R.id.switchCameraButton) { - getPresenter().switchCameraClicked( - mSwitchCameraButton.isSelected() /* useFrontFacingCamera */); - } else if (id == R.id.pauseVideoButton) { - getPresenter().pauseVideoClicked( - !mPauseVideoButton.isSelected() /* pause */); - } else if (id == R.id.overflowButton) { - if (mOverflowPopup != null) { - mOverflowPopup.show(); - } - } else if (id == R.id.manageVideoCallConferenceButton) { - onManageVideoCallConferenceClicked(); - } else { - Log.wtf(this, "onClick: unexpected"); - return; - } - - view.performHapticFeedback( - HapticFeedbackConstants.VIRTUAL_KEY, - HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING); - } - - public void updateColors() { - MaterialPalette themeColors = InCallPresenter.getInstance().getThemeColors(); - - if (mCurrentThemeColors != null && mCurrentThemeColors.equals(themeColors)) { - return; - } - - View[] compoundButtons = { - mAudioButton, - mMuteButton, - mShowDialpadButton, - mHoldButton, - mSwitchCameraButton, - mPauseVideoButton - }; - - for (View button : compoundButtons) { - final LayerDrawable layers = (LayerDrawable) button.getBackground(); - final RippleDrawable btnCompoundDrawable = compoundBackgroundDrawable(themeColors); - layers.setDrawableByLayerId(R.id.compoundBackgroundItem, btnCompoundDrawable); - } - - ImageButton[] normalButtons = { - mSwapButton, - mChangeToVideoButton, - mChangeToVoiceButton, - mAddCallButton, - mMergeButton, - mOverflowButton - }; - - for (ImageButton button : normalButtons) { - final LayerDrawable layers = (LayerDrawable) button.getBackground(); - final RippleDrawable btnDrawable = backgroundDrawable(themeColors); - layers.setDrawableByLayerId(R.id.backgroundItem, btnDrawable); - } - - mCurrentThemeColors = themeColors; - } - - /** - * Generate a RippleDrawable which will be the background for a compound button, i.e. - * a button with pressed and unpressed states. The unpressed state will be the same color - * as the rest of the call card, the pressed state will be the dark version of that color. - */ - private RippleDrawable compoundBackgroundDrawable(MaterialPalette palette) { - Resources res = getResources(); - ColorStateList rippleColor = - ColorStateList.valueOf(res.getColor(R.color.incall_accent_color)); - - StateListDrawable stateListDrawable = new StateListDrawable(); - addSelectedAndFocused(res, stateListDrawable); - addFocused(res, stateListDrawable); - addSelected(res, stateListDrawable, palette); - addUnselected(res, stateListDrawable, palette); - - return new RippleDrawable(rippleColor, stateListDrawable, null); - } - - /** - * Generate a RippleDrawable which will be the background of a button to ensure it - * is the same color as the rest of the call card. - */ - private RippleDrawable backgroundDrawable(MaterialPalette palette) { - Resources res = getResources(); - ColorStateList rippleColor = - ColorStateList.valueOf(res.getColor(R.color.incall_accent_color)); - - StateListDrawable stateListDrawable = new StateListDrawable(); - addFocused(res, stateListDrawable); - addUnselected(res, stateListDrawable, palette); - - return new RippleDrawable(rippleColor, stateListDrawable, null); - } - - // state_selected and state_focused - private void addSelectedAndFocused(Resources res, StateListDrawable drawable) { - int[] selectedAndFocused = {android.R.attr.state_selected, android.R.attr.state_focused}; - Drawable selectedAndFocusedDrawable = res.getDrawable(R.drawable.btn_selected_focused); - drawable.addState(selectedAndFocused, selectedAndFocusedDrawable); - } - - // state_focused - private void addFocused(Resources res, StateListDrawable drawable) { - int[] focused = {android.R.attr.state_focused}; - Drawable focusedDrawable = res.getDrawable(R.drawable.btn_unselected_focused); - drawable.addState(focused, focusedDrawable); - } - - // state_selected - private void addSelected(Resources res, StateListDrawable drawable, MaterialPalette palette) { - int[] selected = {android.R.attr.state_selected}; - LayerDrawable selectedDrawable = (LayerDrawable) res.getDrawable(R.drawable.btn_selected); - ((GradientDrawable) selectedDrawable.getDrawable(0)).setColor(palette.mSecondaryColor); - drawable.addState(selected, selectedDrawable); - } - - // default - private void addUnselected(Resources res, StateListDrawable drawable, MaterialPalette palette) { - LayerDrawable unselectedDrawable = - (LayerDrawable) res.getDrawable(R.drawable.btn_unselected); - ((GradientDrawable) unselectedDrawable.getDrawable(0)).setColor(palette.mPrimaryColor); - drawable.addState(new int[0], unselectedDrawable); - } - - @Override - public void setEnabled(boolean isEnabled) { - mIsEnabled = isEnabled; - - mAudioButton.setEnabled(isEnabled); - mMuteButton.setEnabled(isEnabled); - mShowDialpadButton.setEnabled(isEnabled); - mHoldButton.setEnabled(isEnabled); - mSwapButton.setEnabled(isEnabled); - mChangeToVideoButton.setEnabled(isEnabled); - mChangeToVoiceButton.setEnabled(isEnabled); - mSwitchCameraButton.setEnabled(isEnabled); - mAddCallButton.setEnabled(isEnabled); - mMergeButton.setEnabled(isEnabled); - mPauseVideoButton.setEnabled(isEnabled); - mOverflowButton.setEnabled(isEnabled); - mManageVideoCallConferenceButton.setEnabled(isEnabled); - } - - @Override - public void showButton(int buttonId, boolean show) { - mButtonVisibilityMap.put(buttonId, show ? BUTTON_VISIBLE : BUTTON_HIDDEN); - } - - @Override - public void enableButton(int buttonId, boolean enable) { - final View button = getButtonById(buttonId); - if (button != null) { - button.setEnabled(enable); - } - } - - private View getButtonById(int id) { - if (id == BUTTON_AUDIO) { - return mAudioButton; - } else if (id == BUTTON_MUTE) { - return mMuteButton; - } else if (id == BUTTON_DIALPAD) { - return mShowDialpadButton; - } else if (id == BUTTON_HOLD) { - return mHoldButton; - } else if (id == BUTTON_SWAP) { - return mSwapButton; - } else if (id == BUTTON_UPGRADE_TO_VIDEO) { - return mChangeToVideoButton; - } else if (id == BUTTON_DOWNGRADE_TO_AUDIO) { - return mChangeToVoiceButton; - } else if (id == BUTTON_SWITCH_CAMERA) { - return mSwitchCameraButton; - } else if (id == BUTTON_ADD_CALL) { - return mAddCallButton; - } else if (id == BUTTON_MERGE) { - return mMergeButton; - } else if (id == BUTTON_PAUSE_VIDEO) { - return mPauseVideoButton; - } else if (id == BUTTON_MANAGE_VIDEO_CONFERENCE) { - return mManageVideoCallConferenceButton; - } else { - Log.w(this, "Invalid button id"); - return null; - } - } - - @Override - public void setHold(boolean value) { - if (mHoldButton.isSelected() != value) { - mHoldButton.setSelected(value); - mHoldButton.setContentDescription(getContext().getString( - value ? R.string.onscreenHoldText_selected - : R.string.onscreenHoldText_unselected)); - } - } - - @Override - public void setCameraSwitched(boolean isBackFacingCamera) { - mSwitchCameraButton.setSelected(isBackFacingCamera); - } - - @Override - public void setVideoPaused(boolean isVideoPaused) { - mPauseVideoButton.setSelected(isVideoPaused); - - if (isVideoPaused) { - mPauseVideoButton.setContentDescription(getText(R.string.onscreenTurnOnCameraText)); - } else { - mPauseVideoButton.setContentDescription(getText(R.string.onscreenTurnOffCameraText)); - } - } - - @Override - public void setMute(boolean value) { - if (mMuteButton.isSelected() != value) { - mMuteButton.setSelected(value); - mMuteButton.setContentDescription(getContext().getString( - value ? R.string.onscreenMuteText_selected - : R.string.onscreenMuteText_unselected)); - } - } - - private void addToOverflowMenu(int id, View button, PopupMenu menu) { - button.setVisibility(View.GONE); - menu.getMenu().add(Menu.NONE, id, Menu.NONE, button.getContentDescription()); - mButtonVisibilityMap.put(id, BUTTON_MENU); - } - - private PopupMenu getPopupMenu() { - return new PopupMenu(new ContextThemeWrapper(getActivity(), R.style.InCallPopupMenuStyle), - mOverflowButton); - } - - /** - * Iterates through the list of buttons and toggles their visibility depending on the - * setting configured by the CallButtonPresenter. If there are more visible buttons than - * the allowed maximum, the excess buttons are collapsed into a single overflow menu. - */ - @Override - public void updateButtonStates() { - View prevVisibleButton = null; - int prevVisibleId = -1; - PopupMenu menu = null; - int visibleCount = 0; - for (int i = 0; i < BUTTON_COUNT; i++) { - final int visibility = mButtonVisibilityMap.get(i); - final View button = getButtonById(i); - if (visibility == BUTTON_VISIBLE) { - visibleCount++; - if (visibleCount <= mButtonMaxVisible) { - button.setVisibility(View.VISIBLE); - prevVisibleButton = button; - prevVisibleId = i; - } else { - if (menu == null) { - menu = getPopupMenu(); - } - // Collapse the current button into the overflow menu. If is the first visible - // button that exceeds the threshold, also collapse the previous visible button - // so that the total number of visible buttons will never exceed the threshold. - if (prevVisibleButton != null) { - addToOverflowMenu(prevVisibleId, prevVisibleButton, menu); - prevVisibleButton = null; - prevVisibleId = -1; - } - addToOverflowMenu(i, button, menu); - } - } else if (visibility == BUTTON_HIDDEN) { - button.setVisibility(View.GONE); - } - } - - mOverflowButton.setVisibility(menu != null ? View.VISIBLE : View.GONE); - if (menu != null) { - mOverflowPopup = menu; - mOverflowPopup.setOnMenuItemClickListener(new OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem item) { - final int id = item.getItemId(); - getButtonById(id).performClick(); - return true; - } - }); - } - } - - @Override - public void setAudio(int mode) { - updateAudioButtons(); - refreshAudioModePopup(); - - if (mPrevAudioMode != mode) { - updateAudioButtonContentDescription(mode); - mPrevAudioMode = mode; - } - } - - @Override - public void setSupportedAudio(int modeMask) { - updateAudioButtons(); - refreshAudioModePopup(); - } - - @Override - public boolean onMenuItemClick(MenuItem item) { - Log.d(this, "- onMenuItemClick: " + item); - Log.d(this, " id: " + item.getItemId()); - Log.d(this, " title: '" + item.getTitle() + "'"); - - int mode = CallAudioState.ROUTE_WIRED_OR_EARPIECE; - int resId = item.getItemId(); - - if (resId == R.id.audio_mode_speaker) { - mode = CallAudioState.ROUTE_SPEAKER; - } else if (resId == R.id.audio_mode_earpiece || resId == R.id.audio_mode_wired_headset) { - // InCallCallAudioState.ROUTE_EARPIECE means either the handset earpiece, - // or the wired headset (if connected.) - mode = CallAudioState.ROUTE_WIRED_OR_EARPIECE; - } else if (resId == R.id.audio_mode_bluetooth) { - mode = CallAudioState.ROUTE_BLUETOOTH; - } else { - Log.e(this, "onMenuItemClick: unexpected View ID " + item.getItemId() - + " (MenuItem = '" + item + "')"); - } - - getPresenter().setAudioMode(mode); - - return true; - } - - // PopupMenu.OnDismissListener implementation; see showAudioModePopup(). - // This gets called when the PopupMenu gets dismissed for *any* reason, like - // the user tapping outside its bounds, or pressing Back, or selecting one - // of the menu items. - @Override - public void onDismiss(PopupMenu menu) { - Log.d(this, "- onDismiss: " + menu); - mAudioModePopupVisible = false; - updateAudioButtons(); - } - - /** - * Checks for supporting modes. If bluetooth is supported, it uses the audio - * pop up menu. Otherwise, it toggles the speakerphone. - */ - private void onAudioButtonClicked() { - Log.d(this, "onAudioButtonClicked: " + - CallAudioState.audioRouteToString(getPresenter().getSupportedAudio())); - - if (isSupported(CallAudioState.ROUTE_BLUETOOTH)) { - showAudioModePopup(); - } else { - getPresenter().toggleSpeakerphone(); - } - } - - private void onManageVideoCallConferenceClicked() { - Log.d(this, "onManageVideoCallConferenceClicked"); - InCallPresenter.getInstance().showConferenceCallManager(true); - } - - /** - * Refreshes the "Audio mode" popup if it's visible. This is useful - * (for example) when a wired headset is plugged or unplugged, - * since we need to switch back and forth between the "earpiece" - * and "wired headset" items. - * - * This is safe to call even if the popup is already dismissed, or even if - * you never called showAudioModePopup() in the first place. - */ - public void refreshAudioModePopup() { - if (mAudioModePopup != null && mAudioModePopupVisible) { - // Dismiss the previous one - mAudioModePopup.dismiss(); // safe even if already dismissed - // And bring up a fresh PopupMenu - showAudioModePopup(); - } - } - - /** - * Updates the audio button so that the appriopriate visual layers - * are visible based on the supported audio formats. - */ - private void updateAudioButtons() { - final boolean bluetoothSupported = isSupported(CallAudioState.ROUTE_BLUETOOTH); - final boolean speakerSupported = isSupported(CallAudioState.ROUTE_SPEAKER); - - boolean audioButtonEnabled = false; - boolean audioButtonChecked = false; - boolean showMoreIndicator = false; - - boolean showBluetoothIcon = false; - boolean showSpeakerphoneIcon = false; - boolean showHandsetIcon = false; - - boolean showToggleIndicator = false; - - if (bluetoothSupported) { - Log.d(this, "updateAudioButtons - popup menu mode"); - - audioButtonEnabled = true; - audioButtonChecked = true; - showMoreIndicator = true; - - // Update desired layers: - if (isAudio(CallAudioState.ROUTE_BLUETOOTH)) { - showBluetoothIcon = true; - } else if (isAudio(CallAudioState.ROUTE_SPEAKER)) { - showSpeakerphoneIcon = true; - } else { - showHandsetIcon = true; - // TODO: if a wired headset is plugged in, that takes precedence - // over the handset earpiece. If so, maybe we should show some - // sort of "wired headset" icon here instead of the "handset - // earpiece" icon. (Still need an asset for that, though.) - } - - // The audio button is NOT a toggle in this state, so set selected to false. - mAudioButton.setSelected(false); - } else if (speakerSupported) { - Log.d(this, "updateAudioButtons - speaker toggle mode"); - - audioButtonEnabled = true; - - // The audio button *is* a toggle in this state, and indicated the - // current state of the speakerphone. - audioButtonChecked = isAudio(CallAudioState.ROUTE_SPEAKER); - mAudioButton.setSelected(audioButtonChecked); - - // update desired layers: - showToggleIndicator = true; - showSpeakerphoneIcon = true; - } else { - Log.d(this, "updateAudioButtons - disabled..."); - - // The audio button is a toggle in this state, but that's mostly - // irrelevant since it's always disabled and unchecked. - audioButtonEnabled = false; - audioButtonChecked = false; - mAudioButton.setSelected(false); - - // update desired layers: - showToggleIndicator = true; - showSpeakerphoneIcon = true; - } - - // Finally, update it all! - - Log.v(this, "audioButtonEnabled: " + audioButtonEnabled); - Log.v(this, "audioButtonChecked: " + audioButtonChecked); - Log.v(this, "showMoreIndicator: " + showMoreIndicator); - Log.v(this, "showBluetoothIcon: " + showBluetoothIcon); - Log.v(this, "showSpeakerphoneIcon: " + showSpeakerphoneIcon); - Log.v(this, "showHandsetIcon: " + showHandsetIcon); - - // Only enable the audio button if the fragment is enabled. - mAudioButton.setEnabled(audioButtonEnabled && mIsEnabled); - mAudioButton.setChecked(audioButtonChecked); - - final LayerDrawable layers = (LayerDrawable) mAudioButton.getBackground(); - Log.d(this, "'layers' drawable: " + layers); - - layers.findDrawableByLayerId(R.id.compoundBackgroundItem) - .setAlpha(showToggleIndicator ? VISIBLE : HIDDEN); - - layers.findDrawableByLayerId(R.id.moreIndicatorItem) - .setAlpha(showMoreIndicator ? VISIBLE : HIDDEN); - - layers.findDrawableByLayerId(R.id.bluetoothItem) - .setAlpha(showBluetoothIcon ? VISIBLE : HIDDEN); - - layers.findDrawableByLayerId(R.id.handsetItem) - .setAlpha(showHandsetIcon ? VISIBLE : HIDDEN); - - layers.findDrawableByLayerId(R.id.speakerphoneItem) - .setAlpha(showSpeakerphoneIcon ? VISIBLE : HIDDEN); - - } - - /** - * Update the content description of the audio button. - */ - private void updateAudioButtonContentDescription(int mode) { - int stringId = 0; - - // If bluetooth is not supported, the audio buttion will toggle, so use the label "speaker". - // Otherwise, use the label of the currently selected audio mode. - if (!isSupported(CallAudioState.ROUTE_BLUETOOTH)) { - stringId = R.string.audio_mode_speaker; - } else { - switch (mode) { - case CallAudioState.ROUTE_EARPIECE: - stringId = R.string.audio_mode_earpiece; - break; - case CallAudioState.ROUTE_BLUETOOTH: - stringId = R.string.audio_mode_bluetooth; - break; - case CallAudioState.ROUTE_WIRED_HEADSET: - stringId = R.string.audio_mode_wired_headset; - break; - case CallAudioState.ROUTE_SPEAKER: - stringId = R.string.audio_mode_speaker; - break; - } - } - - if (stringId != 0) { - mAudioButton.setContentDescription(getResources().getString(stringId)); - } - } - - private void showAudioModePopup() { - Log.d(this, "showAudioPopup()..."); - - final ContextThemeWrapper contextWrapper = new ContextThemeWrapper(getActivity(), - R.style.InCallPopupMenuStyle); - mAudioModePopup = new PopupMenu(contextWrapper, mAudioButton /* anchorView */); - mAudioModePopup.getMenuInflater().inflate(R.menu.incall_audio_mode_menu, - mAudioModePopup.getMenu()); - mAudioModePopup.setOnMenuItemClickListener(this); - mAudioModePopup.setOnDismissListener(this); - - final Menu menu = mAudioModePopup.getMenu(); - - // TODO: Still need to have the "currently active" audio mode come - // up pre-selected (or focused?) with a blue highlight. Still - // need exact visual design, and possibly framework support for this. - // See comments below for the exact logic. - - final MenuItem speakerItem = menu.findItem(R.id.audio_mode_speaker); - speakerItem.setEnabled(isSupported(CallAudioState.ROUTE_SPEAKER)); - // TODO: Show speakerItem as initially "selected" if - // speaker is on. - - // We display *either* "earpiece" or "wired headset", never both, - // depending on whether a wired headset is physically plugged in. - final MenuItem earpieceItem = menu.findItem(R.id.audio_mode_earpiece); - final MenuItem wiredHeadsetItem = menu.findItem(R.id.audio_mode_wired_headset); - - final boolean usingHeadset = isSupported(CallAudioState.ROUTE_WIRED_HEADSET); - earpieceItem.setVisible(!usingHeadset); - earpieceItem.setEnabled(!usingHeadset); - wiredHeadsetItem.setVisible(usingHeadset); - wiredHeadsetItem.setEnabled(usingHeadset); - // TODO: Show the above item (either earpieceItem or wiredHeadsetItem) - // as initially "selected" if speakerOn and - // bluetoothIndicatorOn are both false. - - final MenuItem bluetoothItem = menu.findItem(R.id.audio_mode_bluetooth); - bluetoothItem.setEnabled(isSupported(CallAudioState.ROUTE_BLUETOOTH)); - // TODO: Show bluetoothItem as initially "selected" if - // bluetoothIndicatorOn is true. - - mAudioModePopup.show(); - - // Unfortunately we need to manually keep track of the popup menu's - // visiblity, since PopupMenu doesn't have an isShowing() method like - // Dialogs do. - mAudioModePopupVisible = true; - } - - private boolean isSupported(int mode) { - return (mode == (getPresenter().getSupportedAudio() & mode)); - } - - private boolean isAudio(int mode) { - return (mode == getPresenter().getAudioMode()); - } - - @Override - public void displayDialpad(boolean value, boolean animate) { - if (getActivity() != null && getActivity() instanceof InCallActivity) { - boolean changed = ((InCallActivity) getActivity()).showDialpadFragment(value, animate); - if (changed) { - mShowDialpadButton.setSelected(value); - mShowDialpadButton.setContentDescription(getContext().getString( - value /* show */ ? R.string.onscreenShowDialpadText_unselected - : R.string.onscreenShowDialpadText_selected)); - } - } - } - - @Override - public boolean isDialpadVisible() { - if (getActivity() != null && getActivity() instanceof InCallActivity) { - return ((InCallActivity) getActivity()).isDialpadVisible(); - } - return false; - } - - @Override - public Context getContext() { - return getActivity(); - } -} diff --git a/InCallUI/src/com/android/incallui/CallButtonPresenter.java b/InCallUI/src/com/android/incallui/CallButtonPresenter.java deleted file mode 100644 index defafda99..000000000 --- a/InCallUI/src/com/android/incallui/CallButtonPresenter.java +++ /dev/null @@ -1,486 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_ADD_CALL; -import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_AUDIO; -import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_DIALPAD; -import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_DOWNGRADE_TO_AUDIO; -import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_HOLD; -import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_MERGE; -import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_MUTE; -import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_PAUSE_VIDEO; -import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_SWAP; -import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_SWITCH_CAMERA; -import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_UPGRADE_TO_VIDEO; - -import android.content.Context; -import android.os.Build; -import android.os.Bundle; -import android.telecom.CallAudioState; -import android.telecom.InCallService.VideoCall; -import android.telecom.VideoProfile; - -import com.android.contacts.common.compat.CallSdkCompat; -import com.android.contacts.common.compat.SdkVersionOverride; -import com.android.dialer.compat.UserManagerCompat; -import com.android.incallui.AudioModeProvider.AudioModeListener; -import com.android.incallui.InCallCameraManager.Listener; -import com.android.incallui.InCallPresenter.CanAddCallListener; -import com.android.incallui.InCallPresenter.InCallDetailsListener; -import com.android.incallui.InCallPresenter.InCallState; -import com.android.incallui.InCallPresenter.InCallStateListener; -import com.android.incallui.InCallPresenter.IncomingCallListener; - -/** - * Logic for call buttons. - */ -public class CallButtonPresenter extends Presenter - implements InCallStateListener, AudioModeListener, IncomingCallListener, - InCallDetailsListener, CanAddCallListener, Listener { - - private static final String KEY_AUTOMATICALLY_MUTED = "incall_key_automatically_muted"; - private static final String KEY_PREVIOUS_MUTE_STATE = "incall_key_previous_mute_state"; - - private Call mCall; - private boolean mAutomaticallyMuted = false; - private boolean mPreviousMuteState = false; - - public CallButtonPresenter() { - } - - @Override - public void onUiReady(CallButtonUi ui) { - super.onUiReady(ui); - - AudioModeProvider.getInstance().addListener(this); - - // register for call state changes last - final InCallPresenter inCallPresenter = InCallPresenter.getInstance(); - inCallPresenter.addListener(this); - inCallPresenter.addIncomingCallListener(this); - inCallPresenter.addDetailsListener(this); - inCallPresenter.addCanAddCallListener(this); - inCallPresenter.getInCallCameraManager().addCameraSelectionListener(this); - - // Update the buttons state immediately for the current call - onStateChange(InCallState.NO_CALLS, inCallPresenter.getInCallState(), - CallList.getInstance()); - } - - @Override - public void onUiUnready(CallButtonUi ui) { - super.onUiUnready(ui); - - InCallPresenter.getInstance().removeListener(this); - AudioModeProvider.getInstance().removeListener(this); - InCallPresenter.getInstance().removeIncomingCallListener(this); - InCallPresenter.getInstance().removeDetailsListener(this); - InCallPresenter.getInstance().getInCallCameraManager().removeCameraSelectionListener(this); - InCallPresenter.getInstance().removeCanAddCallListener(this); - } - - @Override - public void onStateChange(InCallState oldState, InCallState newState, CallList callList) { - CallButtonUi ui = getUi(); - - if (newState == InCallState.OUTGOING) { - mCall = callList.getOutgoingCall(); - } else if (newState == InCallState.INCALL) { - mCall = callList.getActiveOrBackgroundCall(); - - // When connected to voice mail, automatically shows the dialpad. - // (On previous releases we showed it when in-call shows up, before waiting for - // OUTGOING. We may want to do that once we start showing "Voice mail" label on - // the dialpad too.) - if (ui != null) { - if (oldState == InCallState.OUTGOING && mCall != null) { - if (CallerInfoUtils.isVoiceMailNumber(ui.getContext(), mCall)) { - ui.displayDialpad(true /* show */, true /* animate */); - } - } - } - } else if (newState == InCallState.INCOMING) { - if (ui != null) { - ui.displayDialpad(false /* show */, true /* animate */); - } - mCall = callList.getIncomingCall(); - } else { - mCall = null; - } - updateUi(newState, mCall); - } - - /** - * Updates the user interface in response to a change in the details of a call. - * Currently handles changes to the call buttons in response to a change in the details for a - * call. This is important to ensure changes to the active call are reflected in the available - * buttons. - * - * @param call The active call. - * @param details The call details. - */ - @Override - public void onDetailsChanged(Call call, android.telecom.Call.Details details) { - // Only update if the changes are for the currently active call - if (getUi() != null && call != null && call.equals(mCall)) { - updateButtonsState(call); - } - } - - @Override - public void onIncomingCall(InCallState oldState, InCallState newState, Call call) { - onStateChange(oldState, newState, CallList.getInstance()); - } - - @Override - public void onCanAddCallChanged(boolean canAddCall) { - if (getUi() != null && mCall != null) { - updateButtonsState(mCall); - } - } - - @Override - public void onAudioMode(int mode) { - if (getUi() != null) { - getUi().setAudio(mode); - } - } - - @Override - public void onSupportedAudioMode(int mask) { - if (getUi() != null) { - getUi().setSupportedAudio(mask); - } - } - - @Override - public void onMute(boolean muted) { - if (getUi() != null && !mAutomaticallyMuted) { - getUi().setMute(muted); - } - } - - public int getAudioMode() { - return AudioModeProvider.getInstance().getAudioMode(); - } - - public int getSupportedAudio() { - return AudioModeProvider.getInstance().getSupportedModes(); - } - - public void setAudioMode(int mode) { - - // TODO: Set a intermediate state in this presenter until we get - // an update for onAudioMode(). This will make UI response immediate - // if it turns out to be slow - - Log.d(this, "Sending new Audio Mode: " + CallAudioState.audioRouteToString(mode)); - TelecomAdapter.getInstance().setAudioRoute(mode); - } - - /** - * Function assumes that bluetooth is not supported. - */ - public void toggleSpeakerphone() { - // this function should not be called if bluetooth is available - if (0 != (CallAudioState.ROUTE_BLUETOOTH & getSupportedAudio())) { - - // It's clear the UI is wrong, so update the supported mode once again. - Log.e(this, "toggling speakerphone not allowed when bluetooth supported."); - getUi().setSupportedAudio(getSupportedAudio()); - return; - } - - int newMode = CallAudioState.ROUTE_SPEAKER; - - // if speakerphone is already on, change to wired/earpiece - if (getAudioMode() == CallAudioState.ROUTE_SPEAKER) { - newMode = CallAudioState.ROUTE_WIRED_OR_EARPIECE; - } - - setAudioMode(newMode); - } - - public void muteClicked(boolean checked) { - Log.d(this, "turning on mute: " + checked); - TelecomAdapter.getInstance().mute(checked); - } - - public void holdClicked(boolean checked) { - if (mCall == null) { - return; - } - if (checked) { - Log.i(this, "Putting the call on hold: " + mCall); - TelecomAdapter.getInstance().holdCall(mCall.getId()); - } else { - Log.i(this, "Removing the call from hold: " + mCall); - TelecomAdapter.getInstance().unholdCall(mCall.getId()); - } - } - - public void swapClicked() { - if (mCall == null) { - return; - } - - Log.i(this, "Swapping the call: " + mCall); - TelecomAdapter.getInstance().swap(mCall.getId()); - } - - public void mergeClicked() { - TelecomAdapter.getInstance().merge(mCall.getId()); - } - - public void addCallClicked() { - // Automatically mute the current call - mAutomaticallyMuted = true; - mPreviousMuteState = AudioModeProvider.getInstance().getMute(); - // Simulate a click on the mute button - muteClicked(true); - TelecomAdapter.getInstance().addCall(); - } - - public void changeToVoiceClicked() { - VideoCall videoCall = mCall.getVideoCall(); - if (videoCall == null) { - return; - } - - VideoProfile videoProfile = new VideoProfile(VideoProfile.STATE_AUDIO_ONLY); - videoCall.sendSessionModifyRequest(videoProfile); - } - - public void showDialpadClicked(boolean checked) { - Log.v(this, "Show dialpad " + String.valueOf(checked)); - getUi().displayDialpad(checked /* show */, true /* animate */); - } - - public void changeToVideoClicked() { - VideoCall videoCall = mCall.getVideoCall(); - if (videoCall == null) { - return; - } - int currVideoState = mCall.getVideoState(); - int currUnpausedVideoState = VideoUtils.getUnPausedVideoState(currVideoState); - currUnpausedVideoState |= VideoProfile.STATE_BIDIRECTIONAL; - - VideoProfile videoProfile = new VideoProfile(currUnpausedVideoState); - videoCall.sendSessionModifyRequest(videoProfile); - mCall.setSessionModificationState(Call.SessionModificationState.WAITING_FOR_RESPONSE); - } - - /** - * Switches the camera between the front-facing and back-facing camera. - * @param useFrontFacingCamera True if we should switch to using the front-facing camera, or - * false if we should switch to using the back-facing camera. - */ - public void switchCameraClicked(boolean useFrontFacingCamera) { - InCallCameraManager cameraManager = InCallPresenter.getInstance().getInCallCameraManager(); - cameraManager.setUseFrontFacingCamera(useFrontFacingCamera); - - VideoCall videoCall = mCall.getVideoCall(); - if (videoCall == null) { - return; - } - - String cameraId = cameraManager.getActiveCameraId(); - if (cameraId != null) { - final int cameraDir = cameraManager.isUsingFrontFacingCamera() - ? Call.VideoSettings.CAMERA_DIRECTION_FRONT_FACING - : Call.VideoSettings.CAMERA_DIRECTION_BACK_FACING; - mCall.getVideoSettings().setCameraDir(cameraDir); - videoCall.setCamera(cameraId); - videoCall.requestCameraCapabilities(); - } - } - - - /** - * Stop or start client's video transmission. - * @param pause True if pausing the local user's video, or false if starting the local user's - * video. - */ - public void pauseVideoClicked(boolean pause) { - VideoCall videoCall = mCall.getVideoCall(); - if (videoCall == null) { - return; - } - - final int currUnpausedVideoState = VideoUtils.getUnPausedVideoState(mCall.getVideoState()); - if (pause) { - videoCall.setCamera(null); - VideoProfile videoProfile = new VideoProfile(currUnpausedVideoState - & ~VideoProfile.STATE_TX_ENABLED); - videoCall.sendSessionModifyRequest(videoProfile); - } else { - InCallCameraManager cameraManager = InCallPresenter.getInstance(). - getInCallCameraManager(); - videoCall.setCamera(cameraManager.getActiveCameraId()); - VideoProfile videoProfile = new VideoProfile(currUnpausedVideoState - | VideoProfile.STATE_TX_ENABLED); - videoCall.sendSessionModifyRequest(videoProfile); - mCall.setSessionModificationState(Call.SessionModificationState.WAITING_FOR_RESPONSE); - } - getUi().setVideoPaused(pause); - } - - private void updateUi(InCallState state, Call call) { - Log.d(this, "Updating call UI for call: ", call); - - final CallButtonUi ui = getUi(); - if (ui == null) { - return; - } - - final boolean isEnabled = - state.isConnectingOrConnected() &&!state.isIncoming() && call != null; - ui.setEnabled(isEnabled); - - if (call == null) { - return; - } - - updateButtonsState(call); - } - - /** - * Updates the buttons applicable for the UI. - * - * @param call The active call. - */ - private void updateButtonsState(Call call) { - Log.v(this, "updateButtonsState"); - final CallButtonUi ui = getUi(); - final boolean isVideo = VideoUtils.isVideoCall(call); - - // Common functionality (audio, hold, etc). - // Show either HOLD or SWAP, but not both. If neither HOLD or SWAP is available: - // (1) If the device normally can hold, show HOLD in a disabled state. - // (2) If the device doesn't have the concept of hold/swap, remove the button. - final boolean showSwap = call.can( - android.telecom.Call.Details.CAPABILITY_SWAP_CONFERENCE); - final boolean showHold = !showSwap - && call.can(android.telecom.Call.Details.CAPABILITY_SUPPORT_HOLD) - && call.can(android.telecom.Call.Details.CAPABILITY_HOLD); - final boolean isCallOnHold = call.getState() == Call.State.ONHOLD; - - final boolean showAddCall = TelecomAdapter.getInstance().canAddCall() - && UserManagerCompat.isUserUnlocked(ui.getContext()); - final boolean showMerge = call.can( - android.telecom.Call.Details.CAPABILITY_MERGE_CONFERENCE); - final boolean showUpgradeToVideo = !isVideo && hasVideoCallCapabilities(call); - final boolean showDowngradeToAudio = isVideo && isDowngradeToAudioSupported(call); - final boolean showMute = call.can(android.telecom.Call.Details.CAPABILITY_MUTE); - - ui.showButton(BUTTON_AUDIO, true); - ui.showButton(BUTTON_SWAP, showSwap); - ui.showButton(BUTTON_HOLD, showHold); - ui.setHold(isCallOnHold); - ui.showButton(BUTTON_MUTE, showMute); - ui.showButton(BUTTON_ADD_CALL, showAddCall); - ui.showButton(BUTTON_UPGRADE_TO_VIDEO, showUpgradeToVideo); - ui.showButton(BUTTON_DOWNGRADE_TO_AUDIO, showDowngradeToAudio); - ui.showButton(BUTTON_SWITCH_CAMERA, isVideo); - ui.showButton(BUTTON_PAUSE_VIDEO, isVideo); - if (isVideo) { - getUi().setVideoPaused(!VideoUtils.isTransmissionEnabled(call)); - } - ui.showButton(BUTTON_DIALPAD, true); - ui.showButton(BUTTON_MERGE, showMerge); - - ui.updateButtonStates(); - } - - private boolean hasVideoCallCapabilities(Call call) { - if (SdkVersionOverride.getSdkVersion(Build.VERSION_CODES.M) >= Build.VERSION_CODES.M) { - return call.can(android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_LOCAL_TX) - && call.can(android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_REMOTE_RX); - } - // In L, this single flag represents both video transmitting and receiving capabilities - return call.can(android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_LOCAL_TX); - } - - /** - * Determines if downgrading from a video call to an audio-only call is supported. In order to - * support downgrade to audio, the SDK version must be >= N and the call should NOT have the - * {@link android.telecom.Call.Details#CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO}. - * @param call The call. - * @return {@code true} if downgrading to an audio-only call from a video call is supported. - */ - private boolean isDowngradeToAudioSupported(Call call) { - return !call.can(CallSdkCompat.Details.CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO); - } - - public void refreshMuteState() { - // Restore the previous mute state - if (mAutomaticallyMuted && - AudioModeProvider.getInstance().getMute() != mPreviousMuteState) { - if (getUi() == null) { - return; - } - muteClicked(mPreviousMuteState); - } - mAutomaticallyMuted = false; - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - outState.putBoolean(KEY_AUTOMATICALLY_MUTED, mAutomaticallyMuted); - outState.putBoolean(KEY_PREVIOUS_MUTE_STATE, mPreviousMuteState); - } - - @Override - public void onRestoreInstanceState(Bundle savedInstanceState) { - mAutomaticallyMuted = - savedInstanceState.getBoolean(KEY_AUTOMATICALLY_MUTED, mAutomaticallyMuted); - mPreviousMuteState = - savedInstanceState.getBoolean(KEY_PREVIOUS_MUTE_STATE, mPreviousMuteState); - super.onRestoreInstanceState(savedInstanceState); - } - - public interface CallButtonUi extends Ui { - void showButton(int buttonId, boolean show); - void enableButton(int buttonId, boolean enable); - void setEnabled(boolean on); - void setMute(boolean on); - void setHold(boolean on); - void setCameraSwitched(boolean isBackFacingCamera); - void setVideoPaused(boolean isPaused); - void setAudio(int mode); - void setSupportedAudio(int mask); - void displayDialpad(boolean on, boolean animate); - boolean isDialpadVisible(); - - /** - * Once showButton() has been called on each of the individual buttons in the UI, call - * this to configure the overflow menu appropriately. - */ - void updateButtonStates(); - Context getContext(); - } - - @Override - public void onActiveCameraSelectionChanged(boolean isUsingFrontFacingCamera) { - if (getUi() == null) { - return; - } - getUi().setCameraSwitched(!isUsingFrontFacingCamera); - } -} diff --git a/InCallUI/src/com/android/incallui/CallCardFragment.java b/InCallUI/src/com/android/incallui/CallCardFragment.java deleted file mode 100644 index c2022d18c..000000000 --- a/InCallUI/src/com/android/incallui/CallCardFragment.java +++ /dev/null @@ -1,1510 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.AnimatorSet; -import android.animation.ObjectAnimator; -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.drawable.AnimationDrawable; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.GradientDrawable; -import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import android.os.Trace; -import android.support.v4.graphics.drawable.RoundedBitmapDrawable; -import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory; -import android.telecom.DisconnectCause; -import android.telephony.PhoneNumberUtils; -import android.text.TextUtils; -import android.text.format.DateUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.view.View.OnLayoutChangeListener; -import android.view.ViewGroup; -import android.view.ViewPropertyAnimator; -import android.view.ViewTreeObserver; -import android.view.ViewTreeObserver.OnGlobalLayoutListener; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityManager; -import android.view.animation.Animation; -import android.view.animation.AnimationUtils; -import android.widget.ImageButton; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.ListAdapter; -import android.widget.ListView; -import android.widget.TextView; -import android.widget.Toast; - -import com.android.contacts.common.compat.PhoneNumberUtilsCompat; -import com.android.contacts.common.util.MaterialColorMapUtils.MaterialPalette; -import com.android.contacts.common.widget.FloatingActionButtonController; -import com.android.dialer.R; -import com.android.phone.common.animation.AnimUtils; - -import java.util.List; - -/** - * Fragment for call card. - */ -public class CallCardFragment extends BaseFragment - implements CallCardPresenter.CallCardUi { - private static final String TAG = "CallCardFragment"; - - /** - * Internal class which represents the call state label which is to be applied. - */ - private class CallStateLabel { - private CharSequence mCallStateLabel; - private boolean mIsAutoDismissing; - - public CallStateLabel(CharSequence callStateLabel, boolean isAutoDismissing) { - mCallStateLabel = callStateLabel; - mIsAutoDismissing = isAutoDismissing; - } - - public CharSequence getCallStateLabel() { - return mCallStateLabel; - } - - /** - * Determines if the call state label should auto-dismiss. - * - * @return {@code true} if the call state label should auto-dismiss. - */ - public boolean isAutoDismissing() { - return mIsAutoDismissing; - } - }; - - private static final String IS_DIALPAD_SHOWING_KEY = "is_dialpad_showing"; - - /** - * The duration of time (in milliseconds) a call state label should remain visible before - * resetting to its previous value. - */ - private static final long CALL_STATE_LABEL_RESET_DELAY_MS = 3000; - /** - * Amount of time to wait before sending an announcement via the accessibility manager. - * When the call state changes to an outgoing or incoming state for the first time, the - * UI can often be changing due to call updates or contact lookup. This allows the UI - * to settle to a stable state to ensure that the correct information is announced. - */ - private static final long ACCESSIBILITY_ANNOUNCEMENT_DELAY_MS = 500; - - private AnimatorSet mAnimatorSet; - private int mShrinkAnimationDuration; - private int mFabNormalDiameter; - private int mFabSmallDiameter; - private boolean mIsLandscape; - private boolean mHasLargePhoto; - private boolean mIsDialpadShowing; - - // Primary caller info - private TextView mPhoneNumber; - private TextView mNumberLabel; - private TextView mPrimaryName; - private View mCallStateButton; - private ImageView mCallStateIcon; - private ImageView mCallStateVideoCallIcon; - private TextView mCallStateLabel; - private TextView mCallTypeLabel; - private ImageView mHdAudioIcon; - private ImageView mForwardIcon; - private ImageView mSpamIcon; - private View mCallNumberAndLabel; - private TextView mElapsedTime; - private Drawable mPrimaryPhotoDrawable; - private TextView mCallSubject; - private ImageView mWorkProfileIcon; - - // Container view that houses the entire primary call card, including the call buttons - private View mPrimaryCallCardContainer; - // Container view that houses the primary call information - private ViewGroup mPrimaryCallInfo; - private View mCallButtonsContainer; - private ImageView mPhotoSmall; - - // Secondary caller info - private View mSecondaryCallInfo; - private TextView mSecondaryCallName; - private View mSecondaryCallProviderInfo; - private TextView mSecondaryCallProviderLabel; - private View mSecondaryCallConferenceCallIcon; - private View mSecondaryCallVideoCallIcon; - private View mProgressSpinner; - - // Call card content - private View mCallCardContent; - private ImageView mPhotoLarge; - private View mContactContext; - private TextView mContactContextTitle; - private ListView mContactContextListView; - private LinearLayout mContactContextListHeaders; - - private View mManageConferenceCallButton; - - // Dark number info bar - private TextView mInCallMessageLabel; - - private FloatingActionButtonController mFloatingActionButtonController; - private View mFloatingActionButtonContainer; - private ImageButton mFloatingActionButton; - private int mFloatingActionButtonVerticalOffset; - - private float mTranslationOffset; - private Animation mPulseAnimation; - - private int mVideoAnimationDuration; - // Whether or not the call card is currently in the process of an animation - private boolean mIsAnimating; - - private MaterialPalette mCurrentThemeColors; - - /** - * Call state label to set when an auto-dismissing call state label is dismissed. - */ - private CharSequence mPostResetCallStateLabel; - private boolean mCallStateLabelResetPending = false; - private Handler mHandler; - - /** - * Determines if secondary call info is populated in the secondary call info UI. - */ - private boolean mHasSecondaryCallInfo = false; - - @Override - public CallCardPresenter.CallCardUi getUi() { - return this; - } - - @Override - public CallCardPresenter createPresenter() { - return new CallCardPresenter(); - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - mHandler = new Handler(Looper.getMainLooper()); - mShrinkAnimationDuration = getResources().getInteger(R.integer.shrink_animation_duration); - mVideoAnimationDuration = getResources().getInteger(R.integer.video_animation_duration); - mFloatingActionButtonVerticalOffset = getResources().getDimensionPixelOffset( - R.dimen.floating_action_button_vertical_offset); - mFabNormalDiameter = getResources().getDimensionPixelOffset( - R.dimen.end_call_floating_action_button_diameter); - mFabSmallDiameter = getResources().getDimensionPixelOffset( - R.dimen.end_call_floating_action_button_small_diameter); - - if (savedInstanceState != null) { - mIsDialpadShowing = savedInstanceState.getBoolean(IS_DIALPAD_SHOWING_KEY, false); - } - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - - final CallList calls = CallList.getInstance(); - final Call call = calls.getFirstCall(); - getPresenter().init(getActivity(), call); - } - - @Override - public void onSaveInstanceState(Bundle outState) { - outState.putBoolean(IS_DIALPAD_SHOWING_KEY, mIsDialpadShowing); - super.onSaveInstanceState(outState); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - Trace.beginSection(TAG + " onCreate"); - mTranslationOffset = - getResources().getDimensionPixelSize(R.dimen.call_card_anim_translate_y_offset); - final View view = inflater.inflate(R.layout.call_card_fragment, container, false); - Trace.endSection(); - return view; - } - - @Override - public void onViewCreated(View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - - mPulseAnimation = - AnimationUtils.loadAnimation(view.getContext(), R.anim.call_status_pulse); - - mPhoneNumber = (TextView) view.findViewById(R.id.phoneNumber); - mPrimaryName = (TextView) view.findViewById(R.id.name); - mNumberLabel = (TextView) view.findViewById(R.id.label); - mSecondaryCallInfo = view.findViewById(R.id.secondary_call_info); - mSecondaryCallProviderInfo = view.findViewById(R.id.secondary_call_provider_info); - mCallCardContent = view.findViewById(R.id.call_card_content); - mPhotoLarge = (ImageView) view.findViewById(R.id.photoLarge); - mPhotoLarge.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - getPresenter().onContactPhotoClick(); - } - }); - - mContactContext = view.findViewById(R.id.contact_context); - mContactContextTitle = (TextView) view.findViewById(R.id.contactContextTitle); - mContactContextListView = (ListView) view.findViewById(R.id.contactContextInfo); - // This layout stores all the list header layouts so they can be easily removed. - mContactContextListHeaders = new LinearLayout(getView().getContext()); - mContactContextListView.addHeaderView(mContactContextListHeaders); - - mCallStateIcon = (ImageView) view.findViewById(R.id.callStateIcon); - mCallStateVideoCallIcon = (ImageView) view.findViewById(R.id.videoCallIcon); - mWorkProfileIcon = (ImageView) view.findViewById(R.id.workProfileIcon); - mCallStateLabel = (TextView) view.findViewById(R.id.callStateLabel); - mHdAudioIcon = (ImageView) view.findViewById(R.id.hdAudioIcon); - mForwardIcon = (ImageView) view.findViewById(R.id.forwardIcon); - mSpamIcon = (ImageView) view.findViewById(R.id.spamIcon); - mCallNumberAndLabel = view.findViewById(R.id.labelAndNumber); - mCallTypeLabel = (TextView) view.findViewById(R.id.callTypeLabel); - mElapsedTime = (TextView) view.findViewById(R.id.elapsedTime); - mPrimaryCallCardContainer = view.findViewById(R.id.primary_call_info_container); - mPrimaryCallInfo = (ViewGroup) view.findViewById(R.id.primary_call_banner); - mCallButtonsContainer = view.findViewById(R.id.callButtonFragment); - mPhotoSmall = (ImageView) view.findViewById(R.id.photoSmall); - mPhotoSmall.setVisibility(View.GONE); - mInCallMessageLabel = (TextView) view.findViewById(R.id.connectionServiceMessage); - mProgressSpinner = view.findViewById(R.id.progressSpinner); - - mFloatingActionButtonContainer = view.findViewById( - R.id.floating_end_call_action_button_container); - mFloatingActionButton = (ImageButton) view.findViewById( - R.id.floating_end_call_action_button); - mFloatingActionButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - getPresenter().endCallClicked(); - } - }); - mFloatingActionButtonController = new FloatingActionButtonController(getActivity(), - mFloatingActionButtonContainer, mFloatingActionButton); - - mSecondaryCallInfo.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - getPresenter().secondaryInfoClicked(); - updateFabPositionForSecondaryCallInfo(); - } - }); - - mCallStateButton = view.findViewById(R.id.callStateButton); - mCallStateButton.setOnLongClickListener(new View.OnLongClickListener() { - @Override - public boolean onLongClick(View v) { - getPresenter().onCallStateButtonTouched(); - return false; - } - }); - - mManageConferenceCallButton = view.findViewById(R.id.manage_conference_call_button); - mManageConferenceCallButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - InCallActivity activity = (InCallActivity) getActivity(); - activity.showConferenceFragment(true); - } - }); - - mPrimaryName.setElegantTextHeight(false); - mCallStateLabel.setElegantTextHeight(false); - mCallSubject = (TextView) view.findViewById(R.id.callSubject); - } - - @Override - public void setVisible(boolean on) { - if (on) { - getView().setVisibility(View.VISIBLE); - } else { - getView().setVisibility(View.INVISIBLE); - } - } - - /** - * Hides or shows the progress spinner. - * - * @param visible {@code True} if the progress spinner should be visible. - */ - @Override - public void setProgressSpinnerVisible(boolean visible) { - mProgressSpinner.setVisibility(visible ? View.VISIBLE : View.GONE); - } - - @Override - public void setContactContextTitle(View headerView) { - mContactContextListHeaders.removeAllViews(); - mContactContextListHeaders.addView(headerView); - } - - @Override - public void setContactContextContent(ListAdapter listAdapter) { - mContactContextListView.setAdapter(listAdapter); - } - - @Override - public void showContactContext(boolean show) { - showImageView(mPhotoLarge, !show); - showImageView(mPhotoSmall, show); - mPrimaryCallCardContainer.setElevation( - show ? 0 : getResources().getDimension(R.dimen.primary_call_elevation)); - mContactContext.setVisibility(show ? View.VISIBLE : View.GONE); - } - - /** - * Sets the visibility of the primary call card. - * Ensures that when the primary call card is hidden, the video surface slides over to fill the - * entire screen. - * - * @param visible {@code True} if the primary call card should be visible. - */ - @Override - public void setCallCardVisible(final boolean visible) { - Log.v(this, "setCallCardVisible : isVisible = " + visible); - // When animating the hide/show of the views in a landscape layout, we need to take into - // account whether we are in a left-to-right locale or a right-to-left locale and adjust - // the animations accordingly. - final boolean isLayoutRtl = InCallPresenter.isRtl(); - - // Retrieve here since at fragment creation time the incoming video view is not inflated. - final View videoView = getView().findViewById(R.id.incomingVideo); - if (videoView == null) { - return; - } - - // Determine how much space there is below or to the side of the call card. - final float spaceBesideCallCard = getSpaceBesideCallCard(); - - // We need to translate the video surface, but we need to know its position after the layout - // has occurred so use a {@code ViewTreeObserver}. - final ViewTreeObserver observer = getView().getViewTreeObserver(); - observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { - @Override - public boolean onPreDraw() { - // We don't want to continue getting called. - getView().getViewTreeObserver().removeOnPreDrawListener(this); - - float videoViewTranslation = 0f; - - // Translate the call card to its pre-animation state. - if (!mIsLandscape) { - mPrimaryCallCardContainer.setTranslationY(visible ? - -mPrimaryCallCardContainer.getHeight() : 0); - - ViewGroup.LayoutParams p = videoView.getLayoutParams(); - videoViewTranslation = p.height / 2 - spaceBesideCallCard / 2; - } - - // Perform animation of video view. - ViewPropertyAnimator videoViewAnimator = videoView.animate() - .setInterpolator(AnimUtils.EASE_OUT_EASE_IN) - .setDuration(mVideoAnimationDuration); - if (mIsLandscape) { - videoViewAnimator - .translationX(visible ? videoViewTranslation : 0); - } else { - videoViewAnimator - .translationY(visible ? videoViewTranslation : 0); - } - videoViewAnimator.start(); - - // Animate the call card sliding. - ViewPropertyAnimator callCardAnimator = mPrimaryCallCardContainer.animate() - .setInterpolator(AnimUtils.EASE_OUT_EASE_IN) - .setDuration(mVideoAnimationDuration) - .setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - super.onAnimationEnd(animation); - if (!visible) { - mPrimaryCallCardContainer.setVisibility(View.GONE); - } - } - - @Override - public void onAnimationStart(Animator animation) { - super.onAnimationStart(animation); - if (visible) { - mPrimaryCallCardContainer.setVisibility(View.VISIBLE); - } - } - }); - - if (mIsLandscape) { - float translationX = mPrimaryCallCardContainer.getWidth(); - translationX *= isLayoutRtl ? 1 : -1; - callCardAnimator - .translationX(visible ? 0 : translationX) - .start(); - } else { - callCardAnimator - .translationY(visible ? 0 : -mPrimaryCallCardContainer.getHeight()) - .start(); - } - - return true; - } - }); - } - - /** - * Determines the amount of space below the call card for portrait layouts), or beside the - * call card for landscape layouts. - * - * @return The amount of space below or beside the call card. - */ - public float getSpaceBesideCallCard() { - if (mIsLandscape) { - return getView().getWidth() - mPrimaryCallCardContainer.getWidth(); - } else { - final int callCardHeight; - // Retrieve the actual height of the call card, independent of whether or not the - // outgoing call animation is in progress. The animation does not run in landscape mode - // so this only needs to be done for portrait. - if (mPrimaryCallCardContainer.getTag(R.id.view_tag_callcard_actual_height) != null) { - callCardHeight = (int) mPrimaryCallCardContainer.getTag( - R.id.view_tag_callcard_actual_height); - } else { - callCardHeight = mPrimaryCallCardContainer.getHeight(); - } - return getView().getHeight() - callCardHeight; - } - } - - @Override - public void setPrimaryName(String name, boolean nameIsNumber) { - if (TextUtils.isEmpty(name)) { - mPrimaryName.setText(null); - } else { - mPrimaryName.setText(nameIsNumber - ? PhoneNumberUtilsCompat.createTtsSpannable(name) - : name); - - // Set direction of the name field - int nameDirection = View.TEXT_DIRECTION_INHERIT; - if (nameIsNumber) { - nameDirection = View.TEXT_DIRECTION_LTR; - } - mPrimaryName.setTextDirection(nameDirection); - } - } - - /** - * Sets the primary image for the contact photo. - * - * @param image The drawable to set. - * @param isVisible Whether the contact photo should be visible after being set. - */ - @Override - public void setPrimaryImage(Drawable image, boolean isVisible) { - if (image != null) { - setDrawableToImageViews(image); - showImageView(mPhotoLarge, isVisible); - } - } - - @Override - public void setPrimaryPhoneNumber(String number) { - // Set the number - if (TextUtils.isEmpty(number)) { - mPhoneNumber.setText(null); - mPhoneNumber.setVisibility(View.GONE); - } else { - mPhoneNumber.setText(PhoneNumberUtilsCompat.createTtsSpannable(number)); - mPhoneNumber.setVisibility(View.VISIBLE); - mPhoneNumber.setTextDirection(View.TEXT_DIRECTION_LTR); - } - } - - @Override - public void setPrimaryLabel(String label) { - if (!TextUtils.isEmpty(label)) { - mNumberLabel.setText(label); - mNumberLabel.setVisibility(View.VISIBLE); - } else { - mNumberLabel.setVisibility(View.GONE); - } - - } - - /** - * Sets the primary caller information. - * - * @param number The caller phone number. - * @param name The caller name. - * @param nameIsNumber {@code true} if the name should be shown in place of the phone number. - * @param label The label. - * @param photo The contact photo drawable. - * @param isSipCall {@code true} if this is a SIP call. - * @param isContactPhotoShown {@code true} if the contact photo should be shown (it will be - * updated even if it is not shown). - * @param isWorkCall Whether the call is placed through a work phone account or caller is a work - contact. - */ - @Override - public void setPrimary(String number, String name, boolean nameIsNumber, String label, - Drawable photo, boolean isSipCall, boolean isContactPhotoShown, boolean isWorkCall) { - Log.d(this, "Setting primary call"); - // set the name field. - setPrimaryName(name, nameIsNumber); - - if (TextUtils.isEmpty(number) && TextUtils.isEmpty(label)) { - mCallNumberAndLabel.setVisibility(View.GONE); - mElapsedTime.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); - } else { - mCallNumberAndLabel.setVisibility(View.VISIBLE); - mElapsedTime.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_END); - } - - setPrimaryPhoneNumber(number); - - // Set the label (Mobile, Work, etc) - setPrimaryLabel(label); - - showInternetCallLabel(isSipCall); - - setDrawableToImageViews(photo); - showImageView(mPhotoLarge, isContactPhotoShown); - showImageView(mWorkProfileIcon, isWorkCall); - } - - @Override - public void setSecondary(boolean show, String name, boolean nameIsNumber, String label, - String providerLabel, boolean isConference, boolean isVideoCall, boolean isFullscreen) { - - if (show) { - mHasSecondaryCallInfo = true; - boolean hasProvider = !TextUtils.isEmpty(providerLabel); - initializeSecondaryCallInfo(hasProvider); - - // Do not show the secondary caller info in fullscreen mode, but ensure it is populated - // in case fullscreen mode is exited in the future. - setSecondaryInfoVisible(!isFullscreen); - - mSecondaryCallConferenceCallIcon.setVisibility(isConference ? View.VISIBLE : View.GONE); - mSecondaryCallVideoCallIcon.setVisibility(isVideoCall ? View.VISIBLE : View.GONE); - - mSecondaryCallName.setText(nameIsNumber - ? PhoneNumberUtilsCompat.createTtsSpannable(name) - : name); - if (hasProvider) { - mSecondaryCallProviderLabel.setText(providerLabel); - } - - int nameDirection = View.TEXT_DIRECTION_INHERIT; - if (nameIsNumber) { - nameDirection = View.TEXT_DIRECTION_LTR; - } - mSecondaryCallName.setTextDirection(nameDirection); - } else { - mHasSecondaryCallInfo = false; - setSecondaryInfoVisible(false); - } - } - - /** - * Sets the visibility of the secondary caller info box. Note, if the {@code visible} parameter - * is passed in {@code true}, and there is no secondary caller info populated (as determined by - * {@code mHasSecondaryCallInfo}, the secondary caller info box will not be shown. - * - * @param visible {@code true} if the secondary caller info should be shown, {@code false} - * otherwise. - */ - @Override - public void setSecondaryInfoVisible(final boolean visible) { - boolean wasVisible = mSecondaryCallInfo.isShown(); - final boolean isVisible = visible && mHasSecondaryCallInfo; - Log.v(this, "setSecondaryInfoVisible: wasVisible = " + wasVisible + " isVisible = " - + isVisible); - - // If visibility didn't change, nothing to do. - if (wasVisible == isVisible) { - return; - } - - // If we are showing the secondary info, we need to show it before animating so that its - // height will be determined on layout. - if (isVisible) { - mSecondaryCallInfo.setVisibility(View.VISIBLE); - } else { - mSecondaryCallInfo.setVisibility(View.GONE); - } - - updateFabPositionForSecondaryCallInfo(); - // We need to translate the secondary caller info, but we need to know its position after - // the layout has occurred so use a {@code ViewTreeObserver}. - final ViewTreeObserver observer = getView().getViewTreeObserver(); - - observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { - @Override - public boolean onPreDraw() { - // We don't want to continue getting called. - getView().getViewTreeObserver().removeOnPreDrawListener(this); - - // Get the height of the secondary call info now, and then re-hide the view prior - // to doing the actual animation. - int secondaryHeight = mSecondaryCallInfo.getHeight(); - if (isVisible) { - mSecondaryCallInfo.setVisibility(View.GONE); - } else { - mSecondaryCallInfo.setVisibility(View.VISIBLE); - } - Log.v(this, "setSecondaryInfoVisible: secondaryHeight = " + secondaryHeight); - - // Set the position of the secondary call info card to its starting location. - mSecondaryCallInfo.setTranslationY(visible ? secondaryHeight : 0); - - // Animate the secondary card info slide up/down as it appears and disappears. - ViewPropertyAnimator secondaryInfoAnimator = mSecondaryCallInfo.animate() - .setInterpolator(AnimUtils.EASE_OUT_EASE_IN) - .setDuration(mVideoAnimationDuration) - .translationY(isVisible ? 0 : secondaryHeight) - .setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - if (!isVisible) { - mSecondaryCallInfo.setVisibility(View.GONE); - } - } - - @Override - public void onAnimationStart(Animator animation) { - if (isVisible) { - mSecondaryCallInfo.setVisibility(View.VISIBLE); - } - } - }); - secondaryInfoAnimator.start(); - - // Notify listeners of a change in the visibility of the secondary info. This is - // important when in a video call so that the video call presenter can shift the - // video preview up or down to accommodate the secondary caller info. - InCallPresenter.getInstance().notifySecondaryCallerInfoVisibilityChanged(visible, - secondaryHeight); - - return true; - } - }); - } - - @Override - public void setCallState( - int state, - int videoState, - int sessionModificationState, - DisconnectCause disconnectCause, - String connectionLabel, - Drawable callStateIcon, - String gatewayNumber, - boolean isWifi, - boolean isConference, - boolean isWorkCall) { - boolean isGatewayCall = !TextUtils.isEmpty(gatewayNumber); - CallStateLabel callStateLabel = getCallStateLabelFromState(state, videoState, - sessionModificationState, disconnectCause, connectionLabel, isGatewayCall, isWifi, - isConference, isWorkCall); - - Log.v(this, "setCallState " + callStateLabel.getCallStateLabel()); - Log.v(this, "AutoDismiss " + callStateLabel.isAutoDismissing()); - Log.v(this, "DisconnectCause " + disconnectCause.toString()); - Log.v(this, "gateway " + connectionLabel + gatewayNumber); - - // Check for video state change and update the visibility of the contact photo. The contact - // photo is hidden when the incoming video surface is shown. - // The contact photo visibility can also change in setPrimary(). - boolean showContactPhoto = !VideoCallPresenter.showIncomingVideo(videoState, state); - mPhotoLarge.setVisibility(showContactPhoto ? View.VISIBLE : View.GONE); - - // Check if the call subject is showing -- if it is, we want to bypass showing the call - // state. - boolean isSubjectShowing = mCallSubject.getVisibility() == View.VISIBLE; - - if (TextUtils.equals(callStateLabel.getCallStateLabel(), mCallStateLabel.getText()) && - !isSubjectShowing) { - // Nothing to do if the labels are the same - if (state == Call.State.ACTIVE || state == Call.State.CONFERENCED) { - mCallStateLabel.clearAnimation(); - mCallStateIcon.clearAnimation(); - } - return; - } - - if (isSubjectShowing) { - changeCallStateLabel(null); - callStateIcon = null; - } else { - // Update the call state label and icon. - setCallStateLabel(callStateLabel); - } - - if (!TextUtils.isEmpty(callStateLabel.getCallStateLabel())) { - if (state == Call.State.ACTIVE || state == Call.State.CONFERENCED) { - mCallStateLabel.clearAnimation(); - } else { - mCallStateLabel.startAnimation(mPulseAnimation); - } - } else { - mCallStateLabel.clearAnimation(); - } - - if (callStateIcon != null) { - mCallStateIcon.setVisibility(View.VISIBLE); - // Invoke setAlpha(float) instead of setAlpha(int) to set the view's alpha. This is - // needed because the pulse animation operates on the view alpha. - mCallStateIcon.setAlpha(1.0f); - mCallStateIcon.setImageDrawable(callStateIcon); - - if (state == Call.State.ACTIVE || state == Call.State.CONFERENCED - || TextUtils.isEmpty(callStateLabel.getCallStateLabel())) { - mCallStateIcon.clearAnimation(); - } else { - mCallStateIcon.startAnimation(mPulseAnimation); - } - - if (callStateIcon instanceof AnimationDrawable) { - ((AnimationDrawable) callStateIcon).start(); - } - } else { - mCallStateIcon.clearAnimation(); - - // Invoke setAlpha(float) instead of setAlpha(int) to set the view's alpha. This is - // needed because the pulse animation operates on the view alpha. - mCallStateIcon.setAlpha(0.0f); - mCallStateIcon.setVisibility(View.GONE); - } - - if (VideoUtils.isVideoCall(videoState) - || (state == Call.State.ACTIVE && sessionModificationState - == Call.SessionModificationState.WAITING_FOR_RESPONSE)) { - mCallStateVideoCallIcon.setVisibility(View.VISIBLE); - } else { - mCallStateVideoCallIcon.setVisibility(View.GONE); - } - } - - private void setCallStateLabel(CallStateLabel callStateLabel) { - Log.v(this, "setCallStateLabel : label = " + callStateLabel.getCallStateLabel()); - - if (callStateLabel.isAutoDismissing()) { - mCallStateLabelResetPending = true; - mHandler.postDelayed(new Runnable() { - @Override - public void run() { - Log.v(this, "restoringCallStateLabel : label = " + - mPostResetCallStateLabel); - changeCallStateLabel(mPostResetCallStateLabel); - mCallStateLabelResetPending = false; - } - }, CALL_STATE_LABEL_RESET_DELAY_MS); - - changeCallStateLabel(callStateLabel.getCallStateLabel()); - } else { - // Keep track of the current call state label; used when resetting auto dismissing - // call state labels. - mPostResetCallStateLabel = callStateLabel.getCallStateLabel(); - - if (!mCallStateLabelResetPending) { - changeCallStateLabel(callStateLabel.getCallStateLabel()); - } - } - } - - private void changeCallStateLabel(CharSequence callStateLabel) { - Log.v(this, "changeCallStateLabel : label = " + callStateLabel); - if (!TextUtils.isEmpty(callStateLabel)) { - mCallStateLabel.setText(callStateLabel); - mCallStateLabel.setAlpha(1); - mCallStateLabel.setVisibility(View.VISIBLE); - } else { - Animation callStateLabelAnimation = mCallStateLabel.getAnimation(); - if (callStateLabelAnimation != null) { - callStateLabelAnimation.cancel(); - } - mCallStateLabel.setText(null); - mCallStateLabel.setAlpha(0); - mCallStateLabel.setVisibility(View.GONE); - } - } - - @Override - public void setCallbackNumber(String callbackNumber, boolean isEmergencyCall) { - if (mInCallMessageLabel == null) { - return; - } - - if (TextUtils.isEmpty(callbackNumber)) { - mInCallMessageLabel.setVisibility(View.GONE); - return; - } - - // TODO: The new Locale-specific methods don't seem to be working. Revisit this. - callbackNumber = PhoneNumberUtils.formatNumber(callbackNumber); - - int stringResourceId = isEmergencyCall ? R.string.card_title_callback_number_emergency - : R.string.card_title_callback_number; - - String text = getString(stringResourceId, callbackNumber); - mInCallMessageLabel.setText(text); - - mInCallMessageLabel.setVisibility(View.VISIBLE); - } - - /** - * Sets and shows the call subject if it is not empty. Hides the call subject otherwise. - * - * @param callSubject The call subject. - */ - @Override - public void setCallSubject(String callSubject) { - boolean showSubject = !TextUtils.isEmpty(callSubject); - - mCallSubject.setVisibility(showSubject ? View.VISIBLE : View.GONE); - if (showSubject) { - mCallSubject.setText(callSubject); - } else { - mCallSubject.setText(null); - } - } - - public boolean isAnimating() { - return mIsAnimating; - } - - private void showInternetCallLabel(boolean show) { - if (show) { - final String label = getView().getContext().getString( - R.string.incall_call_type_label_sip); - mCallTypeLabel.setVisibility(View.VISIBLE); - mCallTypeLabel.setText(label); - } else { - mCallTypeLabel.setVisibility(View.GONE); - } - } - - @Override - public void setPrimaryCallElapsedTime(boolean show, long duration) { - if (show) { - if (mElapsedTime.getVisibility() != View.VISIBLE) { - AnimUtils.fadeIn(mElapsedTime, AnimUtils.DEFAULT_DURATION); - } - String callTimeElapsed = DateUtils.formatElapsedTime(duration / 1000); - mElapsedTime.setText(callTimeElapsed); - - String durationDescription = - InCallDateUtils.formatDuration(duration); - mElapsedTime.setContentDescription( - !TextUtils.isEmpty(durationDescription) ? durationDescription : null); - } else { - // hide() animation has no effect if it is already hidden. - AnimUtils.fadeOut(mElapsedTime, AnimUtils.DEFAULT_DURATION); - } - } - - /** - * Set all the ImageViews to the same photo. Currently there are 2 photo views: the large one - * (which fills about the bottom half of the screen) and the small one, which displays as a - * circle next to the primary contact info. This method does not handle whether the ImageView - * is shown or not. - * - * @param photo The photo to set for the image views. - */ - private void setDrawableToImageViews(Drawable photo) { - if (photo == null) { - photo = ContactInfoCache.getInstance(getView().getContext()) - .getDefaultContactPhotoDrawable(); - } - - if (mPrimaryPhotoDrawable == photo){ - return; - } - mPrimaryPhotoDrawable = photo; - - mPhotoLarge.setImageDrawable(photo); - - // Modify the drawable to be round for the smaller ImageView. - Bitmap bitmap = drawableToBitmap(photo); - if (bitmap != null) { - final RoundedBitmapDrawable drawable = - RoundedBitmapDrawableFactory.create(getResources(), bitmap); - drawable.setAntiAlias(true); - drawable.setCornerRadius(bitmap.getHeight() / 2); - photo = drawable; - } - mPhotoSmall.setImageDrawable(photo); - } - - /** - * Helper method for image view to handle animations. - * - * @param view The image view to show or hide. - * @param isVisible {@code true} if we want to show the image, {@code false} to hide it. - */ - private void showImageView(ImageView view, boolean isVisible) { - if (view.getDrawable() == null) { - if (isVisible) { - AnimUtils.fadeIn(mElapsedTime, AnimUtils.DEFAULT_DURATION); - } - } else { - // Cross fading is buggy and not noticeable due to the multiple calls to this method - // that switch drawables in the middle of the cross-fade animations. Just show the - // photo directly instead. - view.setVisibility(isVisible ? View.VISIBLE : View.GONE); - } - } - - /** - * Converts a drawable into a bitmap. - * - * @param drawable the drawable to be converted. - */ - public static Bitmap drawableToBitmap(Drawable drawable) { - Bitmap bitmap; - if (drawable instanceof BitmapDrawable) { - bitmap = ((BitmapDrawable) drawable).getBitmap(); - } else { - if (drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) { - // Needed for drawables that are just a colour. - bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); - } else { - bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), - drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); - } - - Log.i(TAG, "Created bitmap with width " + bitmap.getWidth() + ", height " - + bitmap.getHeight()); - - Canvas canvas = new Canvas(bitmap); - drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); - drawable.draw(canvas); - } - return bitmap; - } - - /** - * Gets the call state label based on the state of the call or cause of disconnect. - * - * Additional labels are applied as follows: - * 1. All outgoing calls with display "Calling via [Provider]". - * 2. Ongoing calls will display the name of the provider. - * 3. Incoming calls will only display "Incoming via..." for accounts. - * 4. Video calls, and session modification states (eg. requesting video). - * 5. Incoming and active Wi-Fi calls will show label provided by hint. - * - * TODO: Move this to the CallCardPresenter. - */ - private CallStateLabel getCallStateLabelFromState(int state, int videoState, - int sessionModificationState, DisconnectCause disconnectCause, String label, - boolean isGatewayCall, boolean isWifi, boolean isConference, boolean isWorkCall) { - final Context context = getView().getContext(); - CharSequence callStateLabel = null; // Label to display as part of the call banner - - boolean hasSuggestedLabel = label != null; - boolean isAccount = hasSuggestedLabel && !isGatewayCall; - boolean isAutoDismissing = false; - - switch (state) { - case Call.State.IDLE: - // "Call state" is meaningless in this state. - break; - case Call.State.ACTIVE: - // We normally don't show a "call state label" at all in this state - // (but we can use the call state label to display the provider name). - if ((isAccount || isWifi || isConference) && hasSuggestedLabel) { - callStateLabel = label; - } else if (sessionModificationState - == Call.SessionModificationState.REQUEST_REJECTED) { - callStateLabel = context.getString(R.string.card_title_video_call_rejected); - isAutoDismissing = true; - } else if (sessionModificationState - == Call.SessionModificationState.REQUEST_FAILED) { - callStateLabel = context.getString(R.string.card_title_video_call_error); - isAutoDismissing = true; - } else if (sessionModificationState - == Call.SessionModificationState.WAITING_FOR_RESPONSE) { - callStateLabel = context.getString(R.string.card_title_video_call_requesting); - } else if (sessionModificationState - == Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) { - callStateLabel = context.getString(R.string.card_title_video_call_requesting); - } else if (VideoUtils.isVideoCall(videoState)) { - callStateLabel = context.getString(R.string.card_title_video_call); - } - break; - case Call.State.ONHOLD: - callStateLabel = context.getString(R.string.card_title_on_hold); - break; - case Call.State.CONNECTING: - case Call.State.DIALING: - if (hasSuggestedLabel && !isWifi) { - callStateLabel = context.getString(R.string.calling_via_template, label); - } else { - callStateLabel = context.getString(R.string.card_title_dialing); - } - break; - case Call.State.REDIALING: - callStateLabel = context.getString(R.string.card_title_redialing); - break; - case Call.State.INCOMING: - case Call.State.CALL_WAITING: - if (isWifi && hasSuggestedLabel) { - callStateLabel = label; - } else if (isAccount) { - callStateLabel = context.getString(R.string.incoming_via_template, label); - } else if (VideoUtils.isVideoCall(videoState)) { - callStateLabel = context.getString(R.string.notification_incoming_video_call); - } else { - callStateLabel = - context.getString(isWorkCall ? R.string.card_title_incoming_work_call - : R.string.card_title_incoming_call); - } - break; - case Call.State.DISCONNECTING: - // While in the DISCONNECTING state we display a "Hanging up" - // message in order to make the UI feel more responsive. (In - // GSM it's normal to see a delay of a couple of seconds while - // negotiating the disconnect with the network, so the "Hanging - // up" state at least lets the user know that we're doing - // something. This state is currently not used with CDMA.) - callStateLabel = context.getString(R.string.card_title_hanging_up); - break; - case Call.State.DISCONNECTED: - callStateLabel = disconnectCause.getLabel(); - if (TextUtils.isEmpty(callStateLabel)) { - callStateLabel = context.getString(R.string.card_title_call_ended); - } - break; - case Call.State.CONFERENCED: - callStateLabel = context.getString(R.string.card_title_conf_call); - break; - default: - Log.wtf(this, "updateCallStateWidgets: unexpected call: " + state); - } - return new CallStateLabel(callStateLabel, isAutoDismissing); - } - - private void initializeSecondaryCallInfo(boolean hasProvider) { - // mSecondaryCallName is initialized here (vs. onViewCreated) because it is inaccessible - // until mSecondaryCallInfo is inflated in the call above. - if (mSecondaryCallName == null) { - mSecondaryCallName = (TextView) getView().findViewById(R.id.secondaryCallName); - mSecondaryCallConferenceCallIcon = - getView().findViewById(R.id.secondaryCallConferenceCallIcon); - mSecondaryCallVideoCallIcon = - getView().findViewById(R.id.secondaryCallVideoCallIcon); - } - - if (mSecondaryCallProviderLabel == null && hasProvider) { - mSecondaryCallProviderInfo.setVisibility(View.VISIBLE); - mSecondaryCallProviderLabel = (TextView) getView() - .findViewById(R.id.secondaryCallProviderLabel); - } - } - - public void dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { - if (event.getEventType() == AccessibilityEvent.TYPE_ANNOUNCEMENT) { - // Indicate this call is in active if no label is provided. The label is empty when - // the call is in active, not in other status such as onhold or dialing etc. - if (!mCallStateLabel.isShown() || TextUtils.isEmpty(mCallStateLabel.getText())) { - event.getText().add( - TextUtils.expandTemplate( - getResources().getText(R.string.accessibility_call_is_active), - mPrimaryName.getText())); - } else { - dispatchPopulateAccessibilityEvent(event, mCallStateLabel); - dispatchPopulateAccessibilityEvent(event, mPrimaryName); - dispatchPopulateAccessibilityEvent(event, mCallTypeLabel); - dispatchPopulateAccessibilityEvent(event, mPhoneNumber); - } - return; - } - dispatchPopulateAccessibilityEvent(event, mCallStateLabel); - dispatchPopulateAccessibilityEvent(event, mPrimaryName); - dispatchPopulateAccessibilityEvent(event, mPhoneNumber); - dispatchPopulateAccessibilityEvent(event, mCallTypeLabel); - dispatchPopulateAccessibilityEvent(event, mSecondaryCallName); - dispatchPopulateAccessibilityEvent(event, mSecondaryCallProviderLabel); - - return; - } - - @Override - public void sendAccessibilityAnnouncement() { - mHandler.postDelayed(new Runnable() { - @Override - public void run() { - if (getView() != null && getView().getParent() != null && - isAccessibilityEnabled(getContext())) { - AccessibilityEvent event = AccessibilityEvent.obtain( - AccessibilityEvent.TYPE_ANNOUNCEMENT); - dispatchPopulateAccessibilityEvent(event); - getView().getParent().requestSendAccessibilityEvent(getView(), event); - } - } - - private boolean isAccessibilityEnabled(Context context) { - AccessibilityManager accessibilityManager = - (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE); - return accessibilityManager != null && accessibilityManager.isEnabled(); - - } - }, ACCESSIBILITY_ANNOUNCEMENT_DELAY_MS); - } - - @Override - public void setEndCallButtonEnabled(boolean enabled, boolean animate) { - if (enabled != mFloatingActionButton.isEnabled()) { - if (animate) { - if (enabled) { - mFloatingActionButtonController.scaleIn(AnimUtils.NO_DELAY); - } else { - mFloatingActionButtonController.scaleOut(); - } - } else { - if (enabled) { - mFloatingActionButtonContainer.setScaleX(1); - mFloatingActionButtonContainer.setScaleY(1); - mFloatingActionButtonContainer.setVisibility(View.VISIBLE); - } else { - mFloatingActionButtonContainer.setVisibility(View.GONE); - } - } - mFloatingActionButton.setEnabled(enabled); - updateFabPosition(); - } - } - - /** - * Changes the visibility of the HD audio icon. - * - * @param visible {@code true} if the UI should show the HD audio icon. - */ - @Override - public void showHdAudioIndicator(boolean visible) { - mHdAudioIcon.setVisibility(visible ? View.VISIBLE : View.GONE); - } - - /** - * Changes the visibility of the forward icon. - * - * @param visible {@code true} if the UI should show the forward icon. - */ - @Override - public void showForwardIndicator(boolean visible) { - mForwardIcon.setVisibility(visible ? View.VISIBLE : View.GONE); - } - - /** - * Changes the visibility of the spam icon. - * - * @param visible {@code true} if the UI should show the spam icon. - */ - @Override - public void showSpamIndicator(boolean visible) { - if (visible) { - mSpamIcon.setVisibility(View.VISIBLE); - mNumberLabel.setText(R.string.label_spam_caller); - mPhoneNumber.setVisibility(View.GONE); - } - } - - /** - * Changes the visibility of the "manage conference call" button. - * - * @param visible Whether to set the button to be visible or not. - */ - @Override - public void showManageConferenceCallButton(boolean visible) { - mManageConferenceCallButton.setVisibility(visible ? View.VISIBLE : View.GONE); - } - - /** - * Determines the current visibility of the manage conference button. - * - * @return {@code true} if the button is visible. - */ - @Override - public boolean isManageConferenceVisible() { - return mManageConferenceCallButton.getVisibility() == View.VISIBLE; - } - - /** - * Determines the current visibility of the call subject. - * - * @return {@code true} if the subject is visible. - */ - @Override - public boolean isCallSubjectVisible() { - return mCallSubject.getVisibility() == View.VISIBLE; - } - - /** - * Get the overall InCallUI background colors and apply to call card. - */ - public void updateColors() { - MaterialPalette themeColors = InCallPresenter.getInstance().getThemeColors(); - - if (mCurrentThemeColors != null && mCurrentThemeColors.equals(themeColors)) { - return; - } - - if (getResources().getBoolean(R.bool.is_layout_landscape)) { - final GradientDrawable drawable = - (GradientDrawable) mPrimaryCallCardContainer.getBackground(); - drawable.setColor(themeColors.mPrimaryColor); - } else { - mPrimaryCallCardContainer.setBackgroundColor(themeColors.mPrimaryColor); - } - mCallButtonsContainer.setBackgroundColor(themeColors.mPrimaryColor); - mCallSubject.setTextColor(themeColors.mPrimaryColor); - mContactContext.setBackgroundColor(themeColors.mPrimaryColor); - //TODO: set color of message text in call context "recent messages" to be the theme color. - - mCurrentThemeColors = themeColors; - } - - private void dispatchPopulateAccessibilityEvent(AccessibilityEvent event, View view) { - if (view == null) return; - final List eventText = event.getText(); - int size = eventText.size(); - view.dispatchPopulateAccessibilityEvent(event); - // if no text added write null to keep relative position - if (size == eventText.size()) { - eventText.add(null); - } - } - - @Override - public void animateForNewOutgoingCall() { - final ViewGroup parent = (ViewGroup) mPrimaryCallCardContainer.getParent(); - - final ViewTreeObserver observer = getView().getViewTreeObserver(); - - mIsAnimating = true; - - observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - final ViewTreeObserver observer = getView().getViewTreeObserver(); - if (!observer.isAlive()) { - return; - } - observer.removeOnGlobalLayoutListener(this); - - final LayoutIgnoringListener listener = new LayoutIgnoringListener(); - mPrimaryCallCardContainer.addOnLayoutChangeListener(listener); - - // Prepare the state of views before the slide animation - final int originalHeight = mPrimaryCallCardContainer.getHeight(); - mPrimaryCallCardContainer.setTag(R.id.view_tag_callcard_actual_height, - originalHeight); - mPrimaryCallCardContainer.setBottom(parent.getHeight()); - - // Set up FAB. - mFloatingActionButtonContainer.setVisibility(View.GONE); - mFloatingActionButtonController.setScreenWidth(parent.getWidth()); - - mCallButtonsContainer.setAlpha(0); - mCallStateLabel.setAlpha(0); - mPrimaryName.setAlpha(0); - mCallTypeLabel.setAlpha(0); - mCallNumberAndLabel.setAlpha(0); - - assignTranslateAnimation(mCallStateLabel, 1); - assignTranslateAnimation(mCallStateIcon, 1); - assignTranslateAnimation(mPrimaryName, 2); - assignTranslateAnimation(mCallNumberAndLabel, 3); - assignTranslateAnimation(mCallTypeLabel, 4); - assignTranslateAnimation(mCallButtonsContainer, 5); - - final Animator animator = getShrinkAnimator(parent.getHeight(), originalHeight); - - animator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - mPrimaryCallCardContainer.setTag(R.id.view_tag_callcard_actual_height, - null); - setViewStatePostAnimation(listener); - mIsAnimating = false; - InCallPresenter.getInstance().onShrinkAnimationComplete(); - if (animator != null) { - animator.removeListener(this); - } - } - }); - animator.start(); - } - }); - } - - @Override - public void showNoteSentToast() { - Toast.makeText(getContext(), R.string.note_sent, Toast.LENGTH_LONG).show(); - } - - public void onDialpadVisibilityChange(boolean isShown) { - mIsDialpadShowing = isShown; - updateFabPosition(); - } - - private void updateFabPosition() { - int offsetY = 0; - if (!mIsDialpadShowing) { - offsetY = mFloatingActionButtonVerticalOffset; - if (mSecondaryCallInfo.isShown() && mHasLargePhoto) { - offsetY -= mSecondaryCallInfo.getHeight(); - } - } - - mFloatingActionButtonController.align( - FloatingActionButtonController.ALIGN_MIDDLE, - 0 /* offsetX */, - offsetY, - true); - - mFloatingActionButtonController.resize( - mIsDialpadShowing ? mFabSmallDiameter : mFabNormalDiameter, true); - } - - @Override - public Context getContext() { - return getActivity(); - } - - @Override - public void onResume() { - super.onResume(); - // If the previous launch animation is still running, cancel it so that we don't get - // stuck in an intermediate animation state. - if (mAnimatorSet != null && mAnimatorSet.isRunning()) { - mAnimatorSet.cancel(); - } - - mIsLandscape = getResources().getBoolean(R.bool.is_layout_landscape); - mHasLargePhoto = getResources().getBoolean(R.bool.has_large_photo); - - final ViewGroup parent = ((ViewGroup) mPrimaryCallCardContainer.getParent()); - final ViewTreeObserver observer = parent.getViewTreeObserver(); - parent.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - ViewTreeObserver viewTreeObserver = observer; - if (!viewTreeObserver.isAlive()) { - viewTreeObserver = parent.getViewTreeObserver(); - } - viewTreeObserver.removeOnGlobalLayoutListener(this); - mFloatingActionButtonController.setScreenWidth(parent.getWidth()); - updateFabPosition(); - } - }); - - updateColors(); - } - - /** - * Adds a global layout listener to update the FAB's positioning on the next layout. This allows - * us to position the FAB after the secondary call info's height has been calculated. - */ - private void updateFabPositionForSecondaryCallInfo() { - mSecondaryCallInfo.getViewTreeObserver().addOnGlobalLayoutListener( - new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - final ViewTreeObserver observer = mSecondaryCallInfo.getViewTreeObserver(); - if (!observer.isAlive()) { - return; - } - observer.removeOnGlobalLayoutListener(this); - - onDialpadVisibilityChange(mIsDialpadShowing); - } - }); - } - - /** - * Animator that performs the upwards shrinking animation of the blue call card scrim. - * At the start of the animation, each child view is moved downwards by a pre-specified amount - * and then translated upwards together with the scrim. - */ - private Animator getShrinkAnimator(int startHeight, int endHeight) { - final ObjectAnimator shrinkAnimator = - ObjectAnimator.ofInt(mPrimaryCallCardContainer, "bottom", startHeight, endHeight); - shrinkAnimator.setDuration(mShrinkAnimationDuration); - shrinkAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - mFloatingActionButton.setEnabled(true); - } - }); - shrinkAnimator.setInterpolator(AnimUtils.EASE_IN); - return shrinkAnimator; - } - - private void assignTranslateAnimation(View view, int offset) { - view.setLayerType(View.LAYER_TYPE_HARDWARE, null); - view.buildLayer(); - view.setTranslationY(mTranslationOffset * offset); - view.animate().translationY(0).alpha(1).withLayer() - .setDuration(mShrinkAnimationDuration).setInterpolator(AnimUtils.EASE_IN); - } - - private void setViewStatePostAnimation(View view) { - view.setTranslationY(0); - view.setAlpha(1); - } - - private void setViewStatePostAnimation(OnLayoutChangeListener layoutChangeListener) { - setViewStatePostAnimation(mCallButtonsContainer); - setViewStatePostAnimation(mCallStateLabel); - setViewStatePostAnimation(mPrimaryName); - setViewStatePostAnimation(mCallTypeLabel); - setViewStatePostAnimation(mCallNumberAndLabel); - setViewStatePostAnimation(mCallStateIcon); - - mPrimaryCallCardContainer.removeOnLayoutChangeListener(layoutChangeListener); - - mFloatingActionButtonController.scaleIn(AnimUtils.NO_DELAY); - } - - private final class LayoutIgnoringListener implements View.OnLayoutChangeListener { - @Override - public void onLayoutChange(View v, - int left, - int top, - int right, - int bottom, - int oldLeft, - int oldTop, - int oldRight, - int oldBottom) { - v.setLeft(oldLeft); - v.setRight(oldRight); - v.setTop(oldTop); - v.setBottom(oldBottom); - } - } -} diff --git a/InCallUI/src/com/android/incallui/CallCardPresenter.java b/InCallUI/src/com/android/incallui/CallCardPresenter.java deleted file mode 100644 index 1ad0c11f1..000000000 --- a/InCallUI/src/com/android/incallui/CallCardPresenter.java +++ /dev/null @@ -1,1181 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import com.google.common.base.Preconditions; - -import android.Manifest; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.os.Bundle; -import android.support.annotation.Nullable; -import android.telecom.Call.Details; -import android.telecom.DisconnectCause; -import android.telecom.PhoneAccount; -import android.telecom.PhoneAccountHandle; -import android.telecom.StatusHints; -import android.telecom.TelecomManager; -import android.telecom.VideoProfile; -import android.telephony.PhoneNumberUtils; -import android.text.TextUtils; -import android.view.View; -import android.view.accessibility.AccessibilityManager; -import android.widget.ListAdapter; - -import com.android.contacts.common.ContactsUtils; -import com.android.contacts.common.compat.telecom.TelecomManagerCompat; -import com.android.contacts.common.preference.ContactsPreferences; -import com.android.contacts.common.testing.NeededForTesting; -import com.android.contacts.common.util.ContactDisplayUtils; -import com.android.dialer.R; -import com.android.incallui.Call.State; -import com.android.incallui.ContactInfoCache.ContactCacheEntry; -import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback; -import com.android.incallui.InCallPresenter.InCallDetailsListener; -import com.android.incallui.InCallPresenter.InCallEventListener; -import com.android.incallui.InCallPresenter.InCallState; -import com.android.incallui.InCallPresenter.InCallStateListener; -import com.android.incallui.InCallPresenter.IncomingCallListener; -import com.android.incalluibind.ObjectFactory; - -import java.lang.ref.WeakReference; - -import static com.android.contacts.common.compat.CallSdkCompat.Details.PROPERTY_ENTERPRISE_CALL; -/** - * Presenter for the Call Card Fragment. - *

- * This class listens for changes to InCallState and passes it along to the fragment. - */ -public class CallCardPresenter extends Presenter - implements InCallStateListener, IncomingCallListener, InCallDetailsListener, - InCallEventListener, CallList.CallUpdateListener, DistanceHelper.Listener { - - public interface EmergencyCallListener { - public void onCallUpdated(BaseFragment fragment, boolean isEmergency); - } - - private static final String TAG = CallCardPresenter.class.getSimpleName(); - private static final long CALL_TIME_UPDATE_INTERVAL_MS = 1000; - - private final EmergencyCallListener mEmergencyCallListener = - ObjectFactory.newEmergencyCallListener(); - private DistanceHelper mDistanceHelper; - - private Call mPrimary; - private Call mSecondary; - private ContactCacheEntry mPrimaryContactInfo; - private ContactCacheEntry mSecondaryContactInfo; - private CallTimer mCallTimer; - private Context mContext; - @Nullable private ContactsPreferences mContactsPreferences; - private boolean mSpinnerShowing = false; - private boolean mHasShownToast = false; - private InCallContactInteractions mInCallContactInteractions; - private boolean mIsFullscreen = false; - - public static class ContactLookupCallback implements ContactInfoCacheCallback { - private final WeakReference mCallCardPresenter; - private final boolean mIsPrimary; - - public ContactLookupCallback(CallCardPresenter callCardPresenter, boolean isPrimary) { - mCallCardPresenter = new WeakReference(callCardPresenter); - mIsPrimary = isPrimary; - } - - @Override - public void onContactInfoComplete(String callId, ContactCacheEntry entry) { - CallCardPresenter presenter = mCallCardPresenter.get(); - if (presenter != null) { - presenter.onContactInfoComplete(callId, entry, mIsPrimary); - } - } - - @Override - public void onImageLoadComplete(String callId, ContactCacheEntry entry) { - CallCardPresenter presenter = mCallCardPresenter.get(); - if (presenter != null) { - presenter.onImageLoadComplete(callId, entry); - } - } - - @Override - public void onContactInteractionsInfoComplete(String callId, ContactCacheEntry entry) { - CallCardPresenter presenter = mCallCardPresenter.get(); - if (presenter != null) { - presenter.onContactInteractionsInfoComplete(callId, entry); - } - } - } - - public CallCardPresenter() { - // create the call timer - mCallTimer = new CallTimer(new Runnable() { - @Override - public void run() { - updateCallTime(); - } - }); - } - - public void init(Context context, Call call) { - mContext = Preconditions.checkNotNull(context); - mDistanceHelper = ObjectFactory.newDistanceHelper(mContext, this); - mContactsPreferences = ContactsPreferencesFactory.newContactsPreferences(mContext); - - // Call may be null if disconnect happened already. - if (call != null) { - mPrimary = call; - if (shouldShowNoteSentToast(mPrimary)) { - final CallCardUi ui = getUi(); - if (ui != null) { - ui.showNoteSentToast(); - } - } - CallList.getInstance().addCallUpdateListener(call.getId(), this); - - // start processing lookups right away. - if (!call.isConferenceCall()) { - startContactInfoSearch(call, true, call.getState() == Call.State.INCOMING); - } else { - updateContactEntry(null, true); - } - } - - onStateChange(null, InCallPresenter.getInstance().getInCallState(), CallList.getInstance()); - } - - @Override - public void onUiReady(CallCardUi ui) { - super.onUiReady(ui); - - if (mContactsPreferences != null) { - mContactsPreferences.refreshValue(ContactsPreferences.DISPLAY_ORDER_KEY); - } - - // Contact search may have completed before ui is ready. - if (mPrimaryContactInfo != null) { - updatePrimaryDisplayInfo(); - } - - // Register for call state changes last - InCallPresenter.getInstance().addListener(this); - InCallPresenter.getInstance().addIncomingCallListener(this); - InCallPresenter.getInstance().addDetailsListener(this); - InCallPresenter.getInstance().addInCallEventListener(this); - } - - @Override - public void onUiUnready(CallCardUi ui) { - super.onUiUnready(ui); - - // stop getting call state changes - InCallPresenter.getInstance().removeListener(this); - InCallPresenter.getInstance().removeIncomingCallListener(this); - InCallPresenter.getInstance().removeDetailsListener(this); - InCallPresenter.getInstance().removeInCallEventListener(this); - if (mPrimary != null) { - CallList.getInstance().removeCallUpdateListener(mPrimary.getId(), this); - } - - if (mDistanceHelper != null) { - mDistanceHelper.cleanUp(); - } - - mPrimary = null; - mPrimaryContactInfo = null; - mSecondaryContactInfo = null; - } - - @Override - public void onIncomingCall(InCallState oldState, InCallState newState, Call call) { - // same logic should happen as with onStateChange() - onStateChange(oldState, newState, CallList.getInstance()); - } - - @Override - public void onStateChange(InCallState oldState, InCallState newState, CallList callList) { - Log.d(this, "onStateChange() " + newState); - final CallCardUi ui = getUi(); - if (ui == null) { - return; - } - - Call primary = null; - Call secondary = null; - - if (newState == InCallState.INCOMING) { - primary = callList.getIncomingCall(); - } else if (newState == InCallState.PENDING_OUTGOING || newState == InCallState.OUTGOING) { - primary = callList.getOutgoingCall(); - if (primary == null) { - primary = callList.getPendingOutgoingCall(); - } - - // getCallToDisplay doesn't go through outgoing or incoming calls. It will return the - // highest priority call to display as the secondary call. - secondary = getCallToDisplay(callList, null, true); - } else if (newState == InCallState.INCALL) { - primary = getCallToDisplay(callList, null, false); - secondary = getCallToDisplay(callList, primary, true); - } - - if (mInCallContactInteractions != null && - (oldState == InCallState.INCOMING || newState == InCallState.INCOMING)) { - ui.showContactContext(newState != InCallState.INCOMING); - } - - Log.d(this, "Primary call: " + primary); - Log.d(this, "Secondary call: " + secondary); - - final boolean primaryChanged = !(Call.areSame(mPrimary, primary) && - Call.areSameNumber(mPrimary, primary)); - final boolean secondaryChanged = !(Call.areSame(mSecondary, secondary) && - Call.areSameNumber(mSecondary, secondary)); - - mSecondary = secondary; - Call previousPrimary = mPrimary; - mPrimary = primary; - - if (primaryChanged && shouldShowNoteSentToast(primary)) { - ui.showNoteSentToast(); - } - - // Refresh primary call information if either: - // 1. Primary call changed. - // 2. The call's ability to manage conference has changed. - // 3. The call subject should be shown or hidden. - if (shouldRefreshPrimaryInfo(primaryChanged, ui, shouldShowCallSubject(mPrimary))) { - // primary call has changed - if (previousPrimary != null) { - //clear progess spinner (if any) related to previous primary call - maybeShowProgressSpinner(previousPrimary.getState(), - Call.SessionModificationState.NO_REQUEST); - CallList.getInstance().removeCallUpdateListener(previousPrimary.getId(), this); - } - CallList.getInstance().addCallUpdateListener(mPrimary.getId(), this); - - mPrimaryContactInfo = ContactInfoCache.buildCacheEntryFromCall(mContext, mPrimary, - mPrimary.getState() == Call.State.INCOMING); - updatePrimaryDisplayInfo(); - maybeStartSearch(mPrimary, true); - maybeClearSessionModificationState(mPrimary); - } - - if (previousPrimary != null && mPrimary == null) { - //clear progess spinner (if any) related to previous primary call - maybeShowProgressSpinner(previousPrimary.getState(), - Call.SessionModificationState.NO_REQUEST); - CallList.getInstance().removeCallUpdateListener(previousPrimary.getId(), this); - } - - if (mSecondary == null) { - // Secondary call may have ended. Update the ui. - mSecondaryContactInfo = null; - updateSecondaryDisplayInfo(); - } else if (secondaryChanged) { - // secondary call has changed - mSecondaryContactInfo = ContactInfoCache.buildCacheEntryFromCall(mContext, mSecondary, - mSecondary.getState() == Call.State.INCOMING); - updateSecondaryDisplayInfo(); - maybeStartSearch(mSecondary, false); - maybeClearSessionModificationState(mSecondary); - } - - // Start/stop timers. - if (isPrimaryCallActive()) { - Log.d(this, "Starting the calltime timer"); - mCallTimer.start(CALL_TIME_UPDATE_INTERVAL_MS); - } else { - Log.d(this, "Canceling the calltime timer"); - mCallTimer.cancel(); - ui.setPrimaryCallElapsedTime(false, 0); - } - - // Set the call state - int callState = Call.State.IDLE; - if (mPrimary != null) { - callState = mPrimary.getState(); - updatePrimaryCallState(); - } else { - getUi().setCallState( - callState, - VideoProfile.STATE_AUDIO_ONLY, - Call.SessionModificationState.NO_REQUEST, - new DisconnectCause(DisconnectCause.UNKNOWN), - null, - null, - null, - false /* isWifi */, - false /* isConference */, - false /* isWorkCall */); - getUi().showHdAudioIndicator(false); - } - - maybeShowManageConferenceCallButton(); - - // Hide the end call button instantly if we're receiving an incoming call. - getUi().setEndCallButtonEnabled(shouldShowEndCallButton(mPrimary, callState), - callState != Call.State.INCOMING /* animate */); - - maybeSendAccessibilityEvent(oldState, newState, primaryChanged); - } - - @Override - public void onDetailsChanged(Call call, Details details) { - updatePrimaryCallState(); - - if (call.can(Details.CAPABILITY_MANAGE_CONFERENCE) != - details.can(Details.CAPABILITY_MANAGE_CONFERENCE)) { - maybeShowManageConferenceCallButton(); - } - } - - @Override - public void onCallChanged(Call call) { - // No-op; specific call updates handled elsewhere. - } - - /** - * Handles a change to the session modification state for a call. Triggers showing the progress - * spinner, as well as updating the call state label. - * - * @param sessionModificationState The new session modification state. - */ - @Override - public void onSessionModificationStateChange(int sessionModificationState) { - Log.d(this, "onSessionModificationStateChange : sessionModificationState = " + - sessionModificationState); - - if (mPrimary == null) { - return; - } - maybeShowProgressSpinner(mPrimary.getState(), sessionModificationState); - getUi().setEndCallButtonEnabled(sessionModificationState != - Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST, - true /* shouldAnimate */); - updatePrimaryCallState(); - } - - /** - * Handles a change to the last forwarding number by refreshing the primary call info. - */ - @Override - public void onLastForwardedNumberChange() { - Log.v(this, "onLastForwardedNumberChange"); - - if (mPrimary == null) { - return; - } - updatePrimaryDisplayInfo(); - } - - /** - * Handles a change to the child number by refreshing the primary call info. - */ - @Override - public void onChildNumberChange() { - Log.v(this, "onChildNumberChange"); - - if (mPrimary == null) { - return; - } - updatePrimaryDisplayInfo(); - } - - private boolean shouldRefreshPrimaryInfo(boolean primaryChanged, CallCardUi ui, - boolean shouldShowCallSubject) { - if (mPrimary == null) { - return false; - } - return primaryChanged || - ui.isManageConferenceVisible() != shouldShowManageConference() || - ui.isCallSubjectVisible() != shouldShowCallSubject; - } - - private String getSubscriptionNumber() { - // If it's an emergency call, and they're not populating the callback number, - // then try to fall back to the phone sub info (to hopefully get the SIM's - // number directly from the telephony layer). - PhoneAccountHandle accountHandle = mPrimary.getAccountHandle(); - if (accountHandle != null) { - TelecomManager mgr = InCallPresenter.getInstance().getTelecomManager(); - PhoneAccount account = TelecomManagerCompat.getPhoneAccount(mgr, accountHandle); - if (account != null) { - return getNumberFromHandle(account.getSubscriptionAddress()); - } - } - return null; - } - - private void updatePrimaryCallState() { - if (getUi() != null && mPrimary != null) { - boolean isWorkCall = mPrimary.hasProperty(PROPERTY_ENTERPRISE_CALL) - || (mPrimaryContactInfo == null ? false - : mPrimaryContactInfo.userType == ContactsUtils.USER_TYPE_WORK); - getUi().setCallState( - mPrimary.getState(), - mPrimary.getVideoState(), - mPrimary.getSessionModificationState(), - mPrimary.getDisconnectCause(), - getConnectionLabel(), - getCallStateIcon(), - getGatewayNumber(), - mPrimary.hasProperty(Details.PROPERTY_WIFI), - mPrimary.isConferenceCall(), - isWorkCall); - - maybeShowHdAudioIcon(); - setCallbackNumber(); - } - } - - /** - * Show the HD icon if the call is active and has {@link Details#PROPERTY_HIGH_DEF_AUDIO}, - * except if the call has a last forwarded number (we will show that icon instead). - */ - private void maybeShowHdAudioIcon() { - boolean showHdAudioIndicator = - isPrimaryCallActive() && mPrimary.hasProperty(Details.PROPERTY_HIGH_DEF_AUDIO) && - TextUtils.isEmpty(mPrimary.getLastForwardedNumber()); - getUi().showHdAudioIndicator(showHdAudioIndicator); - } - - private void maybeShowSpamIconAndLabel() { - getUi().showSpamIndicator(mPrimary.isSpam()); - } - - /** - * Only show the conference call button if we can manage the conference. - */ - private void maybeShowManageConferenceCallButton() { - getUi().showManageConferenceCallButton(shouldShowManageConference()); - } - - /** - * Determines if a pending session modification exists for the current call. If so, the - * progress spinner is shown, and the call state is updated. - * - * @param callState The call state. - * @param sessionModificationState The session modification state. - */ - private void maybeShowProgressSpinner(int callState, int sessionModificationState) { - final boolean show = sessionModificationState == - Call.SessionModificationState.WAITING_FOR_RESPONSE - && callState == Call.State.ACTIVE; - if (show != mSpinnerShowing) { - getUi().setProgressSpinnerVisible(show); - mSpinnerShowing = show; - } - } - - /** - * Determines if the manage conference button should be visible, based on the current primary - * call. - * - * @return {@code True} if the manage conference button should be visible. - */ - private boolean shouldShowManageConference() { - if (mPrimary == null) { - return false; - } - - return mPrimary.can(android.telecom.Call.Details.CAPABILITY_MANAGE_CONFERENCE) - && !mIsFullscreen; - } - - private void setCallbackNumber() { - String callbackNumber = null; - - // Show the emergency callback number if either: - // 1. This is an emergency call. - // 2. The phone is in Emergency Callback Mode, which means we should show the callback - // number. - boolean showCallbackNumber = mPrimary.hasProperty(Details.PROPERTY_EMERGENCY_CALLBACK_MODE); - - if (mPrimary.isEmergencyCall() || showCallbackNumber) { - callbackNumber = getSubscriptionNumber(); - } else { - StatusHints statusHints = mPrimary.getTelecomCall().getDetails().getStatusHints(); - if (statusHints != null) { - Bundle extras = statusHints.getExtras(); - if (extras != null) { - callbackNumber = extras.getString(TelecomManager.EXTRA_CALL_BACK_NUMBER); - } - } - } - - final String simNumber = TelecomManagerCompat.getLine1Number( - InCallPresenter.getInstance().getTelecomManager(), - InCallPresenter.getInstance().getTelephonyManager(), - mPrimary.getAccountHandle()); - if (!showCallbackNumber && PhoneNumberUtils.compare(callbackNumber, simNumber)) { - Log.d(this, "Numbers are the same (and callback number is not being forced to show);" + - " not showing the callback number"); - callbackNumber = null; - } - - getUi().setCallbackNumber(callbackNumber, mPrimary.isEmergencyCall() || showCallbackNumber); - } - - public void updateCallTime() { - final CallCardUi ui = getUi(); - - if (ui == null) { - mCallTimer.cancel(); - } else if (!isPrimaryCallActive()) { - ui.setPrimaryCallElapsedTime(false, 0); - mCallTimer.cancel(); - } else { - final long callStart = mPrimary.getConnectTimeMillis(); - if (callStart > 0) { - final long duration = System.currentTimeMillis() - callStart; - ui.setPrimaryCallElapsedTime(true, duration); - } - } - } - - public void onCallStateButtonTouched() { - Intent broadcastIntent = ObjectFactory.getCallStateButtonBroadcastIntent(mContext); - if (broadcastIntent != null) { - Log.d(this, "Sending call state button broadcast: ", broadcastIntent); - mContext.sendBroadcast(broadcastIntent, Manifest.permission.READ_PHONE_STATE); - } - } - - /** - * Handles click on the contact photo by toggling fullscreen mode if the current call is a video - * call. - */ - public void onContactPhotoClick() { - if (mPrimary != null && mPrimary.isVideoCall(mContext)) { - InCallPresenter.getInstance().toggleFullscreenMode(); - } - } - - private void maybeStartSearch(Call call, boolean isPrimary) { - // no need to start search for conference calls which show generic info. - if (call != null && !call.isConferenceCall()) { - startContactInfoSearch(call, isPrimary, call.getState() == Call.State.INCOMING); - } - } - - private void maybeClearSessionModificationState(Call call) { - if (call.getSessionModificationState() != - Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) { - call.setSessionModificationState(Call.SessionModificationState.NO_REQUEST); - } - } - - /** - * Starts a query for more contact data for the save primary and secondary calls. - */ - private void startContactInfoSearch(final Call call, final boolean isPrimary, - boolean isIncoming) { - final ContactInfoCache cache = ContactInfoCache.getInstance(mContext); - - cache.findInfo(call, isIncoming, new ContactLookupCallback(this, isPrimary)); - } - - private void onContactInfoComplete(String callId, ContactCacheEntry entry, boolean isPrimary) { - final boolean entryMatchesExistingCall = - (isPrimary && mPrimary != null && TextUtils.equals(callId, mPrimary.getId())) || - (!isPrimary && mSecondary != null && TextUtils.equals(callId, mSecondary.getId())); - if (entryMatchesExistingCall) { - updateContactEntry(entry, isPrimary); - } else { - Log.w(this, "Dropping stale contact lookup info for " + callId); - } - - final Call call = CallList.getInstance().getCallById(callId); - if (call != null) { - call.getLogState().contactLookupResult = entry.contactLookupResult; - } - if (entry.contactUri != null) { - CallerInfoUtils.sendViewNotification(mContext, entry.contactUri); - } - } - - private void onImageLoadComplete(String callId, ContactCacheEntry entry) { - if (getUi() == null) { - return; - } - - if (entry.photo != null) { - if (mPrimary != null && callId.equals(mPrimary.getId())) { - boolean showContactPhoto = !VideoCallPresenter.showIncomingVideo( - mPrimary.getVideoState(), mPrimary.getState()); - getUi().setPrimaryImage(entry.photo, showContactPhoto); - } - } - } - - private void onContactInteractionsInfoComplete(String callId, ContactCacheEntry entry) { - if (getUi() == null) { - return; - } - - if (mPrimary != null && callId.equals(mPrimary.getId())) { - mPrimaryContactInfo.locationAddress = entry.locationAddress; - updateContactInteractions(); - } - } - - @Override - public void onLocationReady() { - // This will only update the contacts interactions data if the location returns after - // the contact information is found. - updateContactInteractions(); - } - - private void updateContactInteractions() { - if (mPrimary != null && mPrimaryContactInfo != null - && (mPrimaryContactInfo.locationAddress != null - || mPrimaryContactInfo.openingHours != null)) { - // TODO: This is hardcoded to "isBusiness" because functionality to differentiate - // between business and personal has not yet been added. - if (setInCallContactInteractionsType(true /* isBusiness */)) { - getUi().setContactContextTitle( - mInCallContactInteractions.getBusinessListHeaderView()); - } - - mInCallContactInteractions.setBusinessInfo( - mPrimaryContactInfo.locationAddress, - mDistanceHelper.calculateDistance(mPrimaryContactInfo.locationAddress), - mPrimaryContactInfo.openingHours); - getUi().setContactContextContent(mInCallContactInteractions.getListAdapter()); - getUi().showContactContext(mPrimary.getState() != State.INCOMING); - } else { - getUi().showContactContext(false); - } - } - - /** - * Update the contact interactions type so that the correct UI is shown. - * - * @param isBusiness {@code true} if the interaction is a business interaction, {@code false} if - * it is a personal contact. - * - * @return {@code true} if this is a new type of contact interaction (business or personal). - * {@code false} if it hasn't changed. - */ - private boolean setInCallContactInteractionsType(boolean isBusiness) { - if (mInCallContactInteractions == null) { - mInCallContactInteractions = - new InCallContactInteractions(mContext, isBusiness); - return true; - } - - return mInCallContactInteractions.switchContactType(isBusiness); - } - - private void updateContactEntry(ContactCacheEntry entry, boolean isPrimary) { - if (isPrimary) { - mPrimaryContactInfo = entry; - updatePrimaryDisplayInfo(); - } else { - mSecondaryContactInfo = entry; - updateSecondaryDisplayInfo(); - } - } - - /** - * Get the highest priority call to display. - * Goes through the calls and chooses which to return based on priority of which type of call - * to display to the user. Callers can use the "ignore" feature to get the second best call - * by passing a previously found primary call as ignore. - * - * @param ignore A call to ignore if found. - */ - private Call getCallToDisplay(CallList callList, Call ignore, boolean skipDisconnected) { - // Active calls come second. An active call always gets precedent. - Call retval = callList.getActiveCall(); - if (retval != null && retval != ignore) { - return retval; - } - - // Sometimes there is intemediate state that two calls are in active even one is about - // to be on hold. - retval = callList.getSecondActiveCall(); - if (retval != null && retval != ignore) { - return retval; - } - - // Disconnected calls get primary position if there are no active calls - // to let user know quickly what call has disconnected. Disconnected - // calls are very short lived. - if (!skipDisconnected) { - retval = callList.getDisconnectingCall(); - if (retval != null && retval != ignore) { - return retval; - } - retval = callList.getDisconnectedCall(); - if (retval != null && retval != ignore) { - return retval; - } - } - - // Then we go to background call (calls on hold) - retval = callList.getBackgroundCall(); - if (retval != null && retval != ignore) { - return retval; - } - - // Lastly, we go to a second background call. - retval = callList.getSecondBackgroundCall(); - - return retval; - } - - private void updatePrimaryDisplayInfo() { - final CallCardUi ui = getUi(); - if (ui == null) { - // TODO: May also occur if search result comes back after ui is destroyed. Look into - // removing that case completely. - Log.d(TAG, "updatePrimaryDisplayInfo called but ui is null!"); - return; - } - - if (mPrimary == null) { - // Clear the primary display info. - ui.setPrimary(null, null, false, null, null, false, false, false); - return; - } - - // Hide the contact photo if we are in a video call and the incoming video surface is - // showing. - boolean showContactPhoto = !VideoCallPresenter - .showIncomingVideo(mPrimary.getVideoState(), mPrimary.getState()); - - // Call placed through a work phone account. - boolean hasWorkCallProperty = mPrimary.hasProperty(PROPERTY_ENTERPRISE_CALL); - - if (mPrimary.isConferenceCall()) { - Log.d(TAG, "Update primary display info for conference call."); - - ui.setPrimary( - null /* number */, - getConferenceString(mPrimary), - false /* nameIsNumber */, - null /* label */, - getConferencePhoto(mPrimary), - false /* isSipCall */, - showContactPhoto, - hasWorkCallProperty); - } else if (mPrimaryContactInfo != null) { - Log.d(TAG, "Update primary display info for " + mPrimaryContactInfo); - - String name = getNameForCall(mPrimaryContactInfo); - String number; - - boolean isChildNumberShown = !TextUtils.isEmpty(mPrimary.getChildNumber()); - boolean isForwardedNumberShown = !TextUtils.isEmpty(mPrimary.getLastForwardedNumber()); - boolean isCallSubjectShown = shouldShowCallSubject(mPrimary); - - if (isCallSubjectShown) { - ui.setCallSubject(mPrimary.getCallSubject()); - } else { - ui.setCallSubject(null); - } - - if (isCallSubjectShown) { - number = null; - } else if (isChildNumberShown) { - number = mContext.getString(R.string.child_number, mPrimary.getChildNumber()); - } else if (isForwardedNumberShown) { - // Use last forwarded number instead of second line, if present. - number = mPrimary.getLastForwardedNumber(); - } else { - number = getNumberForCall(mPrimaryContactInfo); - } - - ui.showForwardIndicator(isForwardedNumberShown); - maybeShowHdAudioIcon(); - - boolean nameIsNumber = name != null && name.equals(mPrimaryContactInfo.number); - // Call with caller that is a work contact. - boolean isWorkContact = (mPrimaryContactInfo.userType == ContactsUtils.USER_TYPE_WORK); - ui.setPrimary( - number, - name, - nameIsNumber, - isChildNumberShown || isCallSubjectShown ? null : mPrimaryContactInfo.label, - mPrimaryContactInfo.photo, - mPrimaryContactInfo.isSipCall, - showContactPhoto, - hasWorkCallProperty || isWorkContact); - - updateContactInteractions(); - } else { - // Clear the primary display info. - ui.setPrimary(null, null, false, null, null, false, false, false); - } - - if (mEmergencyCallListener != null) { - boolean isEmergencyCall = mPrimary.isEmergencyCall(); - mEmergencyCallListener.onCallUpdated((BaseFragment) ui, isEmergencyCall); - } - maybeShowSpamIconAndLabel(); - } - - private void updateSecondaryDisplayInfo() { - final CallCardUi ui = getUi(); - if (ui == null) { - return; - } - - if (mSecondary == null) { - // Clear the secondary display info. - ui.setSecondary(false, null, false, null, null, false /* isConference */, - false /* isVideoCall */, mIsFullscreen); - return; - } - - if (mSecondary.isConferenceCall()) { - ui.setSecondary( - true /* show */, - getConferenceString(mSecondary), - false /* nameIsNumber */, - null /* label */, - getCallProviderLabel(mSecondary), - true /* isConference */, - mSecondary.isVideoCall(mContext), - mIsFullscreen); - } else if (mSecondaryContactInfo != null) { - Log.d(TAG, "updateSecondaryDisplayInfo() " + mSecondaryContactInfo); - String name = getNameForCall(mSecondaryContactInfo); - boolean nameIsNumber = name != null && name.equals(mSecondaryContactInfo.number); - ui.setSecondary( - true /* show */, - name, - nameIsNumber, - mSecondaryContactInfo.label, - getCallProviderLabel(mSecondary), - false /* isConference */, - mSecondary.isVideoCall(mContext), - mIsFullscreen); - } else { - // Clear the secondary display info. - ui.setSecondary(false, null, false, null, null, false /* isConference */, - false /* isVideoCall */, mIsFullscreen); - } - } - - - /** - * Gets the phone account to display for a call. - */ - private PhoneAccount getAccountForCall(Call call) { - PhoneAccountHandle accountHandle = call.getAccountHandle(); - if (accountHandle == null) { - return null; - } - return TelecomManagerCompat.getPhoneAccount( - InCallPresenter.getInstance().getTelecomManager(), - accountHandle); - } - - /** - * Returns the gateway number for any existing outgoing call. - */ - private String getGatewayNumber() { - if (hasOutgoingGatewayCall()) { - return getNumberFromHandle(mPrimary.getGatewayInfo().getGatewayAddress()); - } - return null; - } - - /** - * Return the string label to represent the call provider - */ - private String getCallProviderLabel(Call call) { - PhoneAccount account = getAccountForCall(call); - TelecomManager mgr = InCallPresenter.getInstance().getTelecomManager(); - if (account != null && !TextUtils.isEmpty(account.getLabel()) - && TelecomManagerCompat.getCallCapablePhoneAccounts(mgr).size() > 1) { - return account.getLabel().toString(); - } - return null; - } - - /** - * Returns the label (line of text above the number/name) for any given call. - * For example, "calling via [Account/Google Voice]" for outgoing calls. - */ - private String getConnectionLabel() { - StatusHints statusHints = mPrimary.getTelecomCall().getDetails().getStatusHints(); - if (statusHints != null && !TextUtils.isEmpty(statusHints.getLabel())) { - return statusHints.getLabel().toString(); - } - - if (hasOutgoingGatewayCall() && getUi() != null) { - // Return the label for the gateway app on outgoing calls. - final PackageManager pm = mContext.getPackageManager(); - try { - ApplicationInfo info = pm.getApplicationInfo( - mPrimary.getGatewayInfo().getGatewayProviderPackageName(), 0); - return pm.getApplicationLabel(info).toString(); - } catch (PackageManager.NameNotFoundException e) { - Log.e(this, "Gateway Application Not Found.", e); - return null; - } - } - return getCallProviderLabel(mPrimary); - } - - private Drawable getCallStateIcon() { - // Return connection icon if one exists. - StatusHints statusHints = mPrimary.getTelecomCall().getDetails().getStatusHints(); - if (statusHints != null && statusHints.getIcon() != null) { - Drawable icon = statusHints.getIcon().loadDrawable(mContext); - if (icon != null) { - return icon; - } - } - - return null; - } - - private boolean hasOutgoingGatewayCall() { - // We only display the gateway information while STATE_DIALING so return false for any other - // call state. - // TODO: mPrimary can be null because this is called from updatePrimaryDisplayInfo which - // is also called after a contact search completes (call is not present yet). Split the - // UI update so it can receive independent updates. - if (mPrimary == null) { - return false; - } - return Call.State.isDialing(mPrimary.getState()) && mPrimary.getGatewayInfo() != null && - !mPrimary.getGatewayInfo().isEmpty(); - } - - /** - * Gets the name to display for the call. - */ - @NeededForTesting - String getNameForCall(ContactCacheEntry contactInfo) { - String preferredName = ContactDisplayUtils.getPreferredDisplayName( - contactInfo.namePrimary, - contactInfo.nameAlternative, - mContactsPreferences); - if (TextUtils.isEmpty(preferredName)) { - return contactInfo.number; - } - return preferredName; - } - - /** - * Gets the number to display for a call. - */ - @NeededForTesting - String getNumberForCall(ContactCacheEntry contactInfo) { - // If the name is empty, we use the number for the name...so don't show a second - // number in the number field - String preferredName = ContactDisplayUtils.getPreferredDisplayName( - contactInfo.namePrimary, - contactInfo.nameAlternative, - mContactsPreferences); - if (TextUtils.isEmpty(preferredName)) { - return contactInfo.location; - } - return contactInfo.number; - } - - public void secondaryInfoClicked() { - if (mSecondary == null) { - Log.w(this, "Secondary info clicked but no secondary call."); - return; - } - - Log.i(this, "Swapping call to foreground: " + mSecondary); - TelecomAdapter.getInstance().unholdCall(mSecondary.getId()); - } - - public void endCallClicked() { - if (mPrimary == null) { - return; - } - - Log.i(this, "Disconnecting call: " + mPrimary); - final String callId = mPrimary.getId(); - mPrimary.setState(Call.State.DISCONNECTING); - CallList.getInstance().onUpdate(mPrimary); - TelecomAdapter.getInstance().disconnectCall(callId); - } - - private String getNumberFromHandle(Uri handle) { - return handle == null ? "" : handle.getSchemeSpecificPart(); - } - - /** - * Handles a change to the fullscreen mode of the in-call UI. - * - * @param isFullscreenMode {@code True} if the in-call UI is entering full screen mode. - */ - @Override - public void onFullscreenModeChanged(boolean isFullscreenMode) { - mIsFullscreen = isFullscreenMode; - final CallCardUi ui = getUi(); - if (ui == null) { - return; - } - ui.setCallCardVisible(!isFullscreenMode); - ui.setSecondaryInfoVisible(!isFullscreenMode); - maybeShowManageConferenceCallButton(); - } - - @Override - public void onSecondaryCallerInfoVisibilityChanged(boolean isVisible, int height) { - // No-op - the Call Card is the origin of this event. - } - - private boolean isPrimaryCallActive() { - return mPrimary != null && mPrimary.getState() == Call.State.ACTIVE; - } - - private String getConferenceString(Call call) { - boolean isGenericConference = call.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE); - Log.v(this, "getConferenceString: " + isGenericConference); - - final int resId = isGenericConference - ? R.string.card_title_in_call : R.string.card_title_conf_call; - return mContext.getResources().getString(resId); - } - - private Drawable getConferencePhoto(Call call) { - boolean isGenericConference = call.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE); - Log.v(this, "getConferencePhoto: " + isGenericConference); - - final int resId = isGenericConference - ? R.drawable.img_phone : R.drawable.img_conference; - Drawable photo = mContext.getResources().getDrawable(resId); - photo.setAutoMirrored(true); - return photo; - } - - private boolean shouldShowEndCallButton(Call primary, int callState) { - if (primary == null) { - return false; - } - if ((!Call.State.isConnectingOrConnected(callState) - && callState != Call.State.DISCONNECTING) || callState == Call.State.INCOMING) { - return false; - } - if (mPrimary.getSessionModificationState() - == Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) { - return false; - } - return true; - } - - private void maybeSendAccessibilityEvent(InCallState oldState, InCallState newState, - boolean primaryChanged) { - if (mContext == null) { - return; - } - final AccessibilityManager am = (AccessibilityManager) mContext.getSystemService( - Context.ACCESSIBILITY_SERVICE); - if (!am.isEnabled()) { - return; - } - // Announce the current call if it's new incoming/outgoing call or primary call is changed - // due to switching calls between two ongoing calls (one is on hold). - if ((oldState != InCallState.OUTGOING && newState == InCallState.OUTGOING) - || (oldState != InCallState.INCOMING && newState == InCallState.INCOMING) - || primaryChanged) { - if (getUi() != null) { - getUi().sendAccessibilityAnnouncement(); - } - } - } - - /** - * Determines whether the call subject should be visible on the UI. For the call subject to be - * visible, the call has to be in an incoming or waiting state, and the subject must not be - * empty. - * - * @param call The call. - * @return {@code true} if the subject should be shown, {@code false} otherwise. - */ - private boolean shouldShowCallSubject(Call call) { - if (call == null) { - return false; - } - - boolean isIncomingOrWaiting = mPrimary.getState() == Call.State.INCOMING || - mPrimary.getState() == Call.State.CALL_WAITING; - return isIncomingOrWaiting && !TextUtils.isEmpty(call.getCallSubject()) && - call.getNumberPresentation() == TelecomManager.PRESENTATION_ALLOWED && - call.isCallSubjectSupported(); - } - - /** - * Determines whether the "note sent" toast should be shown. It should be shown for a new - * outgoing call with a subject. - * - * @param call The call - * @return {@code true} if the toast should be shown, {@code false} otherwise. - */ - private boolean shouldShowNoteSentToast(Call call) { - return call != null && hasCallSubject(call) && (call.getState() == Call.State.DIALING - || call.getState() == Call.State.CONNECTING); - } - - private static boolean hasCallSubject(Call call) { - return !TextUtils.isEmpty(call.getTelecomCall().getDetails().getIntentExtras() - .getString(TelecomManager.EXTRA_CALL_SUBJECT)); - } - - public interface CallCardUi extends Ui { - void setVisible(boolean on); - void setContactContextTitle(View listHeaderView); - void setContactContextContent(ListAdapter listAdapter); - void showContactContext(boolean show); - void setCallCardVisible(boolean visible); - void setPrimary(String number, String name, boolean nameIsNumber, String label, - Drawable photo, boolean isSipCall, boolean isContactPhotoShown, boolean isWorkCall); - void setSecondary(boolean show, String name, boolean nameIsNumber, String label, - String providerLabel, boolean isConference, boolean isVideoCall, - boolean isFullscreen); - void setSecondaryInfoVisible(boolean visible); - void setCallState(int state, int videoState, int sessionModificationState, - DisconnectCause disconnectCause, String connectionLabel, - Drawable connectionIcon, String gatewayNumber, boolean isWifi, - boolean isConference, boolean isWorkCall); - void setPrimaryCallElapsedTime(boolean show, long duration); - void setPrimaryName(String name, boolean nameIsNumber); - void setPrimaryImage(Drawable image, boolean isVisible); - void setPrimaryPhoneNumber(String phoneNumber); - void setPrimaryLabel(String label); - void setEndCallButtonEnabled(boolean enabled, boolean animate); - void setCallbackNumber(String number, boolean isEmergencyCalls); - void setCallSubject(String callSubject); - void setProgressSpinnerVisible(boolean visible); - void showHdAudioIndicator(boolean visible); - void showForwardIndicator(boolean visible); - void showSpamIndicator(boolean visible); - void showManageConferenceCallButton(boolean visible); - boolean isManageConferenceVisible(); - boolean isCallSubjectVisible(); - void animateForNewOutgoingCall(); - void sendAccessibilityAnnouncement(); - void showNoteSentToast(); - } -} diff --git a/InCallUI/src/com/android/incallui/CallList.java b/InCallUI/src/com/android/incallui/CallList.java deleted file mode 100644 index 48870f68a..000000000 --- a/InCallUI/src/com/android/incallui/CallList.java +++ /dev/null @@ -1,695 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.os.Handler; -import android.os.Message; -import android.os.Trace; -import android.telecom.DisconnectCause; -import android.telecom.PhoneAccount; - -import com.android.contacts.common.testing.NeededForTesting; -import com.android.dialer.logging.Logger; -import com.android.dialer.service.ExtendedCallInfoService; -import com.android.incallui.util.TelecomCallUtil; - -import com.google.common.base.Preconditions; -import com.google.common.collect.Maps; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * Maintains the list of active calls and notifies interested classes of changes to the call list - * as they are received from the telephony stack. Primary listener of changes to this class is - * InCallPresenter. - */ -public class CallList { - - private static final int DISCONNECTED_CALL_SHORT_TIMEOUT_MS = 200; - private static final int DISCONNECTED_CALL_MEDIUM_TIMEOUT_MS = 2000; - private static final int DISCONNECTED_CALL_LONG_TIMEOUT_MS = 5000; - - private static final int EVENT_DISCONNECTED_TIMEOUT = 1; - private static final long BLOCK_QUERY_TIMEOUT_MS = 1000; - - private static CallList sInstance = new CallList(); - - private final HashMap mCallById = new HashMap<>(); - private final HashMap mCallByTelecomCall = new HashMap<>(); - private final HashMap> mCallTextReponsesMap = Maps.newHashMap(); - /** - * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is - * load factor before resizing, 1 means we only expect a single thread to - * access the map so make only a single shard - */ - private final Set mListeners = Collections.newSetFromMap( - new ConcurrentHashMap(8, 0.9f, 1)); - private final HashMap> mCallUpdateListenerMap = Maps - .newHashMap(); - private final Set mPendingDisconnectCalls = Collections.newSetFromMap( - new ConcurrentHashMap(8, 0.9f, 1)); - private ExtendedCallInfoService mExtendedCallInfoService; - - /** - * Static singleton accessor method. - */ - public static CallList getInstance() { - return sInstance; - } - - /** - * USED ONLY FOR TESTING - * Testing-only constructor. Instance should only be acquired through getInstance(). - */ - @NeededForTesting - CallList() { - } - - public void onCallAdded(final android.telecom.Call telecomCall, LatencyReport latencyReport) { - Trace.beginSection("onCallAdded"); - final Call call = new Call(telecomCall, latencyReport); - Log.d(this, "onCallAdded: callState=" + call.getState()); - - if (call.getState() == Call.State.INCOMING || - call.getState() == Call.State.CALL_WAITING) { - onIncoming(call, call.getCannedSmsResponses()); - if (mExtendedCallInfoService != null) { - String number = TelecomCallUtil.getNumber(telecomCall); - mExtendedCallInfoService.getExtendedCallInfo(number, null, - new ExtendedCallInfoService.Listener() { - @Override - public void onComplete(boolean isSpam) { - call.setSpam(isSpam); - onUpdate(call); - } - }); - } - } else { - onUpdate(call); - } - - call.logCallInitiationType(); - Trace.endSection(); - } - - public void onCallRemoved(android.telecom.Call telecomCall) { - if (mCallByTelecomCall.containsKey(telecomCall)) { - Call call = mCallByTelecomCall.get(telecomCall); - Logger.logCall(call); - if (updateCallInMap(call)) { - Log.w(this, "Removing call not previously disconnected " + call.getId()); - } - updateCallTextMap(call, null); - } - } - - /** - * Called when a single call disconnects. - */ - public void onDisconnect(Call call) { - if (updateCallInMap(call)) { - Log.i(this, "onDisconnect: " + call); - // notify those listening for changes on this specific change - notifyCallUpdateListeners(call); - // notify those listening for all disconnects - notifyListenersOfDisconnect(call); - } - } - - /** - * Called when a single call has changed. - */ - public void onIncoming(Call call, List textMessages) { - if (updateCallInMap(call)) { - Log.i(this, "onIncoming - " + call); - } - updateCallTextMap(call, textMessages); - - for (Listener listener : mListeners) { - listener.onIncomingCall(call); - } - } - - public void onUpgradeToVideo(Call call){ - Log.d(this, "onUpgradeToVideo call=" + call); - for (Listener listener : mListeners) { - listener.onUpgradeToVideo(call); - } - } - /** - * Called when a single call has changed. - */ - public void onUpdate(Call call) { - Trace.beginSection("onUpdate"); - onUpdateCall(call); - notifyGenericListeners(); - Trace.endSection(); - } - - /** - * Called when a single call has changed session modification state. - * - * @param call The call. - * @param sessionModificationState The new session modification state. - */ - public void onSessionModificationStateChange(Call call, int sessionModificationState) { - final List listeners = mCallUpdateListenerMap.get(call.getId()); - if (listeners != null) { - for (CallUpdateListener listener : listeners) { - listener.onSessionModificationStateChange(sessionModificationState); - } - } - } - - /** - * Called when the last forwarded number changes for a call. With IMS, the last forwarded - * number changes due to a supplemental service notification, so it is not pressent at the - * start of the call. - * - * @param call The call. - */ - public void onLastForwardedNumberChange(Call call) { - final List listeners = mCallUpdateListenerMap.get(call.getId()); - if (listeners != null) { - for (CallUpdateListener listener : listeners) { - listener.onLastForwardedNumberChange(); - } - } - } - - /** - * Called when the child number changes for a call. The child number can be received after a - * call is initially set up, so we need to be able to inform listeners of the change. - * - * @param call The call. - */ - public void onChildNumberChange(Call call) { - final List listeners = mCallUpdateListenerMap.get(call.getId()); - if (listeners != null) { - for (CallUpdateListener listener : listeners) { - listener.onChildNumberChange(); - } - } - } - - public void notifyCallUpdateListeners(Call call) { - final List listeners = mCallUpdateListenerMap.get(call.getId()); - if (listeners != null) { - for (CallUpdateListener listener : listeners) { - listener.onCallChanged(call); - } - } - } - - /** - * Add a call update listener for a call id. - * - * @param callId The call id to get updates for. - * @param listener The listener to add. - */ - public void addCallUpdateListener(String callId, CallUpdateListener listener) { - List listeners = mCallUpdateListenerMap.get(callId); - if (listeners == null) { - listeners = new CopyOnWriteArrayList(); - mCallUpdateListenerMap.put(callId, listeners); - } - listeners.add(listener); - } - - /** - * Remove a call update listener for a call id. - * - * @param callId The call id to remove the listener for. - * @param listener The listener to remove. - */ - public void removeCallUpdateListener(String callId, CallUpdateListener listener) { - List listeners = mCallUpdateListenerMap.get(callId); - if (listeners != null) { - listeners.remove(listener); - } - } - - public void addListener(Listener listener) { - Preconditions.checkNotNull(listener); - - mListeners.add(listener); - - // Let the listener know about the active calls immediately. - listener.onCallListChange(this); - } - - public void removeListener(Listener listener) { - if (listener != null) { - mListeners.remove(listener); - } - } - - /** - * TODO: Change so that this function is not needed. Instead of assuming there is an active - * call, the code should rely on the status of a specific Call and allow the presenters to - * update the Call object when the active call changes. - */ - public Call getIncomingOrActive() { - Call retval = getIncomingCall(); - if (retval == null) { - retval = getActiveCall(); - } - return retval; - } - - public Call getOutgoingOrActive() { - Call retval = getOutgoingCall(); - if (retval == null) { - retval = getActiveCall(); - } - return retval; - } - - /** - * A call that is waiting for {@link PhoneAccount} selection - */ - public Call getWaitingForAccountCall() { - return getFirstCallWithState(Call.State.SELECT_PHONE_ACCOUNT); - } - - public Call getPendingOutgoingCall() { - return getFirstCallWithState(Call.State.CONNECTING); - } - - public Call getOutgoingCall() { - Call call = getFirstCallWithState(Call.State.DIALING); - if (call == null) { - call = getFirstCallWithState(Call.State.REDIALING); - } - return call; - } - - public Call getActiveCall() { - return getFirstCallWithState(Call.State.ACTIVE); - } - - public Call getSecondActiveCall() { - return getCallWithState(Call.State.ACTIVE, 1); - } - - public Call getBackgroundCall() { - return getFirstCallWithState(Call.State.ONHOLD); - } - - public Call getDisconnectedCall() { - return getFirstCallWithState(Call.State.DISCONNECTED); - } - - public Call getDisconnectingCall() { - return getFirstCallWithState(Call.State.DISCONNECTING); - } - - public Call getSecondBackgroundCall() { - return getCallWithState(Call.State.ONHOLD, 1); - } - - public Call getActiveOrBackgroundCall() { - Call call = getActiveCall(); - if (call == null) { - call = getBackgroundCall(); - } - return call; - } - - public Call getIncomingCall() { - Call call = getFirstCallWithState(Call.State.INCOMING); - if (call == null) { - call = getFirstCallWithState(Call.State.CALL_WAITING); - } - - return call; - } - - public Call getFirstCall() { - Call result = getIncomingCall(); - if (result == null) { - result = getPendingOutgoingCall(); - } - if (result == null) { - result = getOutgoingCall(); - } - if (result == null) { - result = getFirstCallWithState(Call.State.ACTIVE); - } - if (result == null) { - result = getDisconnectingCall(); - } - if (result == null) { - result = getDisconnectedCall(); - } - return result; - } - - public boolean hasLiveCall() { - Call call = getFirstCall(); - if (call == null) { - return false; - } - return call != getDisconnectingCall() && call != getDisconnectedCall(); - } - - /** - * Returns the first call found in the call map with the specified call modification state. - * @param state The session modification state to search for. - * @return The first call with the specified state. - */ - public Call getVideoUpgradeRequestCall() { - for(Call call : mCallById.values()) { - if (call.getSessionModificationState() == - Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) { - return call; - } - } - return null; - } - - public Call getCallById(String callId) { - return mCallById.get(callId); - } - - public Call getCallByTelecomCall(android.telecom.Call telecomCall) { - return mCallByTelecomCall.get(telecomCall); - } - - public List getTextResponses(String callId) { - return mCallTextReponsesMap.get(callId); - } - - /** - * Returns first call found in the call map with the specified state. - */ - public Call getFirstCallWithState(int state) { - return getCallWithState(state, 0); - } - - /** - * Returns the [position]th call found in the call map with the specified state. - * TODO: Improve this logic to sort by call time. - */ - public Call getCallWithState(int state, int positionToFind) { - Call retval = null; - int position = 0; - for (Call call : mCallById.values()) { - if (call.getState() == state) { - if (position >= positionToFind) { - retval = call; - break; - } else { - position++; - } - } - } - - return retval; - } - - /** - * This is called when the service disconnects, either expectedly or unexpectedly. - * For the expected case, it's because we have no calls left. For the unexpected case, - * it is likely a crash of phone and we need to clean up our calls manually. Without phone, - * there can be no active calls, so this is relatively safe thing to do. - */ - public void clearOnDisconnect() { - for (Call call : mCallById.values()) { - final int state = call.getState(); - if (state != Call.State.IDLE && - state != Call.State.INVALID && - state != Call.State.DISCONNECTED) { - - call.setState(Call.State.DISCONNECTED); - call.setDisconnectCause(new DisconnectCause(DisconnectCause.UNKNOWN)); - updateCallInMap(call); - } - } - notifyGenericListeners(); - } - - /** - * Called when the user has dismissed an error dialog. This indicates acknowledgement of - * the disconnect cause, and that any pending disconnects should immediately occur. - */ - public void onErrorDialogDismissed() { - final Iterator iterator = mPendingDisconnectCalls.iterator(); - while (iterator.hasNext()) { - Call call = iterator.next(); - iterator.remove(); - finishDisconnectedCall(call); - } - } - - /** - * Processes an update for a single call. - * - * @param call The call to update. - */ - private void onUpdateCall(Call call) { - Log.d(this, "\t" + call); - if (updateCallInMap(call)) { - Log.i(this, "onUpdate - " + call); - } - updateCallTextMap(call, call.getCannedSmsResponses()); - notifyCallUpdateListeners(call); - } - - /** - * Sends a generic notification to all listeners that something has changed. - * It is up to the listeners to call back to determine what changed. - */ - private void notifyGenericListeners() { - for (Listener listener : mListeners) { - listener.onCallListChange(this); - } - } - - private void notifyListenersOfDisconnect(Call call) { - for (Listener listener : mListeners) { - listener.onDisconnect(call); - } - } - - /** - * Updates the call entry in the local map. - * @return false if no call previously existed and no call was added, otherwise true. - */ - private boolean updateCallInMap(Call call) { - Preconditions.checkNotNull(call); - - boolean updated = false; - - if (call.getState() == Call.State.DISCONNECTED) { - // update existing (but do not add!!) disconnected calls - if (mCallById.containsKey(call.getId())) { - // For disconnected calls, we want to keep them alive for a few seconds so that the - // UI has a chance to display anything it needs when a call is disconnected. - - // Set up a timer to destroy the call after X seconds. - final Message msg = mHandler.obtainMessage(EVENT_DISCONNECTED_TIMEOUT, call); - mHandler.sendMessageDelayed(msg, getDelayForDisconnect(call)); - mPendingDisconnectCalls.add(call); - - mCallById.put(call.getId(), call); - mCallByTelecomCall.put(call.getTelecomCall(), call); - updated = true; - } - } else if (!isCallDead(call)) { - mCallById.put(call.getId(), call); - mCallByTelecomCall.put(call.getTelecomCall(), call); - updated = true; - } else if (mCallById.containsKey(call.getId())) { - mCallById.remove(call.getId()); - mCallByTelecomCall.remove(call.getTelecomCall()); - updated = true; - } - - return updated; - } - - private int getDelayForDisconnect(Call call) { - Preconditions.checkState(call.getState() == Call.State.DISCONNECTED); - - - final int cause = call.getDisconnectCause().getCode(); - final int delay; - switch (cause) { - case DisconnectCause.LOCAL: - delay = DISCONNECTED_CALL_SHORT_TIMEOUT_MS; - break; - case DisconnectCause.REMOTE: - case DisconnectCause.ERROR: - delay = DISCONNECTED_CALL_MEDIUM_TIMEOUT_MS; - break; - case DisconnectCause.REJECTED: - case DisconnectCause.MISSED: - case DisconnectCause.CANCELED: - // no delay for missed/rejected incoming calls and canceled outgoing calls. - delay = 0; - break; - default: - delay = DISCONNECTED_CALL_LONG_TIMEOUT_MS; - break; - } - - return delay; - } - - private void updateCallTextMap(Call call, List textResponses) { - Preconditions.checkNotNull(call); - - if (!isCallDead(call)) { - if (textResponses != null) { - mCallTextReponsesMap.put(call.getId(), textResponses); - } - } else if (mCallById.containsKey(call.getId())) { - mCallTextReponsesMap.remove(call.getId()); - } - } - - private boolean isCallDead(Call call) { - final int state = call.getState(); - return Call.State.IDLE == state || Call.State.INVALID == state; - } - - /** - * Sets up a call for deletion and notifies listeners of change. - */ - private void finishDisconnectedCall(Call call) { - if (mPendingDisconnectCalls.contains(call)) { - mPendingDisconnectCalls.remove(call); - } - call.setState(Call.State.IDLE); - updateCallInMap(call); - notifyGenericListeners(); - } - - /** - * Notifies all video calls of a change in device orientation. - * - * @param rotation The new rotation angle (in degrees). - */ - public void notifyCallsOfDeviceRotation(int rotation) { - for (Call call : mCallById.values()) { - // First, ensure that the call videoState has video enabled (there is no need to set - // device orientation on a voice call which has not yet been upgraded to video). - // Second, ensure a VideoCall is set on the call so that the change can be sent to the - // provider (a VideoCall can be present for a call that does not currently have video, - // but can be upgraded to video). - - // NOTE: is it necessary to use this order because getVideoCall references the class - // VideoProfile which is not available on APIs <23 (M). - if (VideoUtils.isVideoCall(call) && call.getVideoCall() != null) { - call.getVideoCall().setDeviceOrientation(rotation); - } - } - } - - /** - * Handles the timeout for destroying disconnected calls. - */ - private Handler mHandler = new Handler() { - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case EVENT_DISCONNECTED_TIMEOUT: - Log.d(this, "EVENT_DISCONNECTED_TIMEOUT ", msg.obj); - finishDisconnectedCall((Call) msg.obj); - break; - default: - Log.wtf(this, "Message not expected: " + msg.what); - break; - } - } - }; - - public void setExtendedCallInfoService(ExtendedCallInfoService service) { - mExtendedCallInfoService = service; - } - - public void onInCallUiShown(boolean forFullScreenIntent) { - for (Call call : mCallById.values()) { - call.getLatencyReport().onInCallUiShown(forFullScreenIntent); - } - } - - /** - * Listener interface for any class that wants to be notified of changes - * to the call list. - */ - public interface Listener { - /** - * Called when a new incoming call comes in. - * This is the only method that gets called for incoming calls. Listeners - * that want to perform an action on incoming call should respond in this method - * because {@link #onCallListChange} does not automatically get called for - * incoming calls. - */ - public void onIncomingCall(Call call); - /** - * Called when a new modify call request comes in - * This is the only method that gets called for modify requests. - */ - public void onUpgradeToVideo(Call call); - /** - * Called anytime there are changes to the call list. The change can be switching call - * states, updating information, etc. This method will NOT be called for new incoming - * calls and for calls that switch to disconnected state. Listeners must add actions - * to those method implementations if they want to deal with those actions. - */ - public void onCallListChange(CallList callList); - - /** - * Called when a call switches to the disconnected state. This is the only method - * that will get called upon disconnection. - */ - public void onDisconnect(Call call); - - - } - - public interface CallUpdateListener { - // TODO: refactor and limit arg to be call state. Caller info is not needed. - public void onCallChanged(Call call); - - /** - * Notifies of a change to the session modification state for a call. - * - * @param sessionModificationState The new session modification state. - */ - public void onSessionModificationStateChange(int sessionModificationState); - - /** - * Notifies of a change to the last forwarded number for a call. - */ - public void onLastForwardedNumberChange(); - - /** - * Notifies of a change to the child number for a call. - */ - public void onChildNumberChange(); - } -} diff --git a/InCallUI/src/com/android/incallui/CallTimer.java b/InCallUI/src/com/android/incallui/CallTimer.java deleted file mode 100644 index d65e63373..000000000 --- a/InCallUI/src/com/android/incallui/CallTimer.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import com.google.common.base.Preconditions; - -import android.os.Handler; -import android.os.SystemClock; - -/** - * Helper class used to keep track of events requiring regular intervals. - */ -public class CallTimer extends Handler { - private Runnable mInternalCallback; - private Runnable mCallback; - private long mLastReportedTime; - private long mInterval; - private boolean mRunning; - - public CallTimer(Runnable callback) { - Preconditions.checkNotNull(callback); - - mInterval = 0; - mLastReportedTime = 0; - mRunning = false; - mCallback = callback; - mInternalCallback = new CallTimerCallback(); - } - - public boolean start(long interval) { - if (interval <= 0) { - return false; - } - - // cancel any previous timer - cancel(); - - mInterval = interval; - mLastReportedTime = SystemClock.uptimeMillis(); - - mRunning = true; - periodicUpdateTimer(); - - return true; - } - - public void cancel() { - removeCallbacks(mInternalCallback); - mRunning = false; - } - - private void periodicUpdateTimer() { - if (!mRunning) { - return; - } - - final long now = SystemClock.uptimeMillis(); - long nextReport = mLastReportedTime + mInterval; - while (now >= nextReport) { - nextReport += mInterval; - } - - postAtTime(mInternalCallback, nextReport); - mLastReportedTime = nextReport; - - // Run the callback - mCallback.run(); - } - - private class CallTimerCallback implements Runnable { - @Override - public void run() { - periodicUpdateTimer(); - } - } -} diff --git a/InCallUI/src/com/android/incallui/CallerInfo.java b/InCallUI/src/com/android/incallui/CallerInfo.java deleted file mode 100644 index f3d0e0763..000000000 --- a/InCallUI/src/com/android/incallui/CallerInfo.java +++ /dev/null @@ -1,585 +0,0 @@ -/* - * Copyright (C) 2006 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui; - -import com.android.dialer.util.PhoneLookupUtil; -import com.google.common.primitives.Longs; - -import android.content.Context; -import android.database.Cursor; -import android.graphics.Bitmap; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.provider.ContactsContract.CommonDataKinds.Phone; -import android.provider.ContactsContract; -import android.provider.ContactsContract.Contacts; -import android.provider.ContactsContract.Data; -import android.provider.ContactsContract.PhoneLookup; -import android.provider.ContactsContract.RawContacts; -import android.telephony.PhoneNumberUtils; -import android.text.TextUtils; - -import com.android.contacts.common.compat.CompatUtils; -import com.android.contacts.common.compat.PhoneLookupSdkCompat; -import com.android.contacts.common.ContactsUtils; -import com.android.contacts.common.ContactsUtils.UserType; -import com.android.contacts.common.util.PhoneNumberHelper; -import com.android.contacts.common.util.TelephonyManagerUtils; -import com.android.dialer.R; -import com.android.dialer.calllog.ContactInfoHelper; - -/** - * Looks up caller information for the given phone number. - */ -public class CallerInfo { - private static final String TAG = "CallerInfo"; - - // We should always use this projection starting from NYC onward. - private static final String[] DEFAULT_PHONELOOKUP_PROJECTION = new String[] { - PhoneLookupSdkCompat.CONTACT_ID, - PhoneLookup.DISPLAY_NAME, - PhoneLookup.LOOKUP_KEY, - PhoneLookup.NUMBER, - PhoneLookup.NORMALIZED_NUMBER, - PhoneLookup.LABEL, - PhoneLookup.TYPE, - PhoneLookup.PHOTO_URI, - PhoneLookup.CUSTOM_RINGTONE, - PhoneLookup.SEND_TO_VOICEMAIL - }; - - // In pre-N, contact id is stored in {@link PhoneLookup._ID} in non-sip query. - private static final String[] BACKWARD_COMPATIBLE_NON_SIP_DEFAULT_PHONELOOKUP_PROJECTION = - new String[] { - PhoneLookup._ID, - PhoneLookup.DISPLAY_NAME, - PhoneLookup.LOOKUP_KEY, - PhoneLookup.NUMBER, - PhoneLookup.NORMALIZED_NUMBER, - PhoneLookup.LABEL, - PhoneLookup.TYPE, - PhoneLookup.PHOTO_URI, - PhoneLookup.CUSTOM_RINGTONE, - PhoneLookup.SEND_TO_VOICEMAIL - }; - - public static String[] getDefaultPhoneLookupProjection(Uri phoneLookupUri) { - if (CompatUtils.isNCompatible()) { - return DEFAULT_PHONELOOKUP_PROJECTION; - } - // Pre-N - boolean isSip = phoneLookupUri.getBooleanQueryParameter( - ContactsContract.PhoneLookup.QUERY_PARAMETER_SIP_ADDRESS, false); - return (isSip) ? DEFAULT_PHONELOOKUP_PROJECTION - : BACKWARD_COMPATIBLE_NON_SIP_DEFAULT_PHONELOOKUP_PROJECTION; - } - - /** - * Please note that, any one of these member variables can be null, - * and any accesses to them should be prepared to handle such a case. - * - * Also, it is implied that phoneNumber is more often populated than - * name is, (think of calls being dialed/received using numbers where - * names are not known to the device), so phoneNumber should serve as - * a dependable fallback when name is unavailable. - * - * One other detail here is that this CallerInfo object reflects - * information found on a connection, it is an OUTPUT that serves - * mainly to display information to the user. In no way is this object - * used as input to make a connection, so we can choose to display - * whatever human-readable text makes sense to the user for a - * connection. This is especially relevant for the phone number field, - * since it is the one field that is most likely exposed to the user. - * - * As an example: - * 1. User dials "911" - * 2. Device recognizes that this is an emergency number - * 3. We use the "Emergency Number" string instead of "911" in the - * phoneNumber field. - * - * What we're really doing here is treating phoneNumber as an essential - * field here, NOT name. We're NOT always guaranteed to have a name - * for a connection, but the number should be displayable. - */ - public String name; - public String nameAlternative; - public String phoneNumber; - public String normalizedNumber; - public String forwardingNumber; - public String geoDescription; - - public String cnapName; - public int numberPresentation; - public int namePresentation; - public boolean contactExists; - - public String phoneLabel; - /* Split up the phoneLabel into number type and label name */ - public int numberType; - public String numberLabel; - - public int photoResource; - - // Contact ID, which will be 0 if a contact comes from the corp CP2. - public long contactIdOrZero; - public String lookupKeyOrNull; - public boolean needUpdate; - public Uri contactRefUri; - public @UserType long userType; - - /** - * Contact display photo URI. If a contact has no display photo but a thumbnail, it'll be - * the thumbnail URI instead. - */ - public Uri contactDisplayPhotoUri; - - // fields to hold individual contact preference data, - // including the send to voicemail flag and the ringtone - // uri reference. - public Uri contactRingtoneUri; - public boolean shouldSendToVoicemail; - - /** - * Drawable representing the caller image. This is essentially - * a cache for the image data tied into the connection / - * callerinfo object. - * - * This might be a high resolution picture which is more suitable - * for full-screen image view than for smaller icons used in some - * kinds of notifications. - * - * The {@link #isCachedPhotoCurrent} flag indicates if the image - * data needs to be reloaded. - */ - public Drawable cachedPhoto; - /** - * Bitmap representing the caller image which has possibly lower - * resolution than {@link #cachedPhoto} and thus more suitable for - * icons (like notification icons). - * - * In usual cases this is just down-scaled image of {@link #cachedPhoto}. - * If the down-scaling fails, this will just become null. - * - * The {@link #isCachedPhotoCurrent} flag indicates if the image - * data needs to be reloaded. - */ - public Bitmap cachedPhotoIcon; - /** - * Boolean which indicates if {@link #cachedPhoto} and - * {@link #cachedPhotoIcon} is fresh enough. If it is false, - * those images aren't pointing to valid objects. - */ - public boolean isCachedPhotoCurrent; - - /** - * String which holds the call subject sent as extra from the lower layers for this call. This - * is used to display the no-caller ID reason for restricted/unknown number presentation. - */ - public String callSubject; - - private boolean mIsEmergency; - private boolean mIsVoiceMail; - - public CallerInfo() { - // TODO: Move all the basic initialization here? - mIsEmergency = false; - mIsVoiceMail = false; - userType = ContactsUtils.USER_TYPE_CURRENT; - } - - /** - * getCallerInfo given a Cursor. - * @param context the context used to retrieve string constants - * @param contactRef the URI to attach to this CallerInfo object - * @param cursor the first object in the cursor is used to build the CallerInfo object. - * @return the CallerInfo which contains the caller id for the given - * number. The returned CallerInfo is null if no number is supplied. - */ - public static CallerInfo getCallerInfo(Context context, Uri contactRef, Cursor cursor) { - CallerInfo info = new CallerInfo(); - info.photoResource = 0; - info.phoneLabel = null; - info.numberType = 0; - info.numberLabel = null; - info.cachedPhoto = null; - info.isCachedPhotoCurrent = false; - info.contactExists = false; - info.userType = ContactsUtils.USER_TYPE_CURRENT; - - Log.v(TAG, "getCallerInfo() based on cursor..."); - - if (cursor != null) { - if (cursor.moveToFirst()) { - // TODO: photo_id is always available but not taken - // care of here. Maybe we should store it in the - // CallerInfo object as well. - - long contactId = 0L; - int columnIndex; - - // Look for the name - columnIndex = cursor.getColumnIndex(PhoneLookup.DISPLAY_NAME); - if (columnIndex != -1) { - info.name = cursor.getString(columnIndex); - } - - // Look for the number - columnIndex = cursor.getColumnIndex(PhoneLookup.NUMBER); - if (columnIndex != -1) { - info.phoneNumber = cursor.getString(columnIndex); - } - - // Look for the normalized number - columnIndex = cursor.getColumnIndex(PhoneLookup.NORMALIZED_NUMBER); - if (columnIndex != -1) { - info.normalizedNumber = cursor.getString(columnIndex); - } - - // Look for the label/type combo - columnIndex = cursor.getColumnIndex(PhoneLookup.LABEL); - if (columnIndex != -1) { - int typeColumnIndex = cursor.getColumnIndex(PhoneLookup.TYPE); - if (typeColumnIndex != -1) { - info.numberType = cursor.getInt(typeColumnIndex); - info.numberLabel = cursor.getString(columnIndex); - info.phoneLabel = Phone.getTypeLabel(context.getResources(), - info.numberType, info.numberLabel) - .toString(); - } - } - - // Look for the person_id. - columnIndex = getColumnIndexForPersonId(contactRef, cursor); - if (columnIndex != -1) { - contactId = cursor.getLong(columnIndex); - // QuickContacts in M doesn't support enterprise contact id - if (contactId != 0 && (ContactsUtils.FLAG_N_FEATURE - || !Contacts.isEnterpriseContactId(contactId))) { - info.contactIdOrZero = contactId; - Log.v(TAG, "==> got info.contactIdOrZero: " + info.contactIdOrZero); - - // cache the lookup key for later use with person_id to create lookup URIs - columnIndex = cursor.getColumnIndex(PhoneLookup.LOOKUP_KEY); - if (columnIndex != -1) { - info.lookupKeyOrNull = cursor.getString(columnIndex); - } - } - } else { - // No valid columnIndex, so we can't look up person_id. - Log.v(TAG, "Couldn't find contactId column for " + contactRef); - // Watch out: this means that anything that depends on - // person_id will be broken (like contact photo lookups in - // the in-call UI, for example.) - } - - // Display photo URI. - columnIndex = cursor.getColumnIndex(PhoneLookup.PHOTO_URI); - if ((columnIndex != -1) && (cursor.getString(columnIndex) != null)) { - info.contactDisplayPhotoUri = Uri.parse(cursor.getString(columnIndex)); - } else { - info.contactDisplayPhotoUri = null; - } - - // look for the custom ringtone, create from the string stored - // in the database. - columnIndex = cursor.getColumnIndex(PhoneLookup.CUSTOM_RINGTONE); - if ((columnIndex != -1) && (cursor.getString(columnIndex) != null)) { - if (TextUtils.isEmpty(cursor.getString(columnIndex))) { - // make it consistent with frameworks/base/.../CallerInfo.java - info.contactRingtoneUri = Uri.EMPTY; - } else { - info.contactRingtoneUri = Uri.parse(cursor.getString(columnIndex)); - } - } else { - info.contactRingtoneUri = null; - } - - // look for the send to voicemail flag, set it to true only - // under certain circumstances. - columnIndex = cursor.getColumnIndex(PhoneLookup.SEND_TO_VOICEMAIL); - info.shouldSendToVoicemail = (columnIndex != -1) && - ((cursor.getInt(columnIndex)) == 1); - info.contactExists = true; - - // Determine userType by directoryId and contactId - final String directory = contactRef == null ? null - : contactRef.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY); - final Long directoryId = directory == null ? null : Longs.tryParse(directory); - info.userType = ContactsUtils.determineUserType(directoryId, contactId); - - info.nameAlternative = ContactInfoHelper.lookUpDisplayNameAlternative( - context, info.lookupKeyOrNull, info.userType, directoryId); - } - cursor.close(); - } - - info.needUpdate = false; - info.name = normalize(info.name); - info.contactRefUri = contactRef; - - return info; - } - - /** - * getCallerInfo given a URI, look up in the call-log database - * for the uri unique key. - * @param context the context used to get the ContentResolver - * @param contactRef the URI used to lookup caller id - * @return the CallerInfo which contains the caller id for the given - * number. The returned CallerInfo is null if no number is supplied. - */ - private static CallerInfo getCallerInfo(Context context, Uri contactRef) { - - return getCallerInfo(context, contactRef, - context.getContentResolver().query(contactRef, null, null, null, null)); - } - - /** - * Performs another lookup if previous lookup fails and it's a SIP call - * and the peer's username is all numeric. Look up the username as it - * could be a PSTN number in the contact database. - * - * @param context the query context - * @param number the original phone number, could be a SIP URI - * @param previousResult the result of previous lookup - * @return previousResult if it's not the case - */ - static CallerInfo doSecondaryLookupIfNecessary(Context context, - String number, CallerInfo previousResult) { - if (!previousResult.contactExists - && PhoneNumberHelper.isUriNumber(number)) { - String username = PhoneNumberHelper.getUsernameFromUriNumber(number); - if (PhoneNumberUtils.isGlobalPhoneNumber(username)) { - previousResult = getCallerInfo(context, - Uri.withAppendedPath(PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI, - Uri.encode(username))); - } - } - return previousResult; - } - - // Accessors - - /** - * @return true if the caller info is an emergency number. - */ - public boolean isEmergencyNumber() { - return mIsEmergency; - } - - /** - * @return true if the caller info is a voicemail number. - */ - public boolean isVoiceMailNumber() { - return mIsVoiceMail; - } - - /** - * Mark this CallerInfo as an emergency call. - * @param context To lookup the localized 'Emergency Number' string. - * @return this instance. - */ - /* package */ CallerInfo markAsEmergency(Context context) { - name = context.getString(R.string.emergency_call_dialog_number_for_display); - phoneNumber = null; - - photoResource = R.drawable.img_phone; - mIsEmergency = true; - return this; - } - - - /** - * Mark this CallerInfo as a voicemail call. The voicemail label - * is obtained from the telephony manager. Caller must hold the - * READ_PHONE_STATE permission otherwise the phoneNumber will be - * set to null. - * @return this instance. - */ - /* package */ CallerInfo markAsVoiceMail(Context context) { - mIsVoiceMail = true; - - try { - // For voicemail calls, we display the voice mail tag - // instead of the real phone number in the "number" - // field. - name = TelephonyManagerUtils.getVoiceMailAlphaTag(context); - phoneNumber = null; - } catch (SecurityException se) { - // Should never happen: if this process does not have - // permission to retrieve VM tag, it should not have - // permission to retrieve VM number and would not call - // this method. - // Leave phoneNumber untouched. - Log.e(TAG, "Cannot access VoiceMail.", se); - } - // TODO: There is no voicemail picture? - // FIXME: FIND ANOTHER ICON - // photoResource = android.R.drawable.badge_voicemail; - return this; - } - - private static String normalize(String s) { - if (s == null || s.length() > 0) { - return s; - } else { - return null; - } - } - - /** - * Returns the column index to use to find the "person_id" field in - * the specified cursor, based on the contact URI that was originally - * queried. - * - * This is a helper function for the getCallerInfo() method that takes - * a Cursor. Looking up the person_id is nontrivial (compared to all - * the other CallerInfo fields) since the column we need to use - * depends on what query we originally ran. - * - * Watch out: be sure to not do any database access in this method, since - * it's run from the UI thread (see comments below for more info.) - * - * @return the columnIndex to use (with cursor.getLong()) to get the - * person_id, or -1 if we couldn't figure out what colum to use. - * - * TODO: Add a unittest for this method. (This is a little tricky to - * test, since we'll need a live contacts database to test against, - * preloaded with at least some phone numbers and SIP addresses. And - * we'll probably have to hardcode the column indexes we expect, so - * the test might break whenever the contacts schema changes. But we - * can at least make sure we handle all the URI patterns we claim to, - * and that the mime types match what we expect...) - */ - private static int getColumnIndexForPersonId(Uri contactRef, Cursor cursor) { - // TODO: This is pretty ugly now, see bug 2269240 for - // more details. The column to use depends upon the type of URL: - // - content://com.android.contacts/data/phones ==> use the "contact_id" column - // - content://com.android.contacts/phone_lookup ==> use the "_ID" column - // - content://com.android.contacts/data ==> use the "contact_id" column - // If it's none of the above, we leave columnIndex=-1 which means - // that the person_id field will be left unset. - // - // The logic here *used* to be based on the mime type of contactRef - // (for example Phone.CONTENT_ITEM_TYPE would tell us to use the - // RawContacts.CONTACT_ID column). But looking up the mime type requires - // a call to context.getContentResolver().getType(contactRef), which - // isn't safe to do from the UI thread since it can cause an ANR if - // the contacts provider is slow or blocked (like during a sync.) - // - // So instead, figure out the column to use for person_id by just - // looking at the URI itself. - - Log.v(TAG, "- getColumnIndexForPersonId: contactRef URI = '" - + contactRef + "'..."); - // Warning: Do not enable the following logging (due to ANR risk.) - // if (VDBG) Rlog.v(TAG, "- MIME type: " - // + context.getContentResolver().getType(contactRef)); - - String url = contactRef.toString(); - String columnName = null; - if (url.startsWith("content://com.android.contacts/data/phones")) { - // Direct lookup in the Phone table. - // MIME type: Phone.CONTENT_ITEM_TYPE (= "vnd.android.cursor.item/phone_v2") - Log.v(TAG, "'data/phones' URI; using RawContacts.CONTACT_ID"); - columnName = RawContacts.CONTACT_ID; - } else if (url.startsWith("content://com.android.contacts/data")) { - // Direct lookup in the Data table. - // MIME type: Data.CONTENT_TYPE (= "vnd.android.cursor.dir/data") - Log.v(TAG, "'data' URI; using Data.CONTACT_ID"); - // (Note Data.CONTACT_ID and RawContacts.CONTACT_ID are equivalent.) - columnName = Data.CONTACT_ID; - } else if (url.startsWith("content://com.android.contacts/phone_lookup")) { - // Lookup in the PhoneLookup table, which provides "fuzzy matching" - // for phone numbers. - // MIME type: PhoneLookup.CONTENT_TYPE (= "vnd.android.cursor.dir/phone_lookup") - Log.v(TAG, "'phone_lookup' URI; using PhoneLookup._ID"); - columnName = PhoneLookupUtil.getContactIdColumnNameForUri(contactRef); - } else { - Log.v(TAG, "Unexpected prefix for contactRef '" + url + "'"); - } - int columnIndex = (columnName != null) ? cursor.getColumnIndex(columnName) : -1; - Log.v(TAG, "==> Using column '" + columnName - + "' (columnIndex = " + columnIndex + ") for person_id lookup..."); - return columnIndex; - } - - /** - * Updates this CallerInfo's geoDescription field, based on the raw - * phone number in the phoneNumber field. - * - * (Note that the various getCallerInfo() methods do *not* set the - * geoDescription automatically; you need to call this method - * explicitly to get it.) - * - * @param context the context used to look up the current locale / country - * @param fallbackNumber if this CallerInfo's phoneNumber field is empty, - * this specifies a fallback number to use instead. - */ - public void updateGeoDescription(Context context, String fallbackNumber) { - String number = TextUtils.isEmpty(phoneNumber) ? fallbackNumber : phoneNumber; - geoDescription = com.android.dialer.util.PhoneNumberUtil.getGeoDescription(context, number); - } - - /** - * @return a string debug representation of this instance. - */ - @Override - public String toString() { - // Warning: never check in this file with VERBOSE_DEBUG = true - // because that will result in PII in the system log. - final boolean VERBOSE_DEBUG = false; - - if (VERBOSE_DEBUG) { - return new StringBuilder(384) - .append(super.toString() + " { ") - .append("\nname: " + name) - .append("\nphoneNumber: " + phoneNumber) - .append("\nnormalizedNumber: " + normalizedNumber) - .append("\forwardingNumber: " + forwardingNumber) - .append("\ngeoDescription: " + geoDescription) - .append("\ncnapName: " + cnapName) - .append("\nnumberPresentation: " + numberPresentation) - .append("\nnamePresentation: " + namePresentation) - .append("\ncontactExists: " + contactExists) - .append("\nphoneLabel: " + phoneLabel) - .append("\nnumberType: " + numberType) - .append("\nnumberLabel: " + numberLabel) - .append("\nphotoResource: " + photoResource) - .append("\ncontactIdOrZero: " + contactIdOrZero) - .append("\nneedUpdate: " + needUpdate) - .append("\ncontactRefUri: " + contactRefUri) - .append("\ncontactRingtoneUri: " + contactRingtoneUri) - .append("\ncontactDisplayPhotoUri: " + contactDisplayPhotoUri) - .append("\nshouldSendToVoicemail: " + shouldSendToVoicemail) - .append("\ncachedPhoto: " + cachedPhoto) - .append("\nisCachedPhotoCurrent: " + isCachedPhotoCurrent) - .append("\nemergency: " + mIsEmergency) - .append("\nvoicemail: " + mIsVoiceMail) - .append("\nuserType: " + userType) - .append(" }") - .toString(); - } else { - return new StringBuilder(128) - .append(super.toString() + " { ") - .append("name " + ((name == null) ? "null" : "non-null")) - .append(", phoneNumber " + ((phoneNumber == null) ? "null" : "non-null")) - .append(" }") - .toString(); - } - } -} diff --git a/InCallUI/src/com/android/incallui/CallerInfoAsyncQuery.java b/InCallUI/src/com/android/incallui/CallerInfoAsyncQuery.java deleted file mode 100644 index f7f0cbb5d..000000000 --- a/InCallUI/src/com/android/incallui/CallerInfoAsyncQuery.java +++ /dev/null @@ -1,599 +0,0 @@ -/* - * Copyright (C) 2006 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui; - -import com.google.common.primitives.Longs; - -import android.Manifest; -import android.content.AsyncQueryHandler; -import android.content.ContentResolver; -import android.content.Context; -import android.database.Cursor; -import android.database.SQLException; -import android.net.Uri; -import android.os.Handler; -import android.os.Looper; -import android.os.Message; -import android.provider.ContactsContract; -import android.provider.ContactsContract.Directory; -import android.telephony.PhoneNumberUtils; -import android.text.TextUtils; - -import com.android.contacts.common.ContactsUtils; -import com.android.contacts.common.compat.DirectoryCompat; -import com.android.contacts.common.util.PermissionsUtil; -import com.android.contacts.common.util.TelephonyManagerUtils; -import com.android.dialer.R; -import com.android.dialer.calllog.ContactInfoHelper; -import com.android.dialer.service.CachedNumberLookupService; -import com.android.dialer.service.CachedNumberLookupService.CachedContactInfo; -import com.android.dialerbind.ObjectFactory; - -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Locale; - -/** - * Helper class to make it easier to run asynchronous caller-id lookup queries. - * @see CallerInfo - * - */ -public class CallerInfoAsyncQuery { - private static final boolean DBG = false; - private static final String LOG_TAG = "CallerInfoAsyncQuery"; - - private static final int EVENT_NEW_QUERY = 1; - private static final int EVENT_ADD_LISTENER = 2; - private static final int EVENT_END_OF_QUEUE = 3; - private static final int EVENT_EMERGENCY_NUMBER = 4; - private static final int EVENT_VOICEMAIL_NUMBER = 5; - - private CallerInfoAsyncQueryHandler mHandler; - - // If the CallerInfo query finds no contacts, should we use the - // PhoneNumberOfflineGeocoder to look up a "geo description"? - // (TODO: This could become a flag in config.xml if it ever needs to be - // configured on a per-product basis.) - private static final boolean ENABLE_UNKNOWN_NUMBER_GEO_DESCRIPTION = true; - - /** - * Interface for a CallerInfoAsyncQueryHandler result return. - */ - public interface OnQueryCompleteListener { - /** - * Called when the query is complete. - */ - public void onQueryComplete(int token, Object cookie, CallerInfo ci); - } - - - /** - * Wrap the cookie from the WorkerArgs with additional information needed by our - * classes. - */ - private static final class CookieWrapper { - public OnQueryCompleteListener listener; - public Object cookie; - public int event; - public String number; - } - - /** - * Simple exception used to communicate problems with the query pool. - */ - public static class QueryPoolException extends SQLException { - public QueryPoolException(String error) { - super(error); - } - } - - /** - * Our own implementation of the AsyncQueryHandler. - */ - private class CallerInfoAsyncQueryHandler extends AsyncQueryHandler { - - @Override - public void startQuery(int token, Object cookie, Uri uri, String[] projection, - String selection, String[] selectionArgs, String orderBy) { - if (DBG) { - // Show stack trace with the arguments. - android.util.Log.d(LOG_TAG, "InCall: startQuery: url=" + uri + - " projection=[" + Arrays.toString(projection) + "]" + - " selection=" + selection + " " + - " args=[" + Arrays.toString(selectionArgs) + "]", - new RuntimeException("STACKTRACE")); - } - super.startQuery(token, cookie, uri, projection, selection, selectionArgs, orderBy); - } - - /** - * The information relevant to each CallerInfo query. Each query may have multiple - * listeners, so each AsyncCursorInfo is associated with 2 or more CookieWrapper - * objects in the queue (one with a new query event, and one with a end event, with - * 0 or more additional listeners in between). - */ - private Context mQueryContext; - private Uri mQueryUri; - private CallerInfo mCallerInfo; - - /** - * Our own query worker thread. - * - * This thread handles the messages enqueued in the looper. The normal sequence - * of events is that a new query shows up in the looper queue, followed by 0 or - * more add listener requests, and then an end request. Of course, these requests - * can be interlaced with requests from other tokens, but is irrelevant to this - * handler since the handler has no state. - * - * Note that we depend on the queue to keep things in order; in other words, the - * looper queue must be FIFO with respect to input from the synchronous startQuery - * calls and output to this handleMessage call. - * - * This use of the queue is required because CallerInfo objects may be accessed - * multiple times before the query is complete. All accesses (listeners) must be - * queued up and informed in order when the query is complete. - */ - protected class CallerInfoWorkerHandler extends WorkerHandler { - public CallerInfoWorkerHandler(Looper looper) { - super(looper); - } - - @Override - public void handleMessage(Message msg) { - WorkerArgs args = (WorkerArgs) msg.obj; - CookieWrapper cw = (CookieWrapper) args.cookie; - - if (cw == null) { - // Normally, this should never be the case for calls originating - // from within this code. - // However, if there is any code that this Handler calls (such as in - // super.handleMessage) that DOES place unexpected messages on the - // queue, then we need pass these messages on. - Log.d(this, "Unexpected command (CookieWrapper is null): " + msg.what + - " ignored by CallerInfoWorkerHandler, passing onto parent."); - - super.handleMessage(msg); - } else { - Log.d(this, "Processing event: " + cw.event + " token (arg1): " + msg.arg1 + - " command: " + msg.what + " query URI: " + - sanitizeUriToString(args.uri)); - - switch (cw.event) { - case EVENT_NEW_QUERY: - //start the sql command. - super.handleMessage(msg); - break; - - // shortcuts to avoid query for recognized numbers. - case EVENT_EMERGENCY_NUMBER: - case EVENT_VOICEMAIL_NUMBER: - - case EVENT_ADD_LISTENER: - case EVENT_END_OF_QUEUE: - // query was already completed, so just send the reply. - // passing the original token value back to the caller - // on top of the event values in arg1. - Message reply = args.handler.obtainMessage(msg.what); - reply.obj = args; - reply.arg1 = msg.arg1; - - reply.sendToTarget(); - - break; - default: - } - } - } - } - - - /** - * Asynchronous query handler class for the contact / callerinfo object. - */ - private CallerInfoAsyncQueryHandler(Context context) { - super(context.getContentResolver()); - } - - @Override - protected Handler createHandler(Looper looper) { - return new CallerInfoWorkerHandler(looper); - } - - /** - * Overrides onQueryComplete from AsyncQueryHandler. - * - * This method takes into account the state of this class; we construct the CallerInfo - * object only once for each set of listeners. When the query thread has done its work - * and calls this method, we inform the remaining listeners in the queue, until we're - * out of listeners. Once we get the message indicating that we should expect no new - * listeners for this CallerInfo object, we release the AsyncCursorInfo back into the - * pool. - */ - @Override - protected void onQueryComplete(int token, Object cookie, Cursor cursor) { - try { - Log.d(this, "##### onQueryComplete() ##### query complete for token: " + token); - - //get the cookie and notify the listener. - CookieWrapper cw = (CookieWrapper) cookie; - if (cw == null) { - // Normally, this should never be the case for calls originating - // from within this code. - // However, if there is any code that calls this method, we should - // check the parameters to make sure they're viable. - Log.d(this, "Cookie is null, ignoring onQueryComplete() request."); - return; - } - - if (cw.event == EVENT_END_OF_QUEUE) { - release(); - return; - } - - // check the token and if needed, create the callerinfo object. - if (mCallerInfo == null) { - if ((mQueryContext == null) || (mQueryUri == null)) { - throw new QueryPoolException - ("Bad context or query uri, or CallerInfoAsyncQuery already released."); - } - - // adjust the callerInfo data as needed, and only if it was set from the - // initial query request. - // Change the callerInfo number ONLY if it is an emergency number or the - // voicemail number, and adjust other data (including photoResource) - // accordingly. - if (cw.event == EVENT_EMERGENCY_NUMBER) { - // Note we're setting the phone number here (refer to javadoc - // comments at the top of CallerInfo class). - mCallerInfo = new CallerInfo().markAsEmergency(mQueryContext); - } else if (cw.event == EVENT_VOICEMAIL_NUMBER) { - mCallerInfo = new CallerInfo().markAsVoiceMail(mQueryContext); - } else { - mCallerInfo = CallerInfo.getCallerInfo(mQueryContext, mQueryUri, cursor); - Log.d(this, "==> Got mCallerInfo: " + mCallerInfo); - - CallerInfo newCallerInfo = CallerInfo.doSecondaryLookupIfNecessary( - mQueryContext, cw.number, mCallerInfo); - if (newCallerInfo != mCallerInfo) { - mCallerInfo = newCallerInfo; - Log.d(this, "#####async contact look up with numeric username" - + mCallerInfo); - } - - // Final step: look up the geocoded description. - if (ENABLE_UNKNOWN_NUMBER_GEO_DESCRIPTION) { - // Note we do this only if we *don't* have a valid name (i.e. if - // no contacts matched the phone number of the incoming call), - // since that's the only case where the incoming-call UI cares - // about this field. - // - // (TODO: But if we ever want the UI to show the geoDescription - // even when we *do* match a contact, we'll need to either call - // updateGeoDescription() unconditionally here, or possibly add a - // new parameter to CallerInfoAsyncQuery.startQuery() to force - // the geoDescription field to be populated.) - - if (TextUtils.isEmpty(mCallerInfo.name)) { - // Actually when no contacts match the incoming phone number, - // the CallerInfo object is totally blank here (i.e. no name - // *or* phoneNumber). So we need to pass in cw.number as - // a fallback number. - mCallerInfo.updateGeoDescription(mQueryContext, cw.number); - } - } - - // Use the number entered by the user for display. - if (!TextUtils.isEmpty(cw.number)) { - mCallerInfo.phoneNumber = PhoneNumberUtils.formatNumber(cw.number, - mCallerInfo.normalizedNumber, - TelephonyManagerUtils.getCurrentCountryIso(mQueryContext, - Locale.getDefault())); - } - } - - Log.d(this, "constructing CallerInfo object for token: " + token); - - //notify that we can clean up the queue after this. - CookieWrapper endMarker = new CookieWrapper(); - endMarker.event = EVENT_END_OF_QUEUE; - startQuery(token, endMarker, null, null, null, null, null); - } - - //notify the listener that the query is complete. - if (cw.listener != null) { - Log.d(this, "notifying listener: " + cw.listener.getClass().toString() + - " for token: " + token + mCallerInfo); - cw.listener.onQueryComplete(token, cw.cookie, mCallerInfo); - } - } finally { - // The cursor may have been closed in CallerInfo.getCallerInfo() - if (cursor != null && !cursor.isClosed()) { - cursor.close(); - } - } - } - } - - /** - * Private constructor for factory methods. - */ - private CallerInfoAsyncQuery() { - } - - public static void startQuery(final int token, final Context context, final CallerInfo info, - final OnQueryCompleteListener listener, final Object cookie) { - Log.d(LOG_TAG, "##### CallerInfoAsyncQuery startContactProviderQuery()... #####"); - Log.d(LOG_TAG, "- number: " + info.phoneNumber); - Log.d(LOG_TAG, "- cookie: " + cookie); - if (!PermissionsUtil.hasPermission(context, Manifest.permission.READ_CONTACTS)) { - Log.w(LOG_TAG, "Dialer doesn't have permission to read contacts."); - listener.onQueryComplete(token, cookie, info); - return; - } - - OnQueryCompleteListener contactsProviderQueryCompleteListener = - new OnQueryCompleteListener() { - @Override - public void onQueryComplete(int token, Object cookie, CallerInfo ci) { - Log.d(LOG_TAG, "contactsProviderQueryCompleteListener done"); - // If there are no other directory queries, make sure that the listener is - // notified of this result. see b/27621628 - if ((ci != null && ci.contactExists) || - !startOtherDirectoriesQuery(token, context, info, listener, cookie)) { - if (listener != null && ci != null) { - listener.onQueryComplete(token, cookie, ci); - } - } - } - }; - startDefaultDirectoryQuery(token, context, info, contactsProviderQueryCompleteListener, - cookie); - } - - // Private methods - private static CallerInfoAsyncQuery startDefaultDirectoryQuery(int token, Context context, - CallerInfo info, OnQueryCompleteListener listener, Object cookie) { - // Construct the URI object and query params, and start the query. - Uri uri = ContactInfoHelper.getContactInfoLookupUri(info.phoneNumber); - return startQueryInternal(token, context, info, listener, cookie, uri); - } - - /** - * Factory method to start the query based on a CallerInfo object. - * - * Note: if the number contains an "@" character we treat it - * as a SIP address, and look it up directly in the Data table - * rather than using the PhoneLookup table. - * TODO: But eventually we should expose two separate methods, one for - * numbers and one for SIP addresses, and then have - * PhoneUtils.startGetCallerInfo() decide which one to call based on - * the phone type of the incoming connection. - */ - private static CallerInfoAsyncQuery startQueryInternal(int token, Context context, - CallerInfo info, OnQueryCompleteListener listener, Object cookie, Uri contactRef) { - if (DBG) { - Log.d(LOG_TAG, "==> contactRef: " + sanitizeUriToString(contactRef)); - } - - CallerInfoAsyncQuery c = new CallerInfoAsyncQuery(); - c.allocate(context, contactRef); - - //create cookieWrapper, start query - CookieWrapper cw = new CookieWrapper(); - cw.listener = listener; - cw.cookie = cookie; - cw.number = info.phoneNumber; - - // check to see if these are recognized numbers, and use shortcuts if we can. - if (PhoneNumberUtils.isLocalEmergencyNumber(context, info.phoneNumber)) { - cw.event = EVENT_EMERGENCY_NUMBER; - } else if (info.isVoiceMailNumber()) { - cw.event = EVENT_VOICEMAIL_NUMBER; - } else { - cw.event = EVENT_NEW_QUERY; - } - - - String[] proejection = CallerInfo.getDefaultPhoneLookupProjection(contactRef); - c.mHandler.startQuery(token, - cw, // cookie - contactRef, // uri - proejection, // projection - null, // selection - null, // selectionArgs - null); // orderBy - return c; - } - - // Return value indicates if listener was notified. - private static boolean startOtherDirectoriesQuery(int token, Context context, CallerInfo info, - OnQueryCompleteListener listener, Object cookie) { - long[] directoryIds = getDirectoryIds(context); - int size = directoryIds.length; - if (size == 0) { - return false; - } - - DirectoryQueryCompleteListenerFactory listenerFactory = - new DirectoryQueryCompleteListenerFactory(context, size, listener); - - // The current implementation of multiple async query runs in single handler thread - // in AsyncQueryHandler. - // intermediateListener.onQueryComplete is also called from the same caller thread. - // TODO(b/26019872): use thread pool instead of single thread. - for (int i = 0; i < size; i++) { - long directoryId = directoryIds[i]; - Uri uri = ContactInfoHelper.getContactInfoLookupUri(info.phoneNumber, directoryId); - if (DBG) { - Log.d(LOG_TAG, "directoryId: " + directoryId + " uri: " + uri); - } - OnQueryCompleteListener intermediateListener = - listenerFactory.newListener(directoryId); - startQueryInternal(token, context, info, intermediateListener, cookie, uri); - } - return true; - } - - /* Directory lookup related code - START */ - private static final String[] DIRECTORY_PROJECTION = new String[] {Directory._ID}; - - private static long[] getDirectoryIds(Context context) { - ArrayList results = new ArrayList<>(); - - Uri uri = Directory.CONTENT_URI; - if (ContactsUtils.FLAG_N_FEATURE) { - uri = Uri.withAppendedPath(ContactsContract.AUTHORITY_URI, "directories_enterprise"); - } - - ContentResolver cr = context.getContentResolver(); - Cursor cursor = cr.query(uri, DIRECTORY_PROJECTION, null, null, null); - addDirectoryIdsFromCursor(cursor, results); - - return Longs.toArray(results); - } - - private static void addDirectoryIdsFromCursor(Cursor cursor, ArrayList results) { - if (cursor != null) { - int idIndex = cursor.getColumnIndex(Directory._ID); - while (cursor.moveToNext()) { - long id = cursor.getLong(idIndex); - if (DirectoryCompat.isRemoteDirectoryId(id)) { - results.add(id); - } - } - cursor.close(); - } - } - - private static final class DirectoryQueryCompleteListenerFactory { - // Make sure listener to be called once and only once - private int mCount; - private boolean mIsListenerCalled; - private final OnQueryCompleteListener mListener; - private final Context mContext; - private final CachedNumberLookupService mCachedNumberLookupService = - ObjectFactory.newCachedNumberLookupService(); - - private class DirectoryQueryCompleteListener implements OnQueryCompleteListener { - private final long mDirectoryId; - - DirectoryQueryCompleteListener(long directoryId) { - mDirectoryId = directoryId; - } - - @Override - public void onQueryComplete(int token, Object cookie, CallerInfo ci) { - onDirectoryQueryComplete(token, cookie, ci, mDirectoryId); - } - } - - DirectoryQueryCompleteListenerFactory(Context context, int size, - OnQueryCompleteListener listener) { - mCount = size; - mListener = listener; - mIsListenerCalled = false; - mContext = context; - } - - private void onDirectoryQueryComplete(int token, Object cookie, CallerInfo ci, - long directoryId) { - boolean shouldCallListener = false; - synchronized (this) { - mCount = mCount - 1; - if (!mIsListenerCalled && (ci.contactExists || mCount == 0)) { - mIsListenerCalled = true; - shouldCallListener = true; - } - } - - // Don't call callback in synchronized block because mListener.onQueryComplete may - // take long time to complete - if (shouldCallListener && mListener != null) { - addCallerInfoIntoCache(ci, directoryId); - mListener.onQueryComplete(token, cookie, ci); - } - } - - private void addCallerInfoIntoCache(CallerInfo ci, long directoryId) { - if (ci.contactExists && mCachedNumberLookupService != null) { - // 1. Cache caller info - CachedContactInfo cachedContactInfo = CallerInfoUtils - .buildCachedContactInfo(mCachedNumberLookupService, ci); - String directoryLabel = mContext.getString(R.string.directory_search_label); - cachedContactInfo.setDirectorySource(directoryLabel, directoryId); - mCachedNumberLookupService.addContact(mContext, cachedContactInfo); - - // 2. Cache photo - if (ci.contactDisplayPhotoUri != null && ci.normalizedNumber != null) { - try (InputStream in = mContext.getContentResolver() - .openInputStream(ci.contactDisplayPhotoUri)) { - if (in != null) { - mCachedNumberLookupService.addPhoto(mContext, ci.normalizedNumber, in); - } - } catch (IOException e) { - Log.e(LOG_TAG, "failed to fetch directory contact photo", e); - } - - } - } - } - - public OnQueryCompleteListener newListener(long directoryId) { - return new DirectoryQueryCompleteListener(directoryId); - } - } - /* Directory lookup related code - END */ - - /** - * Method to create a new CallerInfoAsyncQueryHandler object, ensuring correct - * state of context and uri. - */ - private void allocate(Context context, Uri contactRef) { - if ((context == null) || (contactRef == null)){ - throw new QueryPoolException("Bad context or query uri."); - } - mHandler = new CallerInfoAsyncQueryHandler(context); - mHandler.mQueryContext = context; - mHandler.mQueryUri = contactRef; - } - - /** - * Releases the relevant data. - */ - private void release() { - mHandler.mQueryContext = null; - mHandler.mQueryUri = null; - mHandler.mCallerInfo = null; - mHandler = null; - } - - private static String sanitizeUriToString(Uri uri) { - if (uri != null) { - String uriString = uri.toString(); - int indexOfLastSlash = uriString.lastIndexOf('/'); - if (indexOfLastSlash > 0) { - return uriString.substring(0, indexOfLastSlash) + "/xxxxxxx"; - } else { - return uriString; - } - } else { - return ""; - } - } -} diff --git a/InCallUI/src/com/android/incallui/CallerInfoUtils.java b/InCallUI/src/com/android/incallui/CallerInfoUtils.java deleted file mode 100644 index 289b652fc..000000000 --- a/InCallUI/src/com/android/incallui/CallerInfoUtils.java +++ /dev/null @@ -1,234 +0,0 @@ -package com.android.incallui; - -import android.content.Context; -import android.content.Loader; -import android.content.Loader.OnLoadCompleteListener; -import android.net.Uri; -import android.telecom.PhoneAccount; -import android.telecom.TelecomManager; -import android.text.TextUtils; -import android.util.Log; - -import com.android.contacts.common.model.Contact; -import com.android.contacts.common.model.ContactLoader; -import com.android.dialer.R; -import com.android.dialer.calllog.ContactInfo; -import com.android.dialer.service.CachedNumberLookupService; -import com.android.dialer.service.CachedNumberLookupService.CachedContactInfo; -import com.android.dialer.util.TelecomUtil; - -import java.util.Arrays; - -/** - * Utility methods for contact and caller info related functionality - */ -public class CallerInfoUtils { - - private static final String TAG = CallerInfoUtils.class.getSimpleName(); - - /** Define for not a special CNAP string */ - private static final int CNAP_SPECIAL_CASE_NO = -1; - - public CallerInfoUtils() { - } - - private static final int QUERY_TOKEN = -1; - - /** - * This is called to get caller info for a call. This will return a CallerInfo - * object immediately based off information in the call, but - * more information is returned to the OnQueryCompleteListener (which contains - * information about the phone number label, user's name, etc). - */ - public static CallerInfo getCallerInfoForCall(Context context, Call call, - CallerInfoAsyncQuery.OnQueryCompleteListener listener) { - CallerInfo info = buildCallerInfo(context, call); - - // TODO: Have phoneapp send a Uri when it knows the contact that triggered this call. - - if (info.numberPresentation == TelecomManager.PRESENTATION_ALLOWED) { - // Start the query with the number provided from the call. - Log.d(TAG, "==> Actually starting CallerInfoAsyncQuery.startQuery()..."); - CallerInfoAsyncQuery.startQuery(QUERY_TOKEN, context, info, listener, call); - } - return info; - } - - public static CallerInfo buildCallerInfo(Context context, Call call) { - CallerInfo info = new CallerInfo(); - - // Store CNAP information retrieved from the Connection (we want to do this - // here regardless of whether the number is empty or not). - info.cnapName = call.getCnapName(); - info.name = info.cnapName; - info.numberPresentation = call.getNumberPresentation(); - info.namePresentation = call.getCnapNamePresentation(); - info.callSubject = call.getCallSubject(); - - String number = call.getNumber(); - if (!TextUtils.isEmpty(number)) { - final String[] numbers = number.split("&"); - number = numbers[0]; - if (numbers.length > 1) { - info.forwardingNumber = numbers[1]; - } - - number = modifyForSpecialCnapCases(context, info, number, info.numberPresentation); - info.phoneNumber = number; - } - - // Because the InCallUI is immediately launched before the call is connected, occasionally - // a voicemail call will be passed to InCallUI as a "voicemail:" URI without a number. - // This call should still be handled as a voicemail call. - if ((call.getHandle() != null && - PhoneAccount.SCHEME_VOICEMAIL.equals(call.getHandle().getScheme())) || - isVoiceMailNumber(context, call)) { - info.markAsVoiceMail(context); - } - - ContactInfoCache.getInstance(context).maybeInsertCnapInformationIntoCache(context, call, - info); - - return info; - } - - /** - * Creates a new {@link CachedContactInfo} from a {@link CallerInfo} - * - * @param lookupService the {@link CachedNumberLookupService} used to build a - * new {@link CachedContactInfo} - * @param {@link CallerInfo} object - * @return a CachedContactInfo object created from this CallerInfo - * @throws NullPointerException if lookupService or ci are null - */ - public static CachedContactInfo buildCachedContactInfo(CachedNumberLookupService lookupService, - CallerInfo ci) { - ContactInfo info = new ContactInfo(); - info.name = ci.name; - info.type = ci.numberType; - info.label = ci.phoneLabel; - info.number = ci.phoneNumber; - info.normalizedNumber = ci.normalizedNumber; - info.photoUri = ci.contactDisplayPhotoUri; - info.userType = ci.userType; - - CachedContactInfo cacheInfo = lookupService.buildCachedContactInfo(info); - cacheInfo.setLookupKey(ci.lookupKeyOrNull); - return cacheInfo; - } - - public static boolean isVoiceMailNumber(Context context, Call call) { - return TelecomUtil.isVoicemailNumber(context, - call.getTelecomCall().getDetails().getAccountHandle(), - call.getNumber()); - } - - /** - * Handles certain "corner cases" for CNAP. When we receive weird phone numbers - * from the network to indicate different number presentations, convert them to - * expected number and presentation values within the CallerInfo object. - * @param number number we use to verify if we are in a corner case - * @param presentation presentation value used to verify if we are in a corner case - * @return the new String that should be used for the phone number - */ - /* package */static String modifyForSpecialCnapCases(Context context, CallerInfo ci, - String number, int presentation) { - // Obviously we return number if ci == null, but still return number if - // number == null, because in these cases the correct string will still be - // displayed/logged after this function returns based on the presentation value. - if (ci == null || number == null) return number; - - Log.d(TAG, "modifyForSpecialCnapCases: initially, number=" - + toLogSafePhoneNumber(number) - + ", presentation=" + presentation + " ci " + ci); - - // "ABSENT NUMBER" is a possible value we could get from the network as the - // phone number, so if this happens, change it to "Unknown" in the CallerInfo - // and fix the presentation to be the same. - final String[] absentNumberValues = - context.getResources().getStringArray(R.array.absent_num); - if (Arrays.asList(absentNumberValues).contains(number) - && presentation == TelecomManager.PRESENTATION_ALLOWED) { - number = context.getString(R.string.unknown); - ci.numberPresentation = TelecomManager.PRESENTATION_UNKNOWN; - } - - // Check for other special "corner cases" for CNAP and fix them similarly. Corner - // cases only apply if we received an allowed presentation from the network, so check - // if we think we have an allowed presentation, or if the CallerInfo presentation doesn't - // match the presentation passed in for verification (meaning we changed it previously - // because it's a corner case and we're being called from a different entry point). - if (ci.numberPresentation == TelecomManager.PRESENTATION_ALLOWED - || (ci.numberPresentation != presentation - && presentation == TelecomManager.PRESENTATION_ALLOWED)) { - // For all special strings, change number & numberPrentation. - if (isCnapSpecialCaseRestricted(number)) { - number = context.getString(R.string.private_num); - ci.numberPresentation = TelecomManager.PRESENTATION_RESTRICTED; - } else if (isCnapSpecialCaseUnknown(number)) { - number = context.getString(R.string.unknown); - ci.numberPresentation = TelecomManager.PRESENTATION_UNKNOWN; - } - Log.d(TAG, "SpecialCnap: number=" + toLogSafePhoneNumber(number) - + "; presentation now=" + ci.numberPresentation); - } - Log.d(TAG, "modifyForSpecialCnapCases: returning number string=" - + toLogSafePhoneNumber(number)); - return number; - } - - private static boolean isCnapSpecialCaseRestricted(String n) { - return n.equals("PRIVATE") || n.equals("P") || n.equals("RES"); - } - - private static boolean isCnapSpecialCaseUnknown(String n) { - return n.equals("UNAVAILABLE") || n.equals("UNKNOWN") || n.equals("UNA") || n.equals("U"); - } - - /* package */static String toLogSafePhoneNumber(String number) { - // For unknown number, log empty string. - if (number == null) { - return ""; - } - - // Todo: Figure out an equivalent for VDBG - if (false) { - // When VDBG is true we emit PII. - return number; - } - - // Do exactly same thing as Uri#toSafeString() does, which will enable us to compare - // sanitized phone numbers. - StringBuilder builder = new StringBuilder(); - for (int i = 0; i < number.length(); i++) { - char c = number.charAt(i); - if (c == '-' || c == '@' || c == '.' || c == '&') { - builder.append(c); - } else { - builder.append('x'); - } - } - return builder.toString(); - } - - /** - * Send a notification using a {@link ContactLoader} to inform the sync adapter that we are - * viewing a particular contact, so that it can download the high-res photo. - */ - public static void sendViewNotification(Context context, Uri contactUri) { - final ContactLoader loader = new ContactLoader(context, contactUri, - true /* postViewNotification */); - loader.registerListener(0, new OnLoadCompleteListener() { - @Override - public void onLoadComplete( - Loader loader, Contact contact) { - try { - loader.reset(); - } catch (RuntimeException e) { - Log.e(TAG, "Error resetting loader", e); - } - } - }); - loader.startLoading(); - } -} diff --git a/InCallUI/src/com/android/incallui/CircularRevealFragment.java b/InCallUI/src/com/android/incallui/CircularRevealFragment.java deleted file mode 100644 index 01bd253ec..000000000 --- a/InCallUI/src/com/android/incallui/CircularRevealFragment.java +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.app.Activity; -import android.app.Fragment; -import android.app.FragmentManager; -import android.graphics.Outline; -import android.graphics.Point; -import android.os.Bundle; -import android.view.Display; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewAnimationUtils; -import android.view.ViewGroup; -import android.view.ViewOutlineProvider; -import android.view.ViewTreeObserver; -import android.view.ViewTreeObserver.OnPreDrawListener; - -import com.android.contacts.common.util.MaterialColorMapUtils.MaterialPalette; -import com.android.dialer.R; - -public class CircularRevealFragment extends Fragment { - static final String TAG = "CircularRevealFragment"; - - private Point mTouchPoint; - private OnCircularRevealCompleteListener mListener; - private boolean mAnimationStarted; - - interface OnCircularRevealCompleteListener { - public void onCircularRevealComplete(FragmentManager fm); - } - - public static void startCircularReveal(FragmentManager fm, Point touchPoint, - OnCircularRevealCompleteListener listener) { - if (fm.findFragmentByTag(TAG) == null) { - fm.beginTransaction().add(R.id.main, - new CircularRevealFragment(touchPoint, listener), TAG) - .commitAllowingStateLoss(); - } else { - Log.w(TAG, "An instance of CircularRevealFragment already exists"); - } - } - - public static void endCircularReveal(FragmentManager fm) { - final Fragment fragment = fm.findFragmentByTag(TAG); - if (fragment != null) { - fm.beginTransaction().remove(fragment).commitAllowingStateLoss(); - } - } - - /** - * Empty constructor used only by the {@link FragmentManager}. - */ - public CircularRevealFragment() {} - - public CircularRevealFragment(Point touchPoint, OnCircularRevealCompleteListener listener) { - mTouchPoint = touchPoint; - mListener = listener; - } - - @Override - public void onResume() { - super.onResume(); - if (!mAnimationStarted) { - // Only run the animation once for each instance of the fragment - startOutgoingAnimation(InCallPresenter.getInstance().getThemeColors()); - } - mAnimationStarted = true; - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - return inflater.inflate(R.layout.outgoing_call_animation, container, false); - } - - public void startOutgoingAnimation(MaterialPalette palette) { - final Activity activity = getActivity(); - if (activity == null) { - Log.w(this, "Asked to do outgoing call animation when not attached"); - return; - } - - final View view = activity.getWindow().getDecorView(); - - // The circle starts from an initial size of 0 so clip it such that it is invisible. - // Otherwise the first frame is drawn with a fully opaque screen which causes jank. When - // the animation later starts, this clip will be clobbered by the circular reveal clip. - // See ViewAnimationUtils.createCircularReveal. - view.setOutlineProvider(new ViewOutlineProvider() { - @Override - public void getOutline(View view, Outline outline) { - // Using (0, 0, 0, 0) will not work since the outline will simply be treated as - // an empty outline. - outline.setOval(-1, -1, 0, 0); - } - }); - view.setClipToOutline(true); - - if (palette != null) { - view.findViewById(R.id.outgoing_call_animation_circle).setBackgroundColor( - palette.mPrimaryColor); - activity.getWindow().setStatusBarColor(palette.mSecondaryColor); - } - - view.getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() { - @Override - public boolean onPreDraw() { - final ViewTreeObserver vto = view.getViewTreeObserver(); - if (vto.isAlive()) { - vto.removeOnPreDrawListener(this); - } - final Animator animator = getRevealAnimator(mTouchPoint); - if (animator != null) { - animator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - view.setClipToOutline(false); - if (mListener != null) { - mListener.onCircularRevealComplete(getFragmentManager()); - } - } - }); - animator.start(); - } - return false; - } - }); - } - - private Animator getRevealAnimator(Point touchPoint) { - final Activity activity = getActivity(); - if (activity == null) { - return null; - } - final View view = activity.getWindow().getDecorView(); - final Display display = activity.getWindowManager().getDefaultDisplay(); - final Point size = new Point(); - display.getSize(size); - - int startX = size.x / 2; - int startY = size.y / 2; - if (touchPoint != null) { - startX = touchPoint.x; - startY = touchPoint.y; - } - - final Animator valueAnimator = ViewAnimationUtils.createCircularReveal(view, - startX, startY, 0, Math.max(size.x, size.y)); - valueAnimator.setDuration(getResources().getInteger(R.integer.reveal_animation_duration)); - return valueAnimator; - } -} diff --git a/InCallUI/src/com/android/incallui/ConferenceManagerFragment.java b/InCallUI/src/com/android/incallui/ConferenceManagerFragment.java deleted file mode 100644 index fe941c8c5..000000000 --- a/InCallUI/src/com/android/incallui/ConferenceManagerFragment.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.app.ActionBar; -import android.content.Context; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ListView; - -import com.android.contacts.common.ContactPhotoManager; -import com.android.dialer.R; - -import java.util.List; - -/** - * Fragment that allows the user to manage a conference call. - */ -public class ConferenceManagerFragment - extends BaseFragment - implements ConferenceManagerPresenter.ConferenceManagerUi { - - private static final String KEY_IS_VISIBLE = "key_conference_is_visible"; - - private ListView mConferenceParticipantList; - private int mActionBarElevation; - private ContactPhotoManager mContactPhotoManager; - private LayoutInflater mInflater; - private ConferenceParticipantListAdapter mConferenceParticipantListAdapter; - private boolean mIsVisible; - private boolean mIsRecreating; - - @Override - public ConferenceManagerPresenter createPresenter() { - return new ConferenceManagerPresenter(); - } - - @Override - public ConferenceManagerPresenter.ConferenceManagerUi getUi() { - return this; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - if (savedInstanceState != null) { - mIsRecreating = true; - mIsVisible = savedInstanceState.getBoolean(KEY_IS_VISIBLE); - } - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - final View parent = - inflater.inflate(R.layout.conference_manager_fragment, container, false); - - mConferenceParticipantList = (ListView) parent.findViewById(R.id.participantList); - mContactPhotoManager = - ContactPhotoManager.getInstance(getActivity().getApplicationContext()); - mActionBarElevation = - (int) getResources().getDimension(R.dimen.incall_action_bar_elevation); - mInflater = LayoutInflater.from(getActivity().getApplicationContext()); - - return parent; - } - - @Override - public void onResume() { - super.onResume(); - if (mIsRecreating) { - onVisibilityChanged(mIsVisible); - } - } - - @Override - public void onSaveInstanceState(Bundle outState) { - outState.putBoolean(KEY_IS_VISIBLE, mIsVisible); - super.onSaveInstanceState(outState); - } - - public void onVisibilityChanged(boolean isVisible) { - mIsVisible = isVisible; - ActionBar actionBar = getActivity().getActionBar(); - if (isVisible) { - actionBar.setTitle(R.string.manageConferenceLabel); - actionBar.setElevation(mActionBarElevation); - actionBar.setHideOffset(0); - actionBar.show(); - - final CallList calls = CallList.getInstance(); - getPresenter().init(getActivity(), calls); - // Request focus on the list of participants for accessibility purposes. This ensures - // that once the list of participants is shown, the first participant is announced. - mConferenceParticipantList.requestFocus(); - } else { - actionBar.setElevation(0); - actionBar.setHideOffset(actionBar.getHeight()); - } - } - - @Override - public boolean isFragmentVisible() { - return isVisible(); - } - - @Override - public void update(Context context, List participants, boolean parentCanSeparate) { - if (mConferenceParticipantListAdapter == null) { - mConferenceParticipantListAdapter = new ConferenceParticipantListAdapter( - mConferenceParticipantList, context, mInflater, mContactPhotoManager); - - mConferenceParticipantList.setAdapter(mConferenceParticipantListAdapter); - } - mConferenceParticipantListAdapter.updateParticipants(participants, parentCanSeparate); - } - - @Override - public void refreshCall(Call call) { - mConferenceParticipantListAdapter.refreshCall(call); - } -} diff --git a/InCallUI/src/com/android/incallui/ConferenceManagerPresenter.java b/InCallUI/src/com/android/incallui/ConferenceManagerPresenter.java deleted file mode 100644 index 6fb6e5dda..000000000 --- a/InCallUI/src/com/android/incallui/ConferenceManagerPresenter.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.content.Context; - -import com.android.incallui.InCallPresenter.InCallDetailsListener; -import com.android.incallui.InCallPresenter.InCallState; -import com.android.incallui.InCallPresenter.InCallStateListener; -import com.android.incallui.InCallPresenter.IncomingCallListener; - -import com.google.common.base.Preconditions; - -import java.util.ArrayList; -import java.util.List; - -/** - * Logic for call buttons. - */ -public class ConferenceManagerPresenter - extends Presenter - implements InCallStateListener, InCallDetailsListener, IncomingCallListener { - - private Context mContext; - - @Override - public void onUiReady(ConferenceManagerUi ui) { - super.onUiReady(ui); - - // register for call state changes last - InCallPresenter.getInstance().addListener(this); - InCallPresenter.getInstance().addIncomingCallListener(this); - } - - @Override - public void onUiUnready(ConferenceManagerUi ui) { - super.onUiUnready(ui); - - InCallPresenter.getInstance().removeListener(this); - InCallPresenter.getInstance().removeIncomingCallListener(this); - } - - @Override - public void onStateChange(InCallState oldState, InCallState newState, CallList callList) { - if (getUi().isFragmentVisible()) { - Log.v(this, "onStateChange" + newState); - if (newState == InCallState.INCALL) { - final Call call = callList.getActiveOrBackgroundCall(); - if (call != null && call.isConferenceCall()) { - Log.v(this, "Number of existing calls is " + - String.valueOf(call.getChildCallIds().size())); - update(callList); - } else { - InCallPresenter.getInstance().showConferenceCallManager(false); - } - } else { - InCallPresenter.getInstance().showConferenceCallManager(false); - } - } - } - - @Override - public void onDetailsChanged(Call call, android.telecom.Call.Details details) { - boolean canDisconnect = details.can( - android.telecom.Call.Details.CAPABILITY_DISCONNECT_FROM_CONFERENCE); - boolean canSeparate = details.can( - android.telecom.Call.Details.CAPABILITY_SEPARATE_FROM_CONFERENCE); - - if (call.can(android.telecom.Call.Details.CAPABILITY_DISCONNECT_FROM_CONFERENCE) - != canDisconnect - || call.can(android.telecom.Call.Details.CAPABILITY_SEPARATE_FROM_CONFERENCE) - != canSeparate) { - getUi().refreshCall(call); - } - - if (!details.can( - android.telecom.Call.Details.CAPABILITY_MANAGE_CONFERENCE)) { - InCallPresenter.getInstance().showConferenceCallManager(false); - } - } - - @Override - public void onIncomingCall(InCallState oldState, InCallState newState, Call call) { - // When incoming call exists, set conference ui invisible. - if (getUi().isFragmentVisible()) { - Log.d(this, "onIncomingCall()... Conference ui is showing, hide it."); - InCallPresenter.getInstance().showConferenceCallManager(false); - } - } - - public void init(Context context, CallList callList) { - mContext = Preconditions.checkNotNull(context); - mContext = context; - update(callList); - } - - /** - * Updates the conference participant adapter. - * - * @param callList The callList. - */ - private void update(CallList callList) { - // callList is non null, but getActiveOrBackgroundCall() may return null - final Call currentCall = callList.getActiveOrBackgroundCall(); - if (currentCall == null) { - return; - } - - ArrayList calls = new ArrayList<>(currentCall.getChildCallIds().size()); - for (String callerId : currentCall.getChildCallIds()) { - calls.add(callList.getCallById(callerId)); - } - - Log.d(this, "Number of calls is " + String.valueOf(calls.size())); - - // Users can split out a call from the conference call if either the active call or the - // holding call is empty. If both are filled, users can not split out another call. - final boolean hasActiveCall = (callList.getActiveCall() != null); - final boolean hasHoldingCall = (callList.getBackgroundCall() != null); - boolean canSeparate = !(hasActiveCall && hasHoldingCall); - - getUi().update(mContext, calls, canSeparate); - } - - public interface ConferenceManagerUi extends Ui { - boolean isFragmentVisible(); - void update(Context context, List participants, boolean parentCanSeparate); - void refreshCall(Call call); - } -} diff --git a/InCallUI/src/com/android/incallui/ConferenceParticipantListAdapter.java b/InCallUI/src/com/android/incallui/ConferenceParticipantListAdapter.java deleted file mode 100644 index d68ae1f6f..000000000 --- a/InCallUI/src/com/android/incallui/ConferenceParticipantListAdapter.java +++ /dev/null @@ -1,533 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import com.google.common.base.MoreObjects; - -import android.content.Context; -import android.net.Uri; -import android.support.annotation.Nullable; -import android.text.BidiFormatter; -import android.text.TextDirectionHeuristics; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.BaseAdapter; -import android.widget.ImageView; -import android.widget.ListView; -import android.widget.TextView; - -import com.android.contacts.common.ContactPhotoManager; -import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest; -import com.android.contacts.common.compat.PhoneNumberUtilsCompat; -import com.android.contacts.common.preference.ContactsPreferences; -import com.android.contacts.common.util.ContactDisplayUtils; -import com.android.dialer.R; -import com.android.incallui.ContactInfoCache.ContactCacheEntry; - -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Objects; - -/** - * Adapter for a ListView containing conference call participant information. - */ -public class ConferenceParticipantListAdapter extends BaseAdapter { - - /** - * Internal class which represents a participant. Includes a reference to the {@link Call} and - * the corresponding {@link ContactCacheEntry} for the participant. - */ - private class ParticipantInfo { - private Call mCall; - private ContactCacheEntry mContactCacheEntry; - private boolean mCacheLookupComplete = false; - - public ParticipantInfo(Call call, ContactCacheEntry contactCacheEntry) { - mCall = call; - mContactCacheEntry = contactCacheEntry; - } - - public Call getCall() { - return mCall; - } - - public void setCall(Call call) { - mCall = call; - } - - public ContactCacheEntry getContactCacheEntry() { - return mContactCacheEntry; - } - - public void setContactCacheEntry(ContactCacheEntry entry) { - mContactCacheEntry = entry; - } - - public boolean isCacheLookupComplete() { - return mCacheLookupComplete; - } - - public void setCacheLookupComplete(boolean cacheLookupComplete) { - mCacheLookupComplete = cacheLookupComplete; - } - - @Override - public boolean equals(Object o) { - if (o instanceof ParticipantInfo) { - ParticipantInfo p = (ParticipantInfo) o; - return - Objects.equals(p.getCall().getId(), mCall.getId()); - } - return false; - } - - @Override - public int hashCode() { - return mCall.getId().hashCode(); - } - } - - /** - * Callback class used when making requests to the {@link ContactInfoCache} to resolve contact - * info and contact photos for conference participants. - */ - public static class ContactLookupCallback implements ContactInfoCache.ContactInfoCacheCallback { - private final WeakReference mListAdapter; - - public ContactLookupCallback(ConferenceParticipantListAdapter listAdapter) { - mListAdapter = new WeakReference(listAdapter); - } - - /** - * Called when contact info has been resolved. - * - * @param callId The call id. - * @param entry The new contact information. - */ - @Override - public void onContactInfoComplete(String callId, ContactCacheEntry entry) { - update(callId, entry); - } - - /** - * Called when contact photo has been loaded into the cache. - * - * @param callId The call id. - * @param entry The new contact information. - */ - @Override - public void onImageLoadComplete(String callId, ContactCacheEntry entry) { - update(callId, entry); - } - - @Override - public void onContactInteractionsInfoComplete(String callId, ContactCacheEntry entry) {} - - /** - * Updates the contact information for a participant. - * - * @param callId The call id. - * @param entry The new contact information. - */ - private void update(String callId, ContactCacheEntry entry) { - ConferenceParticipantListAdapter listAdapter = mListAdapter.get(); - if (listAdapter != null) { - listAdapter.updateContactInfo(callId, entry); - } - } - } - - /** - * Listener used to handle tap of the "disconnect' button for a participant. - */ - private View.OnClickListener mDisconnectListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - View parent = (View) v.getParent(); - String callId = (String) parent.getTag(); - TelecomAdapter.getInstance().disconnectCall(callId); - } - }; - - /** - * Listener used to handle tap of the "separate' button for a participant. - */ - private View.OnClickListener mSeparateListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - View parent = (View) v.getParent(); - String callId = (String) parent.getTag(); - TelecomAdapter.getInstance().separateCall(callId); - } - }; - - /** - * The ListView containing the participant information. - */ - private final ListView mListView; - - /** - * The conference participants to show in the ListView. - */ - private List mConferenceParticipants = new ArrayList<>(); - - /** - * Hashmap to make accessing participant info by call Id faster. - */ - private final HashMap mParticipantsByCallId = new HashMap<>(); - - /** - * The context. - */ - private final Context mContext; - - /** - * ContactsPreferences used to lookup displayName preferences - */ - @Nullable private final ContactsPreferences mContactsPreferences; - - /** - * The layout inflater used to inflate new views. - */ - private final LayoutInflater mLayoutInflater; - - /** - * Contact photo manager to retrieve cached contact photo information. - */ - private final ContactPhotoManager mContactPhotoManager; - - /** - * {@code True} if the conference parent supports separating calls from the conference. - */ - private boolean mParentCanSeparate; - - /** - * Creates an instance of the ConferenceParticipantListAdapter. - * - * @param listView The listview. - * @param context The context. - * @param layoutInflater The layout inflater. - * @param contactPhotoManager The contact photo manager, used to load contact photos. - */ - public ConferenceParticipantListAdapter(ListView listView, Context context, - LayoutInflater layoutInflater, ContactPhotoManager contactPhotoManager) { - - mListView = listView; - mContext = context; - mContactsPreferences = ContactsPreferencesFactory.newContactsPreferences(mContext); - mLayoutInflater = layoutInflater; - mContactPhotoManager = contactPhotoManager; - } - - /** - * Updates the adapter with the new conference participant information provided. - * - * @param conferenceParticipants The list of conference participants. - * @param parentCanSeparate {@code True} if the parent supports separating calls from the - * conference. - */ - public void updateParticipants(List conferenceParticipants, boolean parentCanSeparate) { - if (mContactsPreferences != null) { - mContactsPreferences.refreshValue(ContactsPreferences.DISPLAY_ORDER_KEY); - mContactsPreferences.refreshValue(ContactsPreferences.SORT_ORDER_KEY); - } - mParentCanSeparate = parentCanSeparate; - updateParticipantInfo(conferenceParticipants); - } - - /** - * Determines the number of participants in the conference. - * - * @return The number of participants. - */ - @Override - public int getCount() { - return mConferenceParticipants.size(); - } - - /** - * Retrieves an item from the list of participants. - * - * @param position Position of the item whose data we want within the adapter's - * data set. - * @return The {@link ParticipantInfo}. - */ - @Override - public Object getItem(int position) { - return mConferenceParticipants.get(position); - } - - /** - * Retreives the adapter-specific item id for an item at a specified position. - * - * @param position The position of the item within the adapter's data set whose row id we want. - * @return The item id. - */ - @Override - public long getItemId(int position) { - return position; - } - - /** - * Refreshes call information for the call passed in. - * - * @param call The new call information. - */ - public void refreshCall(Call call) { - String callId = call.getId(); - - if (mParticipantsByCallId.containsKey(callId)) { - ParticipantInfo participantInfo = mParticipantsByCallId.get(callId); - participantInfo.setCall(call); - refreshView(callId); - } - } - - /** - * Attempts to refresh the view for the specified call ID. This ensures the contact info and - * photo loaded from cache are updated. - * - * @param callId The call id. - */ - private void refreshView(String callId) { - int first = mListView.getFirstVisiblePosition(); - int last = mListView.getLastVisiblePosition(); - - for (int position = 0; position <= last - first; position++) { - View view = mListView.getChildAt(position); - String rowCallId = (String) view.getTag(); - if (rowCallId.equals(callId)) { - getView(position+first, view, mListView); - break; - } - } - } - - /** - * Creates or populates an existing conference participant row. - * - * @param position The position of the item within the adapter's data set of the item whose view - * we want. - * @param convertView The old view to reuse, if possible. - * @param parent The parent that this view will eventually be attached to - * @return The populated view. - */ - @Override - public View getView(int position, View convertView, ViewGroup parent) { - // Make sure we have a valid convertView to start with - final View result = convertView == null - ? mLayoutInflater.inflate(R.layout.caller_in_conference, parent, false) - : convertView; - - ParticipantInfo participantInfo = mConferenceParticipants.get(position); - Call call = participantInfo.getCall(); - ContactCacheEntry contactCache = participantInfo.getContactCacheEntry(); - - final ContactInfoCache cache = ContactInfoCache.getInstance(mContext); - - // If a cache lookup has not yet been performed to retrieve the contact information and - // photo, do it now. - if (!participantInfo.isCacheLookupComplete()) { - cache.findInfo(participantInfo.getCall(), - participantInfo.getCall().getState() == Call.State.INCOMING, - new ContactLookupCallback(this)); - } - - boolean thisRowCanSeparate = mParentCanSeparate && call.getTelecomCall().getDetails().can( - android.telecom.Call.Details.CAPABILITY_SEPARATE_FROM_CONFERENCE); - boolean thisRowCanDisconnect = call.getTelecomCall().getDetails().can( - android.telecom.Call.Details.CAPABILITY_DISCONNECT_FROM_CONFERENCE); - - setCallerInfoForRow(result, contactCache.namePrimary, - ContactDisplayUtils.getPreferredDisplayName(contactCache.namePrimary, - contactCache.nameAlternative, mContactsPreferences), - contactCache.number, contactCache.label, - contactCache.lookupKey, contactCache.displayPhotoUri, thisRowCanSeparate, - thisRowCanDisconnect); - - // Tag the row in the conference participant list with the call id to make it easier to - // find calls when contact cache information is loaded. - result.setTag(call.getId()); - - return result; - } - - /** - * Replaces the contact info for a participant and triggers a refresh of the UI. - * - * @param callId The call id. - * @param entry The new contact info. - */ - /* package */ void updateContactInfo(String callId, ContactCacheEntry entry) { - if (mParticipantsByCallId.containsKey(callId)) { - ParticipantInfo participantInfo = mParticipantsByCallId.get(callId); - participantInfo.setContactCacheEntry(entry); - participantInfo.setCacheLookupComplete(true); - refreshView(callId); - } - } - - /** - * Sets the caller information for a row in the conference participant list. - * - * @param view The view to set the details on. - * @param callerName The participant's name. - * @param callerNumber The participant's phone number. - * @param callerNumberType The participant's phone number typ.e - * @param lookupKey The lookup key for the participant (for photo lookup). - * @param photoUri The URI of the contact photo. - * @param thisRowCanSeparate {@code True} if this participant can separate from the conference. - * @param thisRowCanDisconnect {@code True} if this participant can be disconnected. - */ - private final void setCallerInfoForRow(View view, String callerName, String preferredName, - String callerNumber, String callerNumberType, String lookupKey, Uri photoUri, - boolean thisRowCanSeparate, boolean thisRowCanDisconnect) { - - final ImageView photoView = (ImageView) view.findViewById(R.id.callerPhoto); - final TextView nameTextView = (TextView) view.findViewById(R.id.conferenceCallerName); - final TextView numberTextView = (TextView) view.findViewById(R.id.conferenceCallerNumber); - final TextView numberTypeTextView = (TextView) view.findViewById( - R.id.conferenceCallerNumberType); - final View endButton = view.findViewById(R.id.conferenceCallerDisconnect); - final View separateButton = view.findViewById(R.id.conferenceCallerSeparate); - - endButton.setVisibility(thisRowCanDisconnect ? View.VISIBLE : View.GONE); - if (thisRowCanDisconnect) { - endButton.setOnClickListener(mDisconnectListener); - } else { - endButton.setOnClickListener(null); - } - - separateButton.setVisibility(thisRowCanSeparate ? View.VISIBLE : View.GONE); - if (thisRowCanSeparate) { - separateButton.setOnClickListener(mSeparateListener); - } else { - separateButton.setOnClickListener(null); - } - - DefaultImageRequest imageRequest = (photoUri != null) ? null : - new DefaultImageRequest(callerName, lookupKey, true /* isCircularPhoto */); - - mContactPhotoManager.loadDirectoryPhoto(photoView, photoUri, false, true, imageRequest); - - // set the caller name - nameTextView.setText(preferredName); - - // set the caller number in subscript, or make the field disappear. - if (TextUtils.isEmpty(callerNumber)) { - numberTextView.setVisibility(View.GONE); - numberTypeTextView.setVisibility(View.GONE); - } else { - numberTextView.setVisibility(View.VISIBLE); - numberTextView.setText(PhoneNumberUtilsCompat.createTtsSpannable( - BidiFormatter.getInstance().unicodeWrap( - callerNumber, TextDirectionHeuristics.LTR))); - numberTypeTextView.setVisibility(View.VISIBLE); - numberTypeTextView.setText(callerNumberType); - } - } - - /** - * Updates the participant info list which is bound to the ListView. Stores the call and - * contact info for all entries. The list is sorted alphabetically by participant name. - * - * @param conferenceParticipants The calls which make up the conference participants. - */ - private void updateParticipantInfo(List conferenceParticipants) { - final ContactInfoCache cache = ContactInfoCache.getInstance(mContext); - boolean newParticipantAdded = false; - HashSet newCallIds = new HashSet<>(conferenceParticipants.size()); - - // Update or add conference participant info. - for (Call call : conferenceParticipants) { - String callId = call.getId(); - newCallIds.add(callId); - ContactCacheEntry contactCache = cache.getInfo(callId); - if (contactCache == null) { - contactCache = ContactInfoCache.buildCacheEntryFromCall(mContext, call, - call.getState() == Call.State.INCOMING); - } - - if (mParticipantsByCallId.containsKey(callId)) { - ParticipantInfo participantInfo = mParticipantsByCallId.get(callId); - participantInfo.setCall(call); - participantInfo.setContactCacheEntry(contactCache); - } else { - newParticipantAdded = true; - ParticipantInfo participantInfo = new ParticipantInfo(call, contactCache); - mConferenceParticipants.add(participantInfo); - mParticipantsByCallId.put(call.getId(), participantInfo); - } - } - - // Remove any participants that no longer exist. - Iterator> it = - mParticipantsByCallId.entrySet().iterator(); - while (it.hasNext()) { - Map.Entry entry = it.next(); - String existingCallId = entry.getKey(); - if (!newCallIds.contains(existingCallId)) { - ParticipantInfo existingInfo = entry.getValue(); - mConferenceParticipants.remove(existingInfo); - it.remove(); - } - } - - if (newParticipantAdded) { - // Sort the list of participants by contact name. - sortParticipantList(); - } - notifyDataSetChanged(); - } - - /** - * Sorts the participant list by contact name. - */ - private void sortParticipantList() { - Collections.sort(mConferenceParticipants, new Comparator() { - public int compare(ParticipantInfo p1, ParticipantInfo p2) { - // Contact names might be null, so replace with empty string. - ContactCacheEntry c1 = p1.getContactCacheEntry(); - String p1Name = MoreObjects.firstNonNull( - ContactDisplayUtils.getPreferredSortName( - c1.namePrimary, - c1.nameAlternative, - mContactsPreferences), - ""); - - ContactCacheEntry c2 = p2.getContactCacheEntry(); - String p2Name = MoreObjects.firstNonNull( - ContactDisplayUtils.getPreferredSortName( - c2.namePrimary, - c2.nameAlternative, - mContactsPreferences), - ""); - - return p1Name.compareToIgnoreCase(p2Name); - } - }); - } -} diff --git a/InCallUI/src/com/android/incallui/ContactInfoCache.java b/InCallUI/src/com/android/incallui/ContactInfoCache.java deleted file mode 100644 index 9d6fc4627..000000000 --- a/InCallUI/src/com/android/incallui/ContactInfoCache.java +++ /dev/null @@ -1,699 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import com.google.common.base.MoreObjects; -import com.google.common.base.Preconditions; -import com.google.common.collect.Maps; -import com.google.common.collect.Sets; - -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.location.Address; -import android.media.RingtoneManager; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.Looper; -import android.provider.ContactsContract; -import android.provider.ContactsContract.CommonDataKinds.Phone; -import android.provider.ContactsContract.Contacts; -import android.provider.ContactsContract.DisplayNameSources; -import android.telecom.TelecomManager; -import android.text.TextUtils; -import android.util.Pair; - -import com.android.contacts.common.ContactsUtils; -import com.android.contacts.common.util.PhoneNumberHelper; -import com.android.dialer.R; -import com.android.dialer.calllog.ContactInfo; -import com.android.dialer.service.CachedNumberLookupService; -import com.android.dialer.service.CachedNumberLookupService.CachedContactInfo; -import com.android.dialer.util.MoreStrings; -import com.android.incallui.Call.LogState; -import com.android.incallui.service.PhoneNumberService; -import com.android.incalluibind.ObjectFactory; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.Calendar; -import java.util.HashMap; -import java.util.List; -import java.util.Set; - -/** - * Class responsible for querying Contact Information for Call objects. Can perform asynchronous - * requests to the Contact Provider for information as well as respond synchronously for any data - * that it currently has cached from previous queries. This class always gets called from the UI - * thread so it does not need thread protection. - */ -public class ContactInfoCache implements ContactsAsyncHelper.OnImageLoadCompleteListener { - - private static final String TAG = ContactInfoCache.class.getSimpleName(); - private static final int TOKEN_UPDATE_PHOTO_FOR_CALL_STATE = 0; - - private final Context mContext; - private final PhoneNumberService mPhoneNumberService; - private final CachedNumberLookupService mCachedNumberLookupService; - private final HashMap mInfoMap = Maps.newHashMap(); - private final HashMap> mCallBacks = Maps.newHashMap(); - - private static ContactInfoCache sCache = null; - - private Drawable mDefaultContactPhotoDrawable; - private Drawable mConferencePhotoDrawable; - private ContactUtils mContactUtils; - - public static synchronized ContactInfoCache getInstance(Context mContext) { - if (sCache == null) { - sCache = new ContactInfoCache(mContext.getApplicationContext()); - } - return sCache; - } - - private ContactInfoCache(Context context) { - mContext = context; - mPhoneNumberService = ObjectFactory.newPhoneNumberService(context); - mCachedNumberLookupService = - com.android.dialerbind.ObjectFactory.newCachedNumberLookupService(); - mContactUtils = ObjectFactory.getContactUtilsInstance(context); - - } - - public ContactCacheEntry getInfo(String callId) { - return mInfoMap.get(callId); - } - - public static ContactCacheEntry buildCacheEntryFromCall(Context context, Call call, - boolean isIncoming) { - final ContactCacheEntry entry = new ContactCacheEntry(); - - // TODO: get rid of caller info. - final CallerInfo info = CallerInfoUtils.buildCallerInfo(context, call); - ContactInfoCache.populateCacheEntry(context, info, entry, call.getNumberPresentation(), - isIncoming); - return entry; - } - - public void maybeInsertCnapInformationIntoCache(Context context, final Call call, - final CallerInfo info) { - if (mCachedNumberLookupService == null || TextUtils.isEmpty(info.cnapName) - || mInfoMap.get(call.getId()) != null) { - return; - } - final Context applicationContext = context.getApplicationContext(); - Log.i(TAG, "Found contact with CNAP name - inserting into cache"); - new AsyncTask() { - @Override - protected Void doInBackground(Void... params) { - ContactInfo contactInfo = new ContactInfo(); - CachedContactInfo cacheInfo = mCachedNumberLookupService.buildCachedContactInfo( - contactInfo); - cacheInfo.setSource(CachedContactInfo.SOURCE_TYPE_CNAP, "CNAP", 0); - contactInfo.name = info.cnapName; - contactInfo.number = call.getNumber(); - contactInfo.type = ContactsContract.CommonDataKinds.Phone.TYPE_MAIN; - try { - final JSONObject contactRows = new JSONObject().put(Phone.CONTENT_ITEM_TYPE, - new JSONObject() - .put(Phone.NUMBER, contactInfo.number) - .put(Phone.TYPE, Phone.TYPE_MAIN)); - final String jsonString = new JSONObject() - .put(Contacts.DISPLAY_NAME, contactInfo.name) - .put(Contacts.DISPLAY_NAME_SOURCE, DisplayNameSources.STRUCTURED_NAME) - .put(Contacts.CONTENT_ITEM_TYPE, contactRows).toString(); - cacheInfo.setLookupKey(jsonString); - } catch (JSONException e) { - Log.w(TAG, "Creation of lookup key failed when caching CNAP information"); - } - mCachedNumberLookupService.addContact(applicationContext, cacheInfo); - return null; - } - }.execute(); - } - - private class FindInfoCallback implements CallerInfoAsyncQuery.OnQueryCompleteListener { - private final boolean mIsIncoming; - - public FindInfoCallback(boolean isIncoming) { - mIsIncoming = isIncoming; - } - - @Override - public void onQueryComplete(int token, Object cookie, CallerInfo callerInfo) { - findInfoQueryComplete((Call) cookie, callerInfo, mIsIncoming, true); - } - } - - /** - * Requests contact data for the Call object passed in. - * Returns the data through callback. If callback is null, no response is made, however the - * query is still performed and cached. - * - * @param callback The function to call back when the call is found. Can be null. - */ - public void findInfo(final Call call, final boolean isIncoming, - ContactInfoCacheCallback callback) { - Preconditions.checkState(Looper.getMainLooper().getThread() == Thread.currentThread()); - Preconditions.checkNotNull(callback); - - final String callId = call.getId(); - final ContactCacheEntry cacheEntry = mInfoMap.get(callId); - Set callBacks = mCallBacks.get(callId); - - // If we have a previously obtained intermediate result return that now - if (cacheEntry != null) { - Log.d(TAG, "Contact lookup. In memory cache hit; lookup " - + (callBacks == null ? "complete" : "still running")); - callback.onContactInfoComplete(callId, cacheEntry); - // If no other callbacks are in flight, we're done. - if (callBacks == null) { - return; - } - } - - // If the entry already exists, add callback - if (callBacks != null) { - callBacks.add(callback); - return; - } - Log.d(TAG, "Contact lookup. In memory cache miss; searching provider."); - // New lookup - callBacks = Sets.newHashSet(); - callBacks.add(callback); - mCallBacks.put(callId, callBacks); - - /** - * Performs a query for caller information. - * Save any immediate data we get from the query. An asynchronous query may also be made - * for any data that we do not already have. Some queries, such as those for voicemail and - * emergency call information, will not perform an additional asynchronous query. - */ - final CallerInfo callerInfo = CallerInfoUtils.getCallerInfoForCall( - mContext, call, new FindInfoCallback(isIncoming)); - - findInfoQueryComplete(call, callerInfo, isIncoming, false); - } - - private void findInfoQueryComplete(Call call, CallerInfo callerInfo, boolean isIncoming, - boolean didLocalLookup) { - final String callId = call.getId(); - int presentationMode = call.getNumberPresentation(); - if (callerInfo.contactExists || callerInfo.isEmergencyNumber() || - callerInfo.isVoiceMailNumber()) { - presentationMode = TelecomManager.PRESENTATION_ALLOWED; - } - - ContactCacheEntry cacheEntry = mInfoMap.get(callId); - // Ensure we always have a cacheEntry. Replace the existing entry if - // it has no name or if we found a local contact. - if (cacheEntry == null || TextUtils.isEmpty(cacheEntry.namePrimary) || - callerInfo.contactExists) { - cacheEntry = buildEntry(mContext, callId, callerInfo, presentationMode, isIncoming); - mInfoMap.put(callId, cacheEntry); - } - - sendInfoNotifications(callId, cacheEntry); - - if (didLocalLookup) { - // Before issuing a request for more data from other services, we only check that the - // contact wasn't found in the local DB. We don't check the if the cache entry already - // has a name because we allow overriding cnap data with data from other services. - if (!callerInfo.contactExists && mPhoneNumberService != null) { - Log.d(TAG, "Contact lookup. Local contacts miss, checking remote"); - final PhoneNumberServiceListener listener = new PhoneNumberServiceListener(callId); - mPhoneNumberService.getPhoneNumberInfo(cacheEntry.number, listener, listener, - isIncoming); - } else if (cacheEntry.displayPhotoUri != null) { - Log.d(TAG, "Contact lookup. Local contact found, starting image load"); - // Load the image with a callback to update the image state. - // When the load is finished, onImageLoadComplete() will be called. - cacheEntry.isLoadingPhoto = true; - ContactsAsyncHelper.startObtainPhotoAsync(TOKEN_UPDATE_PHOTO_FOR_CALL_STATE, - mContext, cacheEntry.displayPhotoUri, ContactInfoCache.this, callId); - } else { - if (callerInfo.contactExists) { - Log.d(TAG, "Contact lookup done. Local contact found, no image."); - } else { - Log.d(TAG, "Contact lookup done. Local contact not found and" - + " no remote lookup service available."); - } - clearCallbacks(callId); - } - } - } - - class PhoneNumberServiceListener implements PhoneNumberService.NumberLookupListener, - PhoneNumberService.ImageLookupListener, ContactUtils.Listener { - private final String mCallId; - - PhoneNumberServiceListener(String callId) { - mCallId = callId; - } - - @Override - public void onPhoneNumberInfoComplete( - final PhoneNumberService.PhoneNumberInfo info) { - // If we got a miss, this is the end of the lookup pipeline, - // so clear the callbacks and return. - if (info == null) { - Log.d(TAG, "Contact lookup done. Remote contact not found."); - clearCallbacks(mCallId); - return; - } - - ContactCacheEntry entry = new ContactCacheEntry(); - entry.namePrimary = info.getDisplayName(); - entry.number = info.getNumber(); - entry.contactLookupResult = info.getLookupSource(); - final int type = info.getPhoneType(); - final String label = info.getPhoneLabel(); - if (type == Phone.TYPE_CUSTOM) { - entry.label = label; - } else { - final CharSequence typeStr = Phone.getTypeLabel( - mContext.getResources(), type, label); - entry.label = typeStr == null ? null : typeStr.toString(); - } - final ContactCacheEntry oldEntry = mInfoMap.get(mCallId); - if (oldEntry != null) { - // Location is only obtained from local lookup so persist - // the value for remote lookups. Once we have a name this - // field is no longer used; it is persisted here in case - // the UI is ever changed to use it. - entry.location = oldEntry.location; - // Contact specific ringtone is obtained from local lookup. - entry.contactRingtoneUri = oldEntry.contactRingtoneUri; - } - - // If no image and it's a business, switch to using the default business avatar. - if (info.getImageUrl() == null && info.isBusiness()) { - Log.d(TAG, "Business has no image. Using default."); - entry.photo = mContext.getResources().getDrawable(R.drawable.img_business); - } - - mInfoMap.put(mCallId, entry); - sendInfoNotifications(mCallId, entry); - - if (mContactUtils != null) { - // This method will callback "onContactInteractionsFound". - entry.isLoadingContactInteractions = - mContactUtils.retrieveContactInteractionsFromLookupKey( - info.getLookupKey(), this); - } - - entry.isLoadingPhoto = info.getImageUrl() != null; - - // If there is no image or contact interactions then we should not expect another - // callback. - if (!entry.isLoadingPhoto && !entry.isLoadingContactInteractions) { - // We're done, so clear callbacks - clearCallbacks(mCallId); - } - } - - @Override - public void onImageFetchComplete(Bitmap bitmap) { - onImageLoadComplete(TOKEN_UPDATE_PHOTO_FOR_CALL_STATE, null, bitmap, mCallId); - } - - @Override - public void onContactInteractionsFound(Address address, - List> openingHours) { - final ContactCacheEntry entry = mInfoMap.get(mCallId); - if (entry == null) { - Log.e(this, "Contact context received for empty search entry."); - clearCallbacks(mCallId); - return; - } - - entry.isLoadingContactInteractions = false; - - Log.v(ContactInfoCache.this, "Setting contact interactions for entry: ", entry); - - entry.locationAddress = address; - entry.openingHours = openingHours; - sendContactInteractionsNotifications(mCallId, entry); - - if (!entry.isLoadingPhoto) { - clearCallbacks(mCallId); - } - } - } - - /** - * Implemented for ContactsAsyncHelper.OnImageLoadCompleteListener interface. - * make sure that the call state is reflected after the image is loaded. - */ - @Override - public void onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon, Object cookie) { - Log.d(this, "Image load complete with context: ", mContext); - // TODO: may be nice to update the image view again once the newer one - // is available on contacts database. - - final String callId = (String) cookie; - final ContactCacheEntry entry = mInfoMap.get(callId); - - if (entry == null) { - Log.e(this, "Image Load received for empty search entry."); - clearCallbacks(callId); - return; - } - - entry.isLoadingPhoto = false; - - Log.d(this, "setting photo for entry: ", entry); - - // Conference call icons are being handled in CallCardPresenter. - if (photo != null) { - Log.v(this, "direct drawable: ", photo); - entry.photo = photo; - } else if (photoIcon != null) { - Log.v(this, "photo icon: ", photoIcon); - entry.photo = new BitmapDrawable(mContext.getResources(), photoIcon); - } else { - Log.v(this, "unknown photo"); - entry.photo = null; - } - - sendImageNotifications(callId, entry); - - if (!entry.isLoadingContactInteractions) { - clearCallbacks(callId); - } - } - - /** - * Blows away the stored cache values. - */ - public void clearCache() { - mInfoMap.clear(); - mCallBacks.clear(); - } - - private ContactCacheEntry buildEntry(Context context, String callId, - CallerInfo info, int presentation, boolean isIncoming) { - // The actual strings we're going to display onscreen: - Drawable photo = null; - - final ContactCacheEntry cce = new ContactCacheEntry(); - populateCacheEntry(context, info, cce, presentation, isIncoming); - - // This will only be true for emergency numbers - if (info.photoResource != 0) { - photo = context.getResources().getDrawable(info.photoResource); - } else if (info.isCachedPhotoCurrent) { - if (info.cachedPhoto != null) { - photo = info.cachedPhoto; - } else { - photo = getDefaultContactPhotoDrawable(); - } - } else if (info.contactDisplayPhotoUri == null) { - photo = getDefaultContactPhotoDrawable(); - } else { - cce.displayPhotoUri = info.contactDisplayPhotoUri; - } - - // Support any contact id in N because QuickContacts in N starts supporting enterprise - // contact id - if (info.lookupKeyOrNull != null - && (ContactsUtils.FLAG_N_FEATURE || info.contactIdOrZero != 0)) { - cce.lookupUri = Contacts.getLookupUri(info.contactIdOrZero, info.lookupKeyOrNull); - } else { - Log.v(TAG, "lookup key is null or contact ID is 0 on M. Don't create a lookup uri."); - cce.lookupUri = null; - } - - cce.photo = photo; - cce.lookupKey = info.lookupKeyOrNull; - cce.contactRingtoneUri = info.contactRingtoneUri; - if (cce.contactRingtoneUri == null || cce.contactRingtoneUri == Uri.EMPTY) { - cce.contactRingtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE); - } - - return cce; - } - - /** - * Populate a cache entry from a call (which got converted into a caller info). - */ - public static void populateCacheEntry(Context context, CallerInfo info, ContactCacheEntry cce, - int presentation, boolean isIncoming) { - Preconditions.checkNotNull(info); - String displayName = null; - String displayNumber = null; - String displayLocation = null; - String label = null; - boolean isSipCall = false; - - // It appears that there is a small change in behaviour with the - // PhoneUtils' startGetCallerInfo whereby if we query with an - // empty number, we will get a valid CallerInfo object, but with - // fields that are all null, and the isTemporary boolean input - // parameter as true. - - // In the past, we would see a NULL callerinfo object, but this - // ends up causing null pointer exceptions elsewhere down the - // line in other cases, so we need to make this fix instead. It - // appears that this was the ONLY call to PhoneUtils - // .getCallerInfo() that relied on a NULL CallerInfo to indicate - // an unknown contact. - - // Currently, infi.phoneNumber may actually be a SIP address, and - // if so, it might sometimes include the "sip:" prefix. That - // prefix isn't really useful to the user, though, so strip it off - // if present. (For any other URI scheme, though, leave the - // prefix alone.) - // TODO: It would be cleaner for CallerInfo to explicitly support - // SIP addresses instead of overloading the "phoneNumber" field. - // Then we could remove this hack, and instead ask the CallerInfo - // for a "user visible" form of the SIP address. - String number = info.phoneNumber; - - if (!TextUtils.isEmpty(number)) { - isSipCall = PhoneNumberHelper.isUriNumber(number); - if (number.startsWith("sip:")) { - number = number.substring(4); - } - } - - if (TextUtils.isEmpty(info.name)) { - // No valid "name" in the CallerInfo, so fall back to - // something else. - // (Typically, we promote the phone number up to the "name" slot - // onscreen, and possibly display a descriptive string in the - // "number" slot.) - if (TextUtils.isEmpty(number)) { - // No name *or* number! Display a generic "unknown" string - // (or potentially some other default based on the presentation.) - displayName = getPresentationString(context, presentation, info.callSubject); - Log.d(TAG, " ==> no name *or* number! displayName = " + displayName); - } else if (presentation != TelecomManager.PRESENTATION_ALLOWED) { - // This case should never happen since the network should never send a phone # - // AND a restricted presentation. However we leave it here in case of weird - // network behavior - displayName = getPresentationString(context, presentation, info.callSubject); - Log.d(TAG, " ==> presentation not allowed! displayName = " + displayName); - } else if (!TextUtils.isEmpty(info.cnapName)) { - // No name, but we do have a valid CNAP name, so use that. - displayName = info.cnapName; - info.name = info.cnapName; - displayNumber = number; - Log.d(TAG, " ==> cnapName available: displayName '" + displayName + - "', displayNumber '" + displayNumber + "'"); - } else { - // No name; all we have is a number. This is the typical - // case when an incoming call doesn't match any contact, - // or if you manually dial an outgoing number using the - // dialpad. - displayNumber = number; - - // Display a geographical description string if available - // (but only for incoming calls.) - if (isIncoming) { - // TODO (CallerInfoAsyncQuery cleanup): Fix the CallerInfo - // query to only do the geoDescription lookup in the first - // place for incoming calls. - displayLocation = info.geoDescription; // may be null - Log.d(TAG, "Geodescrption: " + info.geoDescription); - } - - Log.d(TAG, " ==> no name; falling back to number:" - + " displayNumber '" + Log.pii(displayNumber) - + "', displayLocation '" + displayLocation + "'"); - } - } else { - // We do have a valid "name" in the CallerInfo. Display that - // in the "name" slot, and the phone number in the "number" slot. - if (presentation != TelecomManager.PRESENTATION_ALLOWED) { - // This case should never happen since the network should never send a name - // AND a restricted presentation. However we leave it here in case of weird - // network behavior - displayName = getPresentationString(context, presentation, info.callSubject); - Log.d(TAG, " ==> valid name, but presentation not allowed!" + - " displayName = " + displayName); - } else { - // Causes cce.namePrimary to be set as info.name below. CallCardPresenter will - // later determine whether to use the name or nameAlternative when presenting - displayName = info.name; - cce.nameAlternative = info.nameAlternative; - displayNumber = number; - label = info.phoneLabel; - Log.d(TAG, " ==> name is present in CallerInfo: displayName '" + displayName - + "', displayNumber '" + displayNumber + "'"); - } - } - - cce.namePrimary = displayName; - cce.number = displayNumber; - cce.location = displayLocation; - cce.label = label; - cce.isSipCall = isSipCall; - cce.userType = info.userType; - - if (info.contactExists) { - cce.contactLookupResult = LogState.LOOKUP_LOCAL_CONTACT; - } - } - - /** - * Sends the updated information to call the callbacks for the entry. - */ - private void sendInfoNotifications(String callId, ContactCacheEntry entry) { - final Set callBacks = mCallBacks.get(callId); - if (callBacks != null) { - for (ContactInfoCacheCallback callBack : callBacks) { - callBack.onContactInfoComplete(callId, entry); - } - } - } - - private void sendImageNotifications(String callId, ContactCacheEntry entry) { - final Set callBacks = mCallBacks.get(callId); - if (callBacks != null && entry.photo != null) { - for (ContactInfoCacheCallback callBack : callBacks) { - callBack.onImageLoadComplete(callId, entry); - } - } - } - - private void sendContactInteractionsNotifications(String callId, ContactCacheEntry entry) { - final Set callBacks = mCallBacks.get(callId); - if (callBacks != null) { - for (ContactInfoCacheCallback callBack : callBacks) { - callBack.onContactInteractionsInfoComplete(callId, entry); - } - } - } - - private void clearCallbacks(String callId) { - mCallBacks.remove(callId); - } - - /** - * Gets name strings based on some special presentation modes and the associated custom label. - */ - private static String getPresentationString(Context context, int presentation, - String customLabel) { - String name = context.getString(R.string.unknown); - if (!TextUtils.isEmpty(customLabel) && - ((presentation == TelecomManager.PRESENTATION_UNKNOWN) || - (presentation == TelecomManager.PRESENTATION_RESTRICTED))) { - name = customLabel; - return name; - } else { - if (presentation == TelecomManager.PRESENTATION_RESTRICTED) { - name = context.getString(R.string.private_num); - } else if (presentation == TelecomManager.PRESENTATION_PAYPHONE) { - name = context.getString(R.string.payphone); - } - } - return name; - } - - public Drawable getDefaultContactPhotoDrawable() { - if (mDefaultContactPhotoDrawable == null) { - mDefaultContactPhotoDrawable = - mContext.getResources().getDrawable(R.drawable.img_no_image_automirrored); - } - return mDefaultContactPhotoDrawable; - } - - public Drawable getConferenceDrawable() { - if (mConferencePhotoDrawable == null) { - mConferencePhotoDrawable = - mContext.getResources().getDrawable(R.drawable.img_conference_automirrored); - } - return mConferencePhotoDrawable; - } - - /** - * Callback interface for the contact query. - */ - public interface ContactInfoCacheCallback { - public void onContactInfoComplete(String callId, ContactCacheEntry entry); - public void onImageLoadComplete(String callId, ContactCacheEntry entry); - public void onContactInteractionsInfoComplete(String callId, ContactCacheEntry entry); - } - - public static class ContactCacheEntry { - public String namePrimary; - public String nameAlternative; - public String number; - public String location; - public String label; - public Drawable photo; - public boolean isSipCall; - // Note in cache entry whether this is a pending async loading action to know whether to - // wait for its callback or not. - public boolean isLoadingPhoto; - public boolean isLoadingContactInteractions; - /** This will be used for the "view" notification. */ - public Uri contactUri; - /** Either a display photo or a thumbnail URI. */ - public Uri displayPhotoUri; - public Uri lookupUri; // Sent to NotificationMananger - public String lookupKey; - public Address locationAddress; - public List> openingHours; - public int contactLookupResult = LogState.LOOKUP_NOT_FOUND; - public long userType = ContactsUtils.USER_TYPE_CURRENT; - public Uri contactRingtoneUri; - - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("name", MoreStrings.toSafeString(namePrimary)) - .add("nameAlternative", MoreStrings.toSafeString(nameAlternative)) - .add("number", MoreStrings.toSafeString(number)) - .add("location", MoreStrings.toSafeString(location)) - .add("label", label) - .add("photo", photo) - .add("isSipCall", isSipCall) - .add("contactUri", contactUri) - .add("displayPhotoUri", displayPhotoUri) - .add("locationAddress", locationAddress) - .add("openingHours", openingHours) - .add("contactLookupResult", contactLookupResult) - .add("userType", userType) - .add("contactRingtoneUri", contactRingtoneUri) - .toString(); - } - } -} diff --git a/InCallUI/src/com/android/incallui/ContactUtils.java b/InCallUI/src/com/android/incallui/ContactUtils.java deleted file mode 100644 index 0750af731..000000000 --- a/InCallUI/src/com/android/incallui/ContactUtils.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ -package com.android.incallui; - -import android.content.Context; -import android.location.Address; -import android.util.Pair; - -import com.android.incalluibind.ObjectFactory; - -import java.util.Calendar; -import java.util.List; - -/** - * Utility functions to help manipulate contact data. - */ -public abstract class ContactUtils { - protected Context mContext; - - public static ContactUtils getInstance(Context context) { - return ObjectFactory.getContactUtilsInstance(context); - } - - protected ContactUtils(Context context) { - mContext = context; - } - - public interface Listener { - public void onContactInteractionsFound(Address address, - List> openingHours); - } - - public abstract boolean retrieveContactInteractionsFromLookupKey(String lookupKey, - Listener listener); -} diff --git a/InCallUI/src/com/android/incallui/ContactsAsyncHelper.java b/InCallUI/src/com/android/incallui/ContactsAsyncHelper.java deleted file mode 100644 index d959fadd4..000000000 --- a/InCallUI/src/com/android/incallui/ContactsAsyncHelper.java +++ /dev/null @@ -1,258 +0,0 @@ -/* - * Copyright (C) 2008 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui; - -import android.app.Notification; -import android.content.ContentUris; -import android.content.Context; -import android.content.res.AssetFileDescriptor; -import android.graphics.Bitmap; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.Looper; -import android.os.Message; -import android.provider.ContactsContract.Contacts; - -import com.android.dialer.R; - -import java.io.IOException; -import java.io.InputStream; - -/** - * Helper class for loading contacts photo asynchronously. - */ -public class ContactsAsyncHelper { - - /** - * Interface for a WorkerHandler result return. - */ - public interface OnImageLoadCompleteListener { - /** - * Called when the image load is complete. - * - * @param token Integer passed in {@link ContactsAsyncHelper#startObtainPhotoAsync(int, - * Context, Uri, OnImageLoadCompleteListener, Object)}. - * @param photo Drawable object obtained by the async load. - * @param photoIcon Bitmap object obtained by the async load. - * @param cookie Object passed in {@link ContactsAsyncHelper#startObtainPhotoAsync(int, - * Context, Uri, OnImageLoadCompleteListener, Object)}. Can be null iff. the original - * cookie is null. - */ - public void onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon, - Object cookie); - } - - // constants - private static final int EVENT_LOAD_IMAGE = 1; - - private final Handler mResultHandler = new Handler() { - /** Called when loading is done. */ - @Override - public void handleMessage(Message msg) { - WorkerArgs args = (WorkerArgs) msg.obj; - switch (msg.arg1) { - case EVENT_LOAD_IMAGE: - if (args.listener != null) { - Log.d(this, "Notifying listener: " + args.listener.toString() + - " image: " + args.displayPhotoUri + " completed"); - args.listener.onImageLoadComplete(msg.what, args.photo, args.photoIcon, - args.cookie); - } - break; - default: - } - } - }; - - /** Handler run on a worker thread to load photo asynchronously. */ - private static Handler sThreadHandler; - - /** For forcing the system to call its constructor */ - @SuppressWarnings("unused") - private static ContactsAsyncHelper sInstance; - - static { - sInstance = new ContactsAsyncHelper(); - } - - private static final class WorkerArgs { - public Context context; - public Uri displayPhotoUri; - public Drawable photo; - public Bitmap photoIcon; - public Object cookie; - public OnImageLoadCompleteListener listener; - } - - /** - * Thread worker class that handles the task of opening the stream and loading - * the images. - */ - private class WorkerHandler extends Handler { - public WorkerHandler(Looper looper) { - super(looper); - } - - @Override - public void handleMessage(Message msg) { - WorkerArgs args = (WorkerArgs) msg.obj; - - switch (msg.arg1) { - case EVENT_LOAD_IMAGE: - InputStream inputStream = null; - try { - try { - inputStream = args.context.getContentResolver() - .openInputStream(args.displayPhotoUri); - } catch (Exception e) { - Log.e(this, "Error opening photo input stream", e); - } - - if (inputStream != null) { - args.photo = Drawable.createFromStream(inputStream, - args.displayPhotoUri.toString()); - - // This assumes Drawable coming from contact database is usually - // BitmapDrawable and thus we can have (down)scaled version of it. - args.photoIcon = getPhotoIconWhenAppropriate(args.context, args.photo); - - Log.d(ContactsAsyncHelper.this, "Loading image: " + msg.arg1 + - " token: " + msg.what + " image URI: " + args.displayPhotoUri); - } else { - args.photo = null; - args.photoIcon = null; - Log.d(ContactsAsyncHelper.this, "Problem with image: " + msg.arg1 + - " token: " + msg.what + " image URI: " + args.displayPhotoUri + - ", using default image."); - } - } finally { - if (inputStream != null) { - try { - inputStream.close(); - } catch (IOException e) { - Log.e(this, "Unable to close input stream.", e); - } - } - } - break; - default: - } - - // send the reply to the enclosing class. - Message reply = ContactsAsyncHelper.this.mResultHandler.obtainMessage(msg.what); - reply.arg1 = msg.arg1; - reply.obj = msg.obj; - reply.sendToTarget(); - } - - /** - * Returns a Bitmap object suitable for {@link Notification}'s large icon. This might - * return null when the given Drawable isn't BitmapDrawable, or if the system fails to - * create a scaled Bitmap for the Drawable. - */ - private Bitmap getPhotoIconWhenAppropriate(Context context, Drawable photo) { - if (!(photo instanceof BitmapDrawable)) { - return null; - } - int iconSize = context.getResources() - .getDimensionPixelSize(R.dimen.notification_icon_size); - Bitmap orgBitmap = ((BitmapDrawable) photo).getBitmap(); - int orgWidth = orgBitmap.getWidth(); - int orgHeight = orgBitmap.getHeight(); - int longerEdge = orgWidth > orgHeight ? orgWidth : orgHeight; - // We want downscaled one only when the original icon is too big. - if (longerEdge > iconSize) { - float ratio = ((float) longerEdge) / iconSize; - int newWidth = (int) (orgWidth / ratio); - int newHeight = (int) (orgHeight / ratio); - // If the longer edge is much longer than the shorter edge, the latter may - // become 0 which will cause a crash. - if (newWidth <= 0 || newHeight <= 0) { - Log.w(this, "Photo icon's width or height become 0."); - return null; - } - - // It is sure ratio >= 1.0f in any case and thus the newly created Bitmap - // should be smaller than the original. - return Bitmap.createScaledBitmap(orgBitmap, newWidth, newHeight, true); - } else { - return orgBitmap; - } - } - } - - /** - * Private constructor for static class - */ - private ContactsAsyncHelper() { - HandlerThread thread = new HandlerThread("ContactsAsyncWorker"); - thread.start(); - sThreadHandler = new WorkerHandler(thread.getLooper()); - } - - /** - * Starts an asynchronous image load. After finishing the load, - * {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap, Object)} - * will be called. - * - * @param token Arbitrary integer which will be returned as the first argument of - * {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap, Object)} - * @param context Context object used to do the time-consuming operation. - * @param displayPhotoUri Uri to be used to fetch the photo - * @param listener Callback object which will be used when the asynchronous load is done. - * Can be null, which means only the asynchronous load is done while there's no way to - * obtain the loaded photos. - * @param cookie Arbitrary object the caller wants to remember, which will become the - * fourth argument of {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, - * Bitmap, Object)}. Can be null, at which the callback will also has null for the argument. - */ - public static final void startObtainPhotoAsync(int token, Context context, Uri displayPhotoUri, - OnImageLoadCompleteListener listener, Object cookie) { - // in case the source caller info is null, the URI will be null as well. - // just update using the placeholder image in this case. - if (displayPhotoUri == null) { - Log.wtf("startObjectPhotoAsync", "Uri is missing"); - return; - } - - // Added additional Cookie field in the callee to handle arguments - // sent to the callback function. - - // setup arguments - WorkerArgs args = new WorkerArgs(); - args.cookie = cookie; - args.context = context; - args.displayPhotoUri = displayPhotoUri; - args.listener = listener; - - // setup message arguments - Message msg = sThreadHandler.obtainMessage(token); - msg.arg1 = EVENT_LOAD_IMAGE; - msg.obj = args; - - Log.d("startObjectPhotoAsync", "Begin loading image: " + args.displayPhotoUri + - ", displaying default image for now."); - - // notify the thread to begin working - sThreadHandler.sendMessage(msg); - } - - -} diff --git a/InCallUI/src/com/android/incallui/ContactsPreferencesFactory.java b/InCallUI/src/com/android/incallui/ContactsPreferencesFactory.java deleted file mode 100644 index a9cc93bda..000000000 --- a/InCallUI/src/com/android/incallui/ContactsPreferencesFactory.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.content.Context; -import android.support.annotation.Nullable; -import com.android.dialer.compat.UserManagerCompat; - -import com.android.contacts.common.preference.ContactsPreferences; -import com.android.contacts.common.testing.NeededForTesting; - -/** - * Factory class for {@link ContactsPreferences}. - */ -public class ContactsPreferencesFactory { - - private static boolean sUseTestInstance; - private static ContactsPreferences sTestInstance; - - /** - * Creates a new {@link ContactsPreferences} object if possible. - * - * @param context the context to use when creating the ContactsPreferences. - * @return a new ContactsPreferences object or {@code null} if the user is locked. - */ - @Nullable - public static ContactsPreferences newContactsPreferences(Context context) { - if (sUseTestInstance) { - return sTestInstance; - } - if (UserManagerCompat.isUserUnlocked(context)) { - return new ContactsPreferences(context); - } - return null; - } - - /** - * Sets the instance to be returned by all calls to {@link #newContactsPreferences(Context)}. - * - * @param testInstance the instance to return. - */ - @NeededForTesting - static void setTestInstance(ContactsPreferences testInstance) { - sUseTestInstance = true; - sTestInstance = testInstance; - } -} diff --git a/InCallUI/src/com/android/incallui/DialpadFragment.java b/InCallUI/src/com/android/incallui/DialpadFragment.java deleted file mode 100644 index ad288bdc6..000000000 --- a/InCallUI/src/com/android/incallui/DialpadFragment.java +++ /dev/null @@ -1,563 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.content.Context; -import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import android.text.Editable; -import android.text.method.DialerKeyListener; -import android.util.AttributeSet; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.view.accessibility.AccessibilityManager; -import android.widget.EditText; -import android.widget.LinearLayout; -import android.widget.TextView; - -import com.android.contacts.common.compat.PhoneNumberUtilsCompat; -import com.android.dialer.R; -import com.android.phone.common.dialpad.DialpadKeyButton; -import com.android.phone.common.dialpad.DialpadView; - -import java.util.HashMap; - -/** - * Fragment for call control buttons - */ -public class DialpadFragment extends BaseFragment - implements DialpadPresenter.DialpadUi, View.OnTouchListener, View.OnKeyListener, - View.OnHoverListener, View.OnClickListener { - - private static final int ACCESSIBILITY_DTMF_STOP_DELAY_MILLIS = 50; - - private final int[] mButtonIds = new int[] {R.id.zero, R.id.one, R.id.two, R.id.three, - R.id.four, R.id.five, R.id.six, R.id.seven, R.id.eight, R.id.nine, R.id.star, - R.id.pound}; - - /** - * LinearLayout with getter and setter methods for the translationY property using floats, - * for animation purposes. - */ - public static class DialpadSlidingLinearLayout extends LinearLayout { - - public DialpadSlidingLinearLayout(Context context) { - super(context); - } - - public DialpadSlidingLinearLayout(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public DialpadSlidingLinearLayout(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - public float getYFraction() { - final int height = getHeight(); - if (height == 0) return 0; - return getTranslationY() / height; - } - - public void setYFraction(float yFraction) { - setTranslationY(yFraction * getHeight()); - } - } - - private EditText mDtmfDialerField; - - /** Hash Map to map a view id to a character*/ - private static final HashMap mDisplayMap = - new HashMap(); - - private static final Handler sHandler = new Handler(Looper.getMainLooper()); - - - /** Set up the static maps*/ - static { - // Map the buttons to the display characters - mDisplayMap.put(R.id.one, '1'); - mDisplayMap.put(R.id.two, '2'); - mDisplayMap.put(R.id.three, '3'); - mDisplayMap.put(R.id.four, '4'); - mDisplayMap.put(R.id.five, '5'); - mDisplayMap.put(R.id.six, '6'); - mDisplayMap.put(R.id.seven, '7'); - mDisplayMap.put(R.id.eight, '8'); - mDisplayMap.put(R.id.nine, '9'); - mDisplayMap.put(R.id.zero, '0'); - mDisplayMap.put(R.id.pound, '#'); - mDisplayMap.put(R.id.star, '*'); - } - - // KeyListener used with the "dialpad digits" EditText widget. - private DTMFKeyListener mDialerKeyListener; - - private DialpadView mDialpadView; - - private int mCurrentTextColor; - - /** - * Our own key listener, specialized for dealing with DTMF codes. - * 1. Ignore the backspace since it is irrelevant. - * 2. Allow ONLY valid DTMF characters to generate a tone and be - * sent as a DTMF code. - * 3. All other remaining characters are handled by the superclass. - * - * This code is purely here to handle events from the hardware keyboard - * while the DTMF dialpad is up. - */ - private class DTMFKeyListener extends DialerKeyListener { - - private DTMFKeyListener() { - super(); - } - - /** - * Overriden to return correct DTMF-dialable characters. - */ - @Override - protected char[] getAcceptedChars(){ - return DTMF_CHARACTERS; - } - - /** special key listener ignores backspace. */ - @Override - public boolean backspace(View view, Editable content, int keyCode, - KeyEvent event) { - return false; - } - - /** - * Return true if the keyCode is an accepted modifier key for the - * dialer (ALT or SHIFT). - */ - private boolean isAcceptableModifierKey(int keyCode) { - switch (keyCode) { - case KeyEvent.KEYCODE_ALT_LEFT: - case KeyEvent.KEYCODE_ALT_RIGHT: - case KeyEvent.KEYCODE_SHIFT_LEFT: - case KeyEvent.KEYCODE_SHIFT_RIGHT: - return true; - default: - return false; - } - } - - /** - * Overriden so that with each valid button press, we start sending - * a dtmf code and play a local dtmf tone. - */ - @Override - public boolean onKeyDown(View view, Editable content, - int keyCode, KeyEvent event) { - // if (DBG) log("DTMFKeyListener.onKeyDown, keyCode " + keyCode + ", view " + view); - - // find the character - char c = (char) lookup(event, content); - - // if not a long press, and parent onKeyDown accepts the input - if (event.getRepeatCount() == 0 && super.onKeyDown(view, content, keyCode, event)) { - - boolean keyOK = ok(getAcceptedChars(), c); - - // if the character is a valid dtmf code, start playing the tone and send the - // code. - if (keyOK) { - Log.d(this, "DTMFKeyListener reading '" + c + "' from input."); - getPresenter().processDtmf(c); - } else { - Log.d(this, "DTMFKeyListener rejecting '" + c + "' from input."); - } - return true; - } - return false; - } - - /** - * Overriden so that with each valid button up, we stop sending - * a dtmf code and the dtmf tone. - */ - @Override - public boolean onKeyUp(View view, Editable content, - int keyCode, KeyEvent event) { - // if (DBG) log("DTMFKeyListener.onKeyUp, keyCode " + keyCode + ", view " + view); - - super.onKeyUp(view, content, keyCode, event); - - // find the character - char c = (char) lookup(event, content); - - boolean keyOK = ok(getAcceptedChars(), c); - - if (keyOK) { - Log.d(this, "Stopping the tone for '" + c + "'"); - getPresenter().stopDtmf(); - return true; - } - - return false; - } - - /** - * Handle individual keydown events when we DO NOT have an Editable handy. - */ - public boolean onKeyDown(KeyEvent event) { - char c = lookup(event); - Log.d(this, "DTMFKeyListener.onKeyDown: event '" + c + "'"); - - // if not a long press, and parent onKeyDown accepts the input - if (event.getRepeatCount() == 0 && c != 0) { - // if the character is a valid dtmf code, start playing the tone and send the - // code. - if (ok(getAcceptedChars(), c)) { - Log.d(this, "DTMFKeyListener reading '" + c + "' from input."); - getPresenter().processDtmf(c); - return true; - } else { - Log.d(this, "DTMFKeyListener rejecting '" + c + "' from input."); - } - } - return false; - } - - /** - * Handle individual keyup events. - * - * @param event is the event we are trying to stop. If this is null, - * then we just force-stop the last tone without checking if the event - * is an acceptable dialer event. - */ - public boolean onKeyUp(KeyEvent event) { - if (event == null) { - //the below piece of code sends stopDTMF event unnecessarily even when a null event - //is received, hence commenting it. - /*if (DBG) log("Stopping the last played tone."); - stopTone();*/ - return true; - } - - char c = lookup(event); - Log.d(this, "DTMFKeyListener.onKeyUp: event '" + c + "'"); - - // TODO: stopTone does not take in character input, we may want to - // consider checking for this ourselves. - if (ok(getAcceptedChars(), c)) { - Log.d(this, "Stopping the tone for '" + c + "'"); - getPresenter().stopDtmf(); - return true; - } - - return false; - } - - /** - * Find the Dialer Key mapped to this event. - * - * @return The char value of the input event, otherwise - * 0 if no matching character was found. - */ - private char lookup(KeyEvent event) { - // This code is similar to {@link DialerKeyListener#lookup(KeyEvent, Spannable) lookup} - int meta = event.getMetaState(); - int number = event.getNumber(); - - if (!((meta & (KeyEvent.META_ALT_ON | KeyEvent.META_SHIFT_ON)) == 0) || (number == 0)) { - int match = event.getMatch(getAcceptedChars(), meta); - number = (match != 0) ? match : number; - } - - return (char) number; - } - - /** - * Check to see if the keyEvent is dialable. - */ - boolean isKeyEventAcceptable (KeyEvent event) { - return (ok(getAcceptedChars(), lookup(event))); - } - - /** - * Overrides the characters used in {@link DialerKeyListener#CHARACTERS} - * These are the valid dtmf characters. - */ - public final char[] DTMF_CHARACTERS = new char[] { - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '#', '*' - }; - } - - @Override - public void onClick(View v) { - final AccessibilityManager accessibilityManager = (AccessibilityManager) - v.getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); - // When accessibility is on, simulate press and release to preserve the - // semantic meaning of performClick(). Required for Braille support. - if (accessibilityManager.isEnabled()) { - final int id = v.getId(); - // Checking the press state prevents double activation. - if (!v.isPressed() && mDisplayMap.containsKey(id)) { - getPresenter().processDtmf(mDisplayMap.get(id)); - sHandler.postDelayed(new Runnable() { - @Override - public void run() { - getPresenter().stopDtmf(); - } - }, ACCESSIBILITY_DTMF_STOP_DELAY_MILLIS); - } - } - if (v.getId() == R.id.dialpad_back) { - getActivity().onBackPressed(); - } - } - - @Override - public boolean onHover(View v, MotionEvent event) { - // When touch exploration is turned on, lifting a finger while inside - // the button's hover target bounds should perform a click action. - final AccessibilityManager accessibilityManager = (AccessibilityManager) - v.getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); - - if (accessibilityManager.isEnabled() - && accessibilityManager.isTouchExplorationEnabled()) { - final int left = v.getPaddingLeft(); - final int right = (v.getWidth() - v.getPaddingRight()); - final int top = v.getPaddingTop(); - final int bottom = (v.getHeight() - v.getPaddingBottom()); - - switch (event.getActionMasked()) { - case MotionEvent.ACTION_HOVER_ENTER: - // Lift-to-type temporarily disables double-tap activation. - v.setClickable(false); - break; - case MotionEvent.ACTION_HOVER_EXIT: - final int x = (int) event.getX(); - final int y = (int) event.getY(); - if ((x > left) && (x < right) && (y > top) && (y < bottom)) { - v.performClick(); - } - v.setClickable(true); - break; - } - } - - return false; - } - - @Override - public boolean onKey(View v, int keyCode, KeyEvent event) { - Log.d(this, "onKey: keyCode " + keyCode + ", view " + v); - - if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER) { - int viewId = v.getId(); - if (mDisplayMap.containsKey(viewId)) { - switch (event.getAction()) { - case KeyEvent.ACTION_DOWN: - if (event.getRepeatCount() == 0) { - getPresenter().processDtmf(mDisplayMap.get(viewId)); - } - break; - case KeyEvent.ACTION_UP: - getPresenter().stopDtmf(); - break; - } - // do not return true [handled] here, since we want the - // press / click animation to be handled by the framework. - } - } - return false; - } - - @Override - public boolean onTouch(View v, MotionEvent event) { - Log.d(this, "onTouch"); - int viewId = v.getId(); - - // if the button is recognized - if (mDisplayMap.containsKey(viewId)) { - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - // Append the character mapped to this button, to the display. - // start the tone - getPresenter().processDtmf(mDisplayMap.get(viewId)); - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - // stop the tone on ANY other event, except for MOVE. - getPresenter().stopDtmf(); - break; - } - // do not return true [handled] here, since we want the - // press / click animation to be handled by the framework. - } - return false; - } - - // TODO(klp) Adds hardware keyboard listener - - @Override - public DialpadPresenter createPresenter() { - return new DialpadPresenter(); - } - - @Override - public DialpadPresenter.DialpadUi getUi() { - return this; - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - final View parent = inflater.inflate( - R.layout.incall_dialpad_fragment, container, false); - mDialpadView = (DialpadView) parent.findViewById(R.id.dialpad_view); - mDialpadView.setCanDigitsBeEdited(false); - mDialpadView.setBackgroundResource(R.color.incall_dialpad_background); - mDtmfDialerField = (EditText) parent.findViewById(R.id.digits); - if (mDtmfDialerField != null) { - mDialerKeyListener = new DTMFKeyListener(); - mDtmfDialerField.setKeyListener(mDialerKeyListener); - // remove the long-press context menus that support - // the edit (copy / paste / select) functions. - mDtmfDialerField.setLongClickable(false); - mDtmfDialerField.setElegantTextHeight(false); - configureKeypadListeners(); - } - View backButton = mDialpadView.findViewById(R.id.dialpad_back); - backButton.setVisibility(View.VISIBLE); - backButton.setOnClickListener(this); - - return parent; - } - - @Override - public void onResume() { - super.onResume(); - updateColors(); - } - - public void updateColors() { - int textColor = InCallPresenter.getInstance().getThemeColors().mPrimaryColor; - - if (mCurrentTextColor == textColor) { - return; - } - - DialpadKeyButton dialpadKey; - for (int i = 0; i < mButtonIds.length; i++) { - dialpadKey = (DialpadKeyButton) mDialpadView.findViewById(mButtonIds[i]); - ((TextView) dialpadKey.findViewById(R.id.dialpad_key_number)).setTextColor(textColor); - } - - mCurrentTextColor = textColor; - } - - @Override - public void onDestroyView() { - mDialerKeyListener = null; - super.onDestroyView(); - } - - /** - * Getter for Dialpad text. - * - * @return String containing current Dialpad EditText text. - */ - public String getDtmfText() { - return mDtmfDialerField.getText().toString(); - } - - /** - * Sets the Dialpad text field with some text. - * - * @param text Text to set Dialpad EditText to. - */ - public void setDtmfText(String text) { - mDtmfDialerField.setText(PhoneNumberUtilsCompat.createTtsSpannable(text)); - } - - @Override - public void setVisible(boolean on) { - if (on) { - getView().setVisibility(View.VISIBLE); - } else { - getView().setVisibility(View.INVISIBLE); - } - } - - /** - * Starts the slide up animation for the Dialpad keys when the Dialpad is revealed. - */ - public void animateShowDialpad() { - final DialpadView dialpadView = (DialpadView) getView().findViewById(R.id.dialpad_view); - dialpadView.animateShow(); - } - - @Override - public void appendDigitsToField(char digit) { - if (mDtmfDialerField != null) { - // TODO: maybe *don't* manually append this digit if - // mDialpadDigits is focused and this key came from the HW - // keyboard, since in that case the EditText field will - // get the key event directly and automatically appends - // whetever the user types. - // (Or, a cleaner fix would be to just make mDialpadDigits - // *not* handle HW key presses. That seems to be more - // complicated than just setting focusable="false" on it, - // though.) - mDtmfDialerField.getText().append(digit); - } - } - - /** - * Called externally (from InCallScreen) to play a DTMF Tone. - */ - /* package */ boolean onDialerKeyDown(KeyEvent event) { - Log.d(this, "Notifying dtmf key down."); - if (mDialerKeyListener != null) { - return mDialerKeyListener.onKeyDown(event); - } else { - return false; - } - } - - /** - * Called externally (from InCallScreen) to cancel the last DTMF Tone played. - */ - public boolean onDialerKeyUp(KeyEvent event) { - Log.d(this, "Notifying dtmf key up."); - if (mDialerKeyListener != null) { - return mDialerKeyListener.onKeyUp(event); - } else { - return false; - } - } - - private void configureKeypadListeners() { - DialpadKeyButton dialpadKey; - for (int i = 0; i < mButtonIds.length; i++) { - dialpadKey = (DialpadKeyButton) mDialpadView.findViewById(mButtonIds[i]); - dialpadKey.setOnTouchListener(this); - dialpadKey.setOnKeyListener(this); - dialpadKey.setOnHoverListener(this); - dialpadKey.setOnClickListener(this); - } - } -} diff --git a/InCallUI/src/com/android/incallui/DialpadPresenter.java b/InCallUI/src/com/android/incallui/DialpadPresenter.java deleted file mode 100644 index 5e24bedef..000000000 --- a/InCallUI/src/com/android/incallui/DialpadPresenter.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.telephony.PhoneNumberUtils; - -/** - * Logic for call buttons. - */ -public class DialpadPresenter extends Presenter - implements InCallPresenter.InCallStateListener { - - private Call mCall; - - @Override - public void onUiReady(DialpadUi ui) { - super.onUiReady(ui); - InCallPresenter.getInstance().addListener(this); - mCall = CallList.getInstance().getOutgoingOrActive(); - } - - @Override - public void onUiUnready(DialpadUi ui) { - super.onUiUnready(ui); - InCallPresenter.getInstance().removeListener(this); - } - - @Override - public void onStateChange(InCallPresenter.InCallState oldState, - InCallPresenter.InCallState newState, CallList callList) { - mCall = callList.getOutgoingOrActive(); - Log.d(this, "DialpadPresenter mCall = " + mCall); - } - - /** - * Processes the specified digit as a DTMF key, by playing the - * appropriate DTMF tone, and appending the digit to the EditText - * field that displays the DTMF digits sent so far. - * - */ - public final void processDtmf(char c) { - Log.d(this, "Processing dtmf key " + c); - // if it is a valid key, then update the display and send the dtmf tone. - if (PhoneNumberUtils.is12Key(c) && mCall != null) { - Log.d(this, "updating display and sending dtmf tone for '" + c + "'"); - - // Append this key to the "digits" widget. - getUi().appendDigitsToField(c); - // Plays the tone through Telecom. - TelecomAdapter.getInstance().playDtmfTone(mCall.getId(), c); - } else { - Log.d(this, "ignoring dtmf request for '" + c + "'"); - } - } - - /** - * Stops the local tone based on the phone type. - */ - public void stopDtmf() { - if (mCall != null) { - Log.d(this, "stopping remote tone"); - TelecomAdapter.getInstance().stopDtmfTone(mCall.getId()); - } - } - - public interface DialpadUi extends Ui { - void setVisible(boolean on); - void appendDigitsToField(char digit); - } -} diff --git a/InCallUI/src/com/android/incallui/DistanceHelper.java b/InCallUI/src/com/android/incallui/DistanceHelper.java deleted file mode 100644 index a4db5fed3..000000000 --- a/InCallUI/src/com/android/incallui/DistanceHelper.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.incallui; - -import android.location.Address; - -/** - * Superclass for a helper class to get the current location and distance to other locations. - */ -public abstract class DistanceHelper { - public static final float DISTANCE_NOT_FOUND = -1; - public static final float MILES_PER_METER = (float) 0.000621371192; - public static final float KILOMETERS_PER_METER = (float) 0.001; - - public interface Listener { - public void onLocationReady(); - } - - public void cleanUp() {} - - public float calculateDistance(Address address) { - return DISTANCE_NOT_FOUND; - } -} diff --git a/InCallUI/src/com/android/incallui/ExternalCallList.java b/InCallUI/src/com/android/incallui/ExternalCallList.java deleted file mode 100644 index 06e0bb975..000000000 --- a/InCallUI/src/com/android/incallui/ExternalCallList.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.android.incallui; - -import com.google.common.base.Preconditions; - -import com.android.contacts.common.compat.CallSdkCompat; - -import android.os.Handler; -import android.os.Looper; -import android.telecom.Call; -import android.util.ArraySet; - -import java.util.Collections; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; - -/** - * Tracks the external calls known to the InCall UI. - * - * External calls are those with {@link android.telecom.Call.Details#PROPERTY_IS_EXTERNAL_CALL}. - */ -public class ExternalCallList { - - public interface ExternalCallListener { - void onExternalCallAdded(Call call); - void onExternalCallRemoved(Call call); - void onExternalCallUpdated(Call call); - } - - /** - * Handles {@link android.telecom.Call.Callback} callbacks. - */ - private final Call.Callback mTelecomCallCallback = new Call.Callback() { - @Override - public void onDetailsChanged(Call call, Call.Details details) { - notifyExternalCallUpdated(call); - } - }; - - private final Set mExternalCalls = new ArraySet<>(); - private final Set mExternalCallListeners = Collections.newSetFromMap( - new ConcurrentHashMap(8, 0.9f, 1)); - - /** - * Begins tracking an external call and notifies listeners of the new call. - */ - public void onCallAdded(Call telecomCall) { - Preconditions.checkArgument(telecomCall.getDetails() - .hasProperty(CallSdkCompat.Details.PROPERTY_IS_EXTERNAL_CALL)); - mExternalCalls.add(telecomCall); - telecomCall.registerCallback(mTelecomCallCallback, new Handler(Looper.getMainLooper())); - notifyExternalCallAdded(telecomCall); - } - - /** - * Stops tracking an external call and notifies listeners of the removal of the call. - */ - public void onCallRemoved(Call telecomCall) { - Preconditions.checkArgument(mExternalCalls.contains(telecomCall)); - mExternalCalls.remove(telecomCall); - telecomCall.unregisterCallback(mTelecomCallCallback); - notifyExternalCallRemoved(telecomCall); - } - - /** - * Adds a new listener to external call events. - */ - public void addExternalCallListener(ExternalCallListener listener) { - mExternalCallListeners.add(Preconditions.checkNotNull(listener)); - } - - /** - * Removes a listener to external call events. - */ - public void removeExternalCallListener(ExternalCallListener listener) { - Preconditions.checkArgument(mExternalCallListeners.contains(listener)); - mExternalCallListeners.remove(Preconditions.checkNotNull(listener)); - } - - /** - * Notifies listeners of the addition of a new external call. - */ - private void notifyExternalCallAdded(Call call) { - for (ExternalCallListener listener : mExternalCallListeners) { - listener.onExternalCallAdded(call); - } - } - - /** - * Notifies listeners of the removal of an external call. - */ - private void notifyExternalCallRemoved(Call call) { - for (ExternalCallListener listener : mExternalCallListeners) { - listener.onExternalCallRemoved(call); - } - } - - /** - * Notifies listeners of changes to an external call. - */ - private void notifyExternalCallUpdated(Call call) { - for (ExternalCallListener listener : mExternalCallListeners) { - listener.onExternalCallUpdated(call); - } - } -} diff --git a/InCallUI/src/com/android/incallui/ExternalCallNotifier.java b/InCallUI/src/com/android/incallui/ExternalCallNotifier.java deleted file mode 100644 index 639a46da0..000000000 --- a/InCallUI/src/com/android/incallui/ExternalCallNotifier.java +++ /dev/null @@ -1,406 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui; - -import com.google.common.base.Preconditions; - -import com.android.contacts.common.ContactsUtils; -import com.android.contacts.common.compat.CallSdkCompat; -import com.android.contacts.common.preference.ContactsPreferences; -import com.android.contacts.common.util.BitmapUtil; -import com.android.contacts.common.util.ContactDisplayUtils; -import com.android.dialer.R; -import com.android.incallui.util.TelecomCallUtil; - -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.drawable.BitmapDrawable; -import android.net.Uri; -import android.support.annotation.Nullable; -import android.telecom.Call; -import android.telecom.PhoneAccount; -import android.text.BidiFormatter; -import android.text.TextDirectionHeuristics; -import android.text.TextUtils; -import android.util.ArrayMap; - -import java.util.Map; - -/** - * Handles the display of notifications for "external calls". - * - * External calls are a representation of a call which is in progress on the user's other device - * (e.g. another phone, or a watch). - */ -public class ExternalCallNotifier implements ExternalCallList.ExternalCallListener { - - /** - * Tag used with the notification manager to uniquely identify external call notifications. - */ - private static final String NOTIFICATION_TAG = "EXTERNAL_CALL"; - - /** - * Represents a call and associated cached notification data. - */ - private static class NotificationInfo { - private final Call mCall; - private final int mNotificationId; - @Nullable private String mContentTitle; - @Nullable private Bitmap mLargeIcon; - @Nullable private String mPersonReference; - - public NotificationInfo(Call call, int notificationId) { - Preconditions.checkNotNull(call); - mCall = call; - mNotificationId = notificationId; - } - - public Call getCall() { - return mCall; - } - - public int getNotificationId() { - return mNotificationId; - } - - public @Nullable String getContentTitle() { - return mContentTitle; - } - - public @Nullable Bitmap getLargeIcon() { - return mLargeIcon; - } - - public @Nullable String getPersonReference() { - return mPersonReference; - } - - public void setContentTitle(@Nullable String contentTitle) { - mContentTitle = contentTitle; - } - - public void setLargeIcon(@Nullable Bitmap largeIcon) { - mLargeIcon = largeIcon; - } - - public void setPersonReference(@Nullable String personReference) { - mPersonReference = personReference; - } - } - - private final Context mContext; - private final ContactInfoCache mContactInfoCache; - private Map mNotifications = new ArrayMap<>(); - private int mNextUniqueNotificationId; - private ContactsPreferences mContactsPreferences; - - /** - * Initializes a new instance of the external call notifier. - */ - public ExternalCallNotifier(Context context, ContactInfoCache contactInfoCache) { - mContext = Preconditions.checkNotNull(context); - mContactsPreferences = ContactsPreferencesFactory.newContactsPreferences(mContext); - mContactInfoCache = Preconditions.checkNotNull(contactInfoCache); - } - - /** - * Handles the addition of a new external call by showing a new notification. - * Triggered by {@link CallList#onCallAdded(android.telecom.Call)}. - */ - @Override - public void onExternalCallAdded(android.telecom.Call call) { - Log.i(this, "onExternalCallAdded " + call); - Preconditions.checkArgument(!mNotifications.containsKey(call)); - NotificationInfo info = new NotificationInfo(call, mNextUniqueNotificationId++); - mNotifications.put(call, info); - - showNotifcation(info); - } - - /** - * Handles the removal of an external call by hiding its associated notification. - * Triggered by {@link CallList#onCallRemoved(android.telecom.Call)}. - */ - @Override - public void onExternalCallRemoved(android.telecom.Call call) { - Log.i(this, "onExternalCallRemoved " + call); - - dismissNotification(call); - } - - /** - * Handles updates to an external call. - */ - @Override - public void onExternalCallUpdated(Call call) { - Preconditions.checkArgument(mNotifications.containsKey(call)); - postNotification(mNotifications.get(call)); - } - - /** - * Initiates a call pull given a notification ID. - * - * @param notificationId The notification ID associated with the external call which is to be - * pulled. - */ - public void pullExternalCall(int notificationId) { - for (NotificationInfo info : mNotifications.values()) { - if (info.getNotificationId() == notificationId) { - CallSdkCompat.pullExternalCall(info.getCall()); - return; - } - } - } - - /** - * Shows a notification for a new external call. Performs a contact cache lookup to find any - * associated photo and information for the call. - */ - private void showNotifcation(final NotificationInfo info) { - // We make a call to the contact info cache to query for supplemental data to what the - // call provides. This includes the contact name and photo. - // This callback will always get called immediately and synchronously with whatever data - // it has available, and may make a subsequent call later (same thread) if it had to - // call into the contacts provider for more data. - com.android.incallui.Call incallCall = new com.android.incallui.Call(info.getCall(), - new LatencyReport(), false /* registerCallback */); - - mContactInfoCache.findInfo(incallCall, false /* isIncoming */, - new ContactInfoCache.ContactInfoCacheCallback() { - @Override - public void onContactInfoComplete(String callId, - ContactInfoCache.ContactCacheEntry entry) { - - // Ensure notification still exists as the external call could have been - // removed during async contact info lookup. - if (mNotifications.containsKey(info.getCall())) { - saveContactInfo(info, entry); - } - } - - @Override - public void onImageLoadComplete(String callId, - ContactInfoCache.ContactCacheEntry entry) { - - // Ensure notification still exists as the external call could have been - // removed during async contact info lookup. - if (mNotifications.containsKey(info.getCall())) { - savePhoto(info, entry); - } - } - - @Override - public void onContactInteractionsInfoComplete(String callId, - ContactInfoCache.ContactCacheEntry entry) { - } - }); - } - - /** - * Dismisses a notification for an external call. - */ - private void dismissNotification(Call call) { - Preconditions.checkArgument(mNotifications.containsKey(call)); - - NotificationManager notificationManager = - (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.cancel(NOTIFICATION_TAG, mNotifications.get(call).getNotificationId()); - - mNotifications.remove(call); - } - - /** - * Attempts to build a large icon to use for the notification based on the contact info and - * post the updated notification to the notification manager. - */ - private void savePhoto(NotificationInfo info, ContactInfoCache.ContactCacheEntry entry) { - Bitmap largeIcon = getLargeIconToDisplay(mContext, entry, info.getCall()); - if (largeIcon != null) { - largeIcon = getRoundedIcon(mContext, largeIcon); - } - info.setLargeIcon(largeIcon); - postNotification(info); - } - - /** - * Builds and stores the contact information the notification will display and posts the updated - * notification to the notification manager. - */ - private void saveContactInfo(NotificationInfo info, ContactInfoCache.ContactCacheEntry entry) { - info.setContentTitle(getContentTitle(mContext, mContactsPreferences, - entry, info.getCall())); - info.setPersonReference(getPersonReference(entry, info.getCall())); - postNotification(info); - } - - /** - * Rebuild an existing or show a new notification given {@link NotificationInfo}. - */ - private void postNotification(NotificationInfo info) { - Log.i(this, "postNotification : " + info.getContentTitle()); - Notification.Builder builder = new Notification.Builder(mContext); - // Set notification as ongoing since calls are long-running versus a point-in-time notice. - builder.setOngoing(true); - // Make the notification prioritized over the other normal notifications. - builder.setPriority(Notification.PRIORITY_HIGH); - // Set the content ("Ongoing call on another device") - builder.setContentText(mContext.getString(R.string.notification_external_call)); - builder.setSmallIcon(R.drawable.ic_call_white_24dp); - builder.setContentTitle(info.getContentTitle()); - builder.setLargeIcon(info.getLargeIcon()); - builder.setColor(mContext.getResources().getColor(R.color.dialer_theme_color)); - builder.addPerson(info.getPersonReference()); - - // Where the external call supports being transferred to the local device, add an action - // to the notification to initiate the call pull process. - if ((info.getCall().getDetails().getCallCapabilities() - & CallSdkCompat.Details.CAPABILITY_CAN_PULL_CALL) - == CallSdkCompat.Details.CAPABILITY_CAN_PULL_CALL) { - - Intent intent = new Intent( - NotificationBroadcastReceiver.ACTION_PULL_EXTERNAL_CALL, null, mContext, - NotificationBroadcastReceiver.class); - intent.putExtra(NotificationBroadcastReceiver.EXTRA_NOTIFICATION_ID, - info.getNotificationId()); - - builder.addAction(new Notification.Action.Builder(R.drawable.ic_call_white_24dp, - mContext.getText(R.string.notification_transfer_call), - PendingIntent.getBroadcast(mContext, 0, intent, 0)).build()); - } - - /** - * This builder is used for the notification shown when the device is locked and the user - * has set their notification settings to 'hide sensitive content' - * {@see Notification.Builder#setPublicVersion}. - */ - Notification.Builder publicBuilder = new Notification.Builder(mContext); - publicBuilder.setSmallIcon(R.drawable.ic_call_white_24dp); - publicBuilder.setColor(mContext.getResources().getColor(R.color.dialer_theme_color)); - - builder.setPublicVersion(publicBuilder.build()); - Notification notification = builder.build(); - - NotificationManager notificationManager = - (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.notify(NOTIFICATION_TAG, info.getNotificationId(), notification); - } - - /** - * Finds a large icon to display in a notification for a call. For conference calls, a - * conference call icon is used, otherwise if contact info is specified, the user's contact - * photo or avatar is used. - * - * @param context The context. - * @param contactInfo The contact cache info. - * @param call The call. - * @return The large icon to use for the notification. - */ - private @Nullable Bitmap getLargeIconToDisplay(Context context, - ContactInfoCache.ContactCacheEntry contactInfo, android.telecom.Call call) { - - Bitmap largeIcon = null; - if (call.getDetails().hasProperty(android.telecom.Call.Details.PROPERTY_CONFERENCE) && - !call.getDetails() - .hasProperty(android.telecom.Call.Details.PROPERTY_GENERIC_CONFERENCE)) { - - largeIcon = BitmapFactory.decodeResource(context.getResources(), - R.drawable.img_conference); - } - if (contactInfo.photo != null && (contactInfo.photo instanceof BitmapDrawable)) { - largeIcon = ((BitmapDrawable) contactInfo.photo).getBitmap(); - } - return largeIcon; - } - - /** - * Given a bitmap, returns a rounded version of the icon suitable for display in a notification. - * - * @param context The context. - * @param bitmap The bitmap to round. - * @return The rounded bitmap. - */ - private @Nullable Bitmap getRoundedIcon(Context context, @Nullable Bitmap bitmap) { - if (bitmap == null) { - return null; - } - final int height = (int) context.getResources().getDimension( - android.R.dimen.notification_large_icon_height); - final int width = (int) context.getResources().getDimension( - android.R.dimen.notification_large_icon_width); - return BitmapUtil.getRoundedBitmap(bitmap, width, height); - } - - /** - * Builds a notification content title for a call. If the call is a conference call, it is - * identified as such. Otherwise an attempt is made to show an associated contact name or - * phone number. - * - * @param context The context. - * @param contactsPreferences Contacts preferences, used to determine the preferred formatting - * for contact names. - * @param contactInfo The contact info which was looked up in the contact cache. - * @param call The call to generate a title for. - * @return The content title. - */ - private @Nullable String getContentTitle(Context context, - @Nullable ContactsPreferences contactsPreferences, - ContactInfoCache.ContactCacheEntry contactInfo, android.telecom.Call call) { - - if (call.getDetails().hasProperty(android.telecom.Call.Details.PROPERTY_CONFERENCE) && - !call.getDetails() - .hasProperty(android.telecom.Call.Details.PROPERTY_GENERIC_CONFERENCE)) { - - return context.getResources().getString(R.string.card_title_conf_call); - } - - String preferredName = ContactDisplayUtils.getPreferredDisplayName(contactInfo.namePrimary, - contactInfo.nameAlternative, contactsPreferences); - if (TextUtils.isEmpty(preferredName)) { - return TextUtils.isEmpty(contactInfo.number) ? null : BidiFormatter.getInstance() - .unicodeWrap(contactInfo.number, TextDirectionHeuristics.LTR); - } - return preferredName; - } - - /** - * Gets a "person reference" for a notification, used by the system to determine whether the - * notification should be allowed past notification interruption filters. - * - * @param contactInfo The contact info from cache. - * @param call The call. - * @return the person reference. - */ - private String getPersonReference(ContactInfoCache.ContactCacheEntry contactInfo, - Call call) { - - String number = TelecomCallUtil.getNumber(call); - // Query {@link Contacts#CONTENT_LOOKUP_URI} directly with work lookup key is not allowed. - // So, do not pass {@link Contacts#CONTENT_LOOKUP_URI} to NotificationManager to avoid - // NotificationManager using it. - if (contactInfo.lookupUri != null && contactInfo.userType != ContactsUtils.USER_TYPE_WORK) { - return contactInfo.lookupUri.toString(); - } else if (!TextUtils.isEmpty(number)) { - return Uri.fromParts(PhoneAccount.SCHEME_TEL, number, null).toString(); - } - return ""; - } -} diff --git a/InCallUI/src/com/android/incallui/FragmentDisplayManager.java b/InCallUI/src/com/android/incallui/FragmentDisplayManager.java deleted file mode 100644 index 045d999a0..000000000 --- a/InCallUI/src/com/android/incallui/FragmentDisplayManager.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui; - -import android.app.Fragment; - -interface FragmentDisplayManager { - public void onFragmentAttached(Fragment fragment); -} diff --git a/InCallUI/src/com/android/incallui/GlowPadAnswerFragment.java b/InCallUI/src/com/android/incallui/GlowPadAnswerFragment.java deleted file mode 100644 index 62a8e7829..000000000 --- a/InCallUI/src/com/android/incallui/GlowPadAnswerFragment.java +++ /dev/null @@ -1,155 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.os.Bundle; -import android.telecom.VideoProfile; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import com.android.dialer.R; - -public class GlowPadAnswerFragment extends AnswerFragment { - - private GlowPadWrapper mGlowpad; - - public GlowPadAnswerFragment() { - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - mGlowpad = (GlowPadWrapper) inflater.inflate(R.layout.answer_fragment, - container, false); - - Log.d(this, "Creating view for answer fragment ", this); - Log.d(this, "Created from activity", getActivity()); - mGlowpad.setAnswerFragment(this); - - return mGlowpad; - } - - @Override - public void onResume() { - super.onResume(); - mGlowpad.requestFocus(); - } - - @Override - public void onDestroyView() { - Log.d(this, "onDestroyView"); - if (mGlowpad != null) { - mGlowpad.stopPing(); - mGlowpad = null; - } - super.onDestroyView(); - } - - @Override - public void onShowAnswerUi(boolean shown) { - Log.d(this, "Show answer UI: " + shown); - if (shown) { - mGlowpad.startPing(); - } else { - mGlowpad.stopPing(); - } - } - - /** - * Sets targets on the glowpad according to target set identified by the parameter. - * - * @param targetSet Integer identifying the set of targets to use. - */ - public void showTargets(int targetSet) { - showTargets(targetSet, VideoProfile.STATE_BIDIRECTIONAL); - } - - /** - * Sets targets on the glowpad according to target set identified by the parameter. - * - * @param targetSet Integer identifying the set of targets to use. - */ - @Override - public void showTargets(int targetSet, int videoState) { - final int targetResourceId; - final int targetDescriptionsResourceId; - final int directionDescriptionsResourceId; - final int handleDrawableResourceId; - mGlowpad.setVideoState(videoState); - - switch (targetSet) { - case TARGET_SET_FOR_AUDIO_WITH_SMS: - targetResourceId = R.array.incoming_call_widget_audio_with_sms_targets; - targetDescriptionsResourceId = - R.array.incoming_call_widget_audio_with_sms_target_descriptions; - directionDescriptionsResourceId = - R.array.incoming_call_widget_audio_with_sms_direction_descriptions; - handleDrawableResourceId = R.drawable.ic_incall_audio_handle; - break; - case TARGET_SET_FOR_VIDEO_WITHOUT_SMS: - targetResourceId = R.array.incoming_call_widget_video_without_sms_targets; - targetDescriptionsResourceId = - R.array.incoming_call_widget_video_without_sms_target_descriptions; - directionDescriptionsResourceId = - R.array.incoming_call_widget_video_without_sms_direction_descriptions; - handleDrawableResourceId = R.drawable.ic_incall_video_handle; - break; - case TARGET_SET_FOR_VIDEO_WITH_SMS: - targetResourceId = R.array.incoming_call_widget_video_with_sms_targets; - targetDescriptionsResourceId = - R.array.incoming_call_widget_video_with_sms_target_descriptions; - directionDescriptionsResourceId = - R.array.incoming_call_widget_video_with_sms_direction_descriptions; - handleDrawableResourceId = R.drawable.ic_incall_video_handle; - break; - case TARGET_SET_FOR_VIDEO_ACCEPT_REJECT_REQUEST: - targetResourceId = - R.array.incoming_call_widget_video_request_targets; - targetDescriptionsResourceId = - R.array.incoming_call_widget_video_request_target_descriptions; - directionDescriptionsResourceId = R.array - .incoming_call_widget_video_request_target_direction_descriptions; - handleDrawableResourceId = R.drawable.ic_incall_video_handle; - break; - case TARGET_SET_FOR_AUDIO_WITHOUT_SMS: - default: - targetResourceId = R.array.incoming_call_widget_audio_without_sms_targets; - targetDescriptionsResourceId = - R.array.incoming_call_widget_audio_without_sms_target_descriptions; - directionDescriptionsResourceId = - R.array.incoming_call_widget_audio_without_sms_direction_descriptions; - handleDrawableResourceId = R.drawable.ic_incall_audio_handle; - break; - } - - if (targetResourceId != mGlowpad.getTargetResourceId()) { - mGlowpad.setTargetResources(targetResourceId); - mGlowpad.setTargetDescriptionsResourceId(targetDescriptionsResourceId); - mGlowpad.setDirectionDescriptionsResourceId(directionDescriptionsResourceId); - mGlowpad.setHandleDrawable(handleDrawableResourceId); - mGlowpad.reset(false); - } - } - - @Override - protected void onMessageDialogCancel() { - if (mGlowpad != null) { - mGlowpad.startPing(); - } - } -} diff --git a/InCallUI/src/com/android/incallui/GlowPadWrapper.java b/InCallUI/src/com/android/incallui/GlowPadWrapper.java deleted file mode 100644 index 342f6b4fd..000000000 --- a/InCallUI/src/com/android/incallui/GlowPadWrapper.java +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.content.Context; -import android.os.Handler; -import android.os.Message; -import android.telecom.VideoProfile; -import android.util.AttributeSet; -import android.view.View; - -import com.android.dialer.R; -import com.android.incallui.widget.multiwaveview.GlowPadView; - -/** - * - */ -public class GlowPadWrapper extends GlowPadView implements GlowPadView.OnTriggerListener { - - // Parameters for the GlowPadView "ping" animation; see triggerPing(). - private static final int PING_MESSAGE_WHAT = 101; - private static final boolean ENABLE_PING_AUTO_REPEAT = true; - private static final long PING_REPEAT_DELAY_MS = 1200; - - private final Handler mPingHandler = new Handler() { - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case PING_MESSAGE_WHAT: - triggerPing(); - break; - } - } - }; - - private AnswerFragment mAnswerFragment; - private boolean mPingEnabled = true; - private boolean mTargetTriggered = false; - private int mVideoState = VideoProfile.STATE_BIDIRECTIONAL; - - public GlowPadWrapper(Context context) { - super(context); - Log.d(this, "class created " + this + " "); - } - - public GlowPadWrapper(Context context, AttributeSet attrs) { - super(context, attrs); - Log.d(this, "class created " + this); - } - - @Override - protected void onFinishInflate() { - Log.d(this, "onFinishInflate()"); - super.onFinishInflate(); - setOnTriggerListener(this); - } - - public void startPing() { - Log.d(this, "startPing"); - mPingEnabled = true; - triggerPing(); - } - - public void stopPing() { - Log.d(this, "stopPing"); - mPingEnabled = false; - mPingHandler.removeMessages(PING_MESSAGE_WHAT); - } - - private void triggerPing() { - Log.d(this, "triggerPing(): " + mPingEnabled + " " + this); - if (mPingEnabled && !mPingHandler.hasMessages(PING_MESSAGE_WHAT)) { - ping(); - - if (ENABLE_PING_AUTO_REPEAT) { - mPingHandler.sendEmptyMessageDelayed(PING_MESSAGE_WHAT, PING_REPEAT_DELAY_MS); - } - } - } - - @Override - public void onGrabbed(View v, int handle) { - Log.d(this, "onGrabbed()"); - stopPing(); - } - - @Override - public void onReleased(View v, int handle) { - Log.d(this, "onReleased()"); - if (mTargetTriggered) { - mTargetTriggered = false; - } else { - startPing(); - } - } - - @Override - public void onTrigger(View v, int target) { - Log.d(this, "onTrigger() view=" + v + " target=" + target); - final int resId = getResourceIdForTarget(target); - if (resId == R.drawable.ic_lockscreen_answer) { - mAnswerFragment.onAnswer(VideoProfile.STATE_AUDIO_ONLY, getContext()); - mTargetTriggered = true; - } else if (resId == R.drawable.ic_lockscreen_decline) { - mAnswerFragment.onDecline(getContext()); - mTargetTriggered = true; - } else if (resId == R.drawable.ic_lockscreen_text) { - mAnswerFragment.onText(); - mTargetTriggered = true; - } else if (resId == R.drawable.ic_videocam || resId == R.drawable.ic_lockscreen_answer_video) { - mAnswerFragment.onAnswer(mVideoState, getContext()); - mTargetTriggered = true; - } else if (resId == R.drawable.ic_lockscreen_decline_video) { - mAnswerFragment.onDeclineUpgradeRequest(getContext()); - mTargetTriggered = true; - } else { - // Code should never reach here. - Log.e(this, "Trigger detected on unhandled resource. Skipping."); - } - } - - @Override - public void onGrabbedStateChange(View v, int handle) { - - } - - @Override - public void onFinishFinalAnimation() { - - } - - public void setAnswerFragment(AnswerFragment fragment) { - mAnswerFragment = fragment; - } - - /** - * Sets the video state represented by the "video" icon on the glow pad. - * - * @param videoState The new video state. - */ - public void setVideoState(int videoState) { - mVideoState = videoState; - } -} diff --git a/InCallUI/src/com/android/incallui/InCallActivity.java b/InCallUI/src/com/android/incallui/InCallActivity.java deleted file mode 100644 index eaaedff2c..000000000 --- a/InCallUI/src/com/android/incallui/InCallActivity.java +++ /dev/null @@ -1,980 +0,0 @@ -/* - * Copyright (C) 2006 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui; - -import android.app.ActionBar; -import android.app.Activity; -import android.app.ActivityManager; -import android.app.AlertDialog; -import android.app.DialogFragment; -import android.app.Fragment; -import android.app.FragmentManager; -import android.app.FragmentTransaction; -import android.content.Context; -import android.content.DialogInterface; -import android.content.DialogInterface.OnCancelListener; -import android.content.DialogInterface.OnClickListener; -import android.content.Intent; -import android.content.res.Configuration; -import android.graphics.Point; -import android.hardware.SensorManager; -import android.os.Bundle; -import android.os.Trace; -import android.telecom.DisconnectCause; -import android.telecom.PhoneAccountHandle; -import android.text.TextUtils; -import android.view.KeyEvent; -import android.view.MenuItem; -import android.view.MotionEvent; -import android.view.OrientationEventListener; -import android.view.Surface; -import android.view.View; -import android.view.View.OnTouchListener; -import android.view.Window; -import android.view.WindowManager; -import android.view.accessibility.AccessibilityEvent; -import android.view.animation.Animation; -import android.view.animation.AnimationUtils; - -import com.android.contacts.common.activity.TransactionSafeActivity; -import com.android.contacts.common.compat.CompatUtils; -import com.android.contacts.common.interactions.TouchPointManager; -import com.android.contacts.common.widget.SelectPhoneAccountDialogFragment; -import com.android.contacts.common.widget.SelectPhoneAccountDialogFragment.SelectPhoneAccountListener; -import com.android.dialer.R; -import com.android.dialer.logging.Logger; -import com.android.dialer.logging.ScreenEvent; -import com.android.incallui.Call.State; -import com.android.incallui.util.AccessibilityUtil; -import com.android.phone.common.animation.AnimUtils; -import com.android.phone.common.animation.AnimationListenerAdapter; - -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; - -/** - * Main activity that the user interacts with while in a live call. - */ -public class InCallActivity extends TransactionSafeActivity implements FragmentDisplayManager { - - public static final String TAG = InCallActivity.class.getSimpleName(); - - public static final String SHOW_DIALPAD_EXTRA = "InCallActivity.show_dialpad"; - public static final String DIALPAD_TEXT_EXTRA = "InCallActivity.dialpad_text"; - public static final String NEW_OUTGOING_CALL_EXTRA = "InCallActivity.new_outgoing_call"; - public static final String FOR_FULL_SCREEN_INTENT = "InCallActivity.for_full_screen_intent"; - - private static final String TAG_DIALPAD_FRAGMENT = "tag_dialpad_fragment"; - private static final String TAG_CONFERENCE_FRAGMENT = "tag_conference_manager_fragment"; - private static final String TAG_CALLCARD_FRAGMENT = "tag_callcard_fragment"; - private static final String TAG_ANSWER_FRAGMENT = "tag_answer_fragment"; - private static final String TAG_SELECT_ACCT_FRAGMENT = "tag_select_acct_fragment"; - - private static final int DIALPAD_REQUEST_NONE = 1; - private static final int DIALPAD_REQUEST_SHOW = 2; - private static final int DIALPAD_REQUEST_HIDE = 3; - - /** - * This is used to relaunch the activity if resizing beyond which it needs to load different - * layout file. - */ - private static final int SCREEN_HEIGHT_RESIZE_THRESHOLD = 500; - - private CallButtonFragment mCallButtonFragment; - private CallCardFragment mCallCardFragment; - private AnswerFragment mAnswerFragment; - private DialpadFragment mDialpadFragment; - private ConferenceManagerFragment mConferenceManagerFragment; - private FragmentManager mChildFragmentManager; - - private AlertDialog mDialog; - private InCallOrientationEventListener mInCallOrientationEventListener; - - /** - * Used to indicate whether the dialpad should be hidden or shown {@link #onResume}. - * {@code #DIALPAD_REQUEST_SHOW} indicates that the dialpad should be shown. - * {@code #DIALPAD_REQUEST_HIDE} indicates that the dialpad should be hidden. - * {@code #DIALPAD_REQUEST_NONE} indicates no change should be made to dialpad visibility. - */ - private int mShowDialpadRequest = DIALPAD_REQUEST_NONE; - - /** - * Use to determine if the dialpad should be animated on show. - */ - private boolean mAnimateDialpadOnShow; - - /** - * Use to determine the DTMF Text which should be pre-populated in the dialpad. - */ - private String mDtmfText; - - /** - * Use to pass parameters for showing the PostCharDialog to {@link #onResume} - */ - private boolean mShowPostCharWaitDialogOnResume; - private String mShowPostCharWaitDialogCallId; - private String mShowPostCharWaitDialogChars; - - private boolean mIsLandscape; - private Animation mSlideIn; - private Animation mSlideOut; - private boolean mDismissKeyguard = false; - - AnimationListenerAdapter mSlideOutListener = new AnimationListenerAdapter() { - @Override - public void onAnimationEnd(Animation animation) { - showFragment(TAG_DIALPAD_FRAGMENT, false, true); - } - }; - - private OnTouchListener mDispatchTouchEventListener; - - private SelectPhoneAccountListener mSelectAcctListener = new SelectPhoneAccountListener() { - @Override - public void onPhoneAccountSelected(PhoneAccountHandle selectedAccountHandle, - boolean setDefault) { - InCallPresenter.getInstance().handleAccountSelection(selectedAccountHandle, - setDefault); - } - - @Override - public void onDialogDismissed() { - InCallPresenter.getInstance().cancelAccountSelection(); - } - }; - - @Override - protected void onCreate(Bundle icicle) { - Log.d(this, "onCreate()... this = " + this); - - super.onCreate(icicle); - - // set this flag so this activity will stay in front of the keyguard - // Have the WindowManager filter out touch events that are "too fat". - int flags = WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED - | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON - | WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES; - - getWindow().addFlags(flags); - - // Setup action bar for the conference call manager. - requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY); - ActionBar actionBar = getActionBar(); - if (actionBar != null) { - actionBar.setDisplayHomeAsUpEnabled(true); - actionBar.setDisplayShowTitleEnabled(true); - actionBar.hide(); - } - - // TODO(klp): Do we need to add this back when prox sensor is not available? - // lp.inputFeatures |= WindowManager.LayoutParams.INPUT_FEATURE_DISABLE_USER_ACTIVITY; - - setContentView(R.layout.incall_screen); - - internalResolveIntent(getIntent()); - - mIsLandscape = getResources().getConfiguration().orientation == - Configuration.ORIENTATION_LANDSCAPE; - - final boolean isRtl = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == - View.LAYOUT_DIRECTION_RTL; - - if (mIsLandscape) { - mSlideIn = AnimationUtils.loadAnimation(this, - isRtl ? R.anim.dialpad_slide_in_left : R.anim.dialpad_slide_in_right); - mSlideOut = AnimationUtils.loadAnimation(this, - isRtl ? 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); - - mSlideOut.setAnimationListener(mSlideOutListener); - - // If the dialpad fragment already exists, retrieve it. This is important when rotating as - // we will not be able to hide or show the dialpad after the rotation otherwise. - Fragment existingFragment = - getFragmentManager().findFragmentByTag(DialpadFragment.class.getName()); - if (existingFragment != null) { - mDialpadFragment = (DialpadFragment) existingFragment; - } - - if (icicle != null) { - // If the dialpad was shown before, set variables indicating it should be shown and - // populated with the previous DTMF text. The dialpad is actually shown and populated - // in onResume() to ensure the hosting CallCardFragment has been inflated and is ready - // to receive it. - if (icicle.containsKey(SHOW_DIALPAD_EXTRA)) { - boolean showDialpad = icicle.getBoolean(SHOW_DIALPAD_EXTRA); - mShowDialpadRequest = showDialpad ? DIALPAD_REQUEST_SHOW : DIALPAD_REQUEST_HIDE; - mAnimateDialpadOnShow = false; - } - mDtmfText = icicle.getString(DIALPAD_TEXT_EXTRA); - - SelectPhoneAccountDialogFragment dialogFragment = (SelectPhoneAccountDialogFragment) - getFragmentManager().findFragmentByTag(TAG_SELECT_ACCT_FRAGMENT); - if (dialogFragment != null) { - dialogFragment.setListener(mSelectAcctListener); - } - } - mInCallOrientationEventListener = new InCallOrientationEventListener(this); - - Log.d(this, "onCreate(): exit"); - } - - @Override - protected void onSaveInstanceState(Bundle out) { - // TODO: The dialpad fragment should handle this as part of its own state - out.putBoolean(SHOW_DIALPAD_EXTRA, - mCallButtonFragment != null && mCallButtonFragment.isDialpadVisible()); - if (mDialpadFragment != null) { - out.putString(DIALPAD_TEXT_EXTRA, mDialpadFragment.getDtmfText()); - } - super.onSaveInstanceState(out); - } - - @Override - protected void onStart() { - Log.d(this, "onStart()..."); - super.onStart(); - - // setting activity should be last thing in setup process - InCallPresenter.getInstance().setActivity(this); - enableInCallOrientationEventListener(getRequestedOrientation() == - InCallOrientationEventListener.FULL_SENSOR_SCREEN_ORIENTATION); - - InCallPresenter.getInstance().onActivityStarted(); - } - - @Override - protected void onResume() { - Log.i(this, "onResume()..."); - super.onResume(); - - InCallPresenter.getInstance().setThemeColors(); - InCallPresenter.getInstance().onUiShowing(true); - - // Clear fullscreen state onResume; the stored value may not match reality. - InCallPresenter.getInstance().clearFullscreen(); - - // If there is a pending request to show or hide the dialpad, handle that now. - if (mShowDialpadRequest != DIALPAD_REQUEST_NONE) { - if (mShowDialpadRequest == DIALPAD_REQUEST_SHOW) { - // Exit fullscreen so that the user has access to the dialpad hide/show button and - // can hide the dialpad. Important when showing the dialpad from within dialer. - InCallPresenter.getInstance().setFullScreen(false, true /* force */); - - mCallButtonFragment.displayDialpad(true /* show */, - mAnimateDialpadOnShow /* animate */); - mAnimateDialpadOnShow = false; - - if (mDialpadFragment != null) { - mDialpadFragment.setDtmfText(mDtmfText); - mDtmfText = null; - } - } else { - Log.v(this, "onResume : force hide dialpad"); - if (mDialpadFragment != null) { - mCallButtonFragment.displayDialpad(false /* show */, false /* animate */); - } - } - mShowDialpadRequest = DIALPAD_REQUEST_NONE; - } - - if (mShowPostCharWaitDialogOnResume) { - showPostCharWaitDialog(mShowPostCharWaitDialogCallId, mShowPostCharWaitDialogChars); - } - - CallList.getInstance().onInCallUiShown( - getIntent().getBooleanExtra(FOR_FULL_SCREEN_INTENT, false)); - } - - // onPause is guaranteed to be called when the InCallActivity goes - // in the background. - @Override - protected void onPause() { - Log.d(this, "onPause()..."); - if (mDialpadFragment != null) { - mDialpadFragment.onDialerKeyUp(null); - } - - InCallPresenter.getInstance().onUiShowing(false); - if (isFinishing()) { - InCallPresenter.getInstance().unsetActivity(this); - } - super.onPause(); - } - - @Override - protected void onStop() { - Log.d(this, "onStop()..."); - enableInCallOrientationEventListener(false); - InCallPresenter.getInstance().updateIsChangingConfigurations(); - InCallPresenter.getInstance().onActivityStopped(); - super.onStop(); - } - - @Override - protected void onDestroy() { - Log.d(this, "onDestroy()... this = " + this); - InCallPresenter.getInstance().unsetActivity(this); - InCallPresenter.getInstance().updateIsChangingConfigurations(); - super.onDestroy(); - } - - /** - * When fragments have a parent fragment, onAttachFragment is not called on the parent - * activity. To fix this, register our own callback instead that is always called for - * all fragments. - * - * @see {@link BaseFragment#onAttach(Activity)} - */ - @Override - public void onFragmentAttached(Fragment fragment) { - if (fragment instanceof DialpadFragment) { - mDialpadFragment = (DialpadFragment) fragment; - } else if (fragment instanceof AnswerFragment) { - mAnswerFragment = (AnswerFragment) fragment; - } else if (fragment instanceof CallCardFragment) { - mCallCardFragment = (CallCardFragment) fragment; - mChildFragmentManager = mCallCardFragment.getChildFragmentManager(); - } else if (fragment instanceof ConferenceManagerFragment) { - mConferenceManagerFragment = (ConferenceManagerFragment) fragment; - } else if (fragment instanceof CallButtonFragment) { - mCallButtonFragment = (CallButtonFragment) fragment; - } - } - - @Override - public void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - Configuration oldConfig = getResources().getConfiguration(); - Log.v(this, String.format( - "incallui config changed, screen size: w%ddp x h%ddp old:w%ddp x h%ddp", - newConfig.screenWidthDp, newConfig.screenHeightDp, - oldConfig.screenWidthDp, oldConfig.screenHeightDp)); - // Recreate this activity if height is changing beyond the threshold to load different - // layout file. - if (oldConfig.screenHeightDp < SCREEN_HEIGHT_RESIZE_THRESHOLD && - newConfig.screenHeightDp > SCREEN_HEIGHT_RESIZE_THRESHOLD || - oldConfig.screenHeightDp > SCREEN_HEIGHT_RESIZE_THRESHOLD && - newConfig.screenHeightDp < SCREEN_HEIGHT_RESIZE_THRESHOLD) { - Log.i(this, String.format( - "Recreate activity due to resize beyond threshold: %d dp", - SCREEN_HEIGHT_RESIZE_THRESHOLD)); - recreate(); - } - } - - /** - * Returns true when the Activity is currently visible. - */ - /* package */ boolean isVisible() { - return isSafeToCommitTransactions(); - } - - private boolean hasPendingDialogs() { - return mDialog != null || (mAnswerFragment != null && mAnswerFragment.hasPendingDialogs()); - } - - @Override - public void finish() { - Log.i(this, "finish(). Dialog showing: " + (mDialog != null)); - - // skip finish if we are still showing a dialog. - if (!hasPendingDialogs()) { - super.finish(); - } - } - - @Override - protected void onNewIntent(Intent intent) { - Log.d(this, "onNewIntent: intent = " + intent); - - // We're being re-launched with a new Intent. Since it's possible for a - // single InCallActivity instance to persist indefinitely (even if we - // finish() ourselves), this sequence can potentially happen any time - // the InCallActivity needs to be displayed. - - // Stash away the new intent so that we can get it in the future - // by calling getIntent(). (Otherwise getIntent() will return the - // original Intent from when we first got created!) - setIntent(intent); - - // Activities are always paused before receiving a new intent, so - // we can count on our onResume() method being called next. - - // Just like in onCreate(), handle the intent. - internalResolveIntent(intent); - } - - @Override - public void onBackPressed() { - Log.i(this, "onBackPressed"); - - // BACK is also used to exit out of any "special modes" of the - // in-call UI: - if (!isVisible()) { - return; - } - - if ((mConferenceManagerFragment == null || !mConferenceManagerFragment.isVisible()) - && (mCallCardFragment == null || !mCallCardFragment.isVisible())) { - return; - } - - if (mDialpadFragment != null && mDialpadFragment.isVisible()) { - mCallButtonFragment.displayDialpad(false /* show */, true /* animate */); - return; - } else if (mConferenceManagerFragment != null && mConferenceManagerFragment.isVisible()) { - showConferenceFragment(false); - return; - } - - // Always disable the Back key while an incoming call is ringing - final Call call = CallList.getInstance().getIncomingCall(); - if (call != null) { - Log.i(this, "Consume Back press for an incoming call"); - return; - } - - // Nothing special to do. Fall back to the default behavior. - super.onBackPressed(); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - final int itemId = item.getItemId(); - if (itemId == android.R.id.home) { - onBackPressed(); - return true; - } - return super.onOptionsItemSelected(item); - } - - @Override - public boolean onKeyUp(int keyCode, KeyEvent event) { - // push input to the dialer. - if (mDialpadFragment != null && (mDialpadFragment.isVisible()) && - (mDialpadFragment.onDialerKeyUp(event))) { - return true; - } else if (keyCode == KeyEvent.KEYCODE_CALL) { - // Always consume CALL to be sure the PhoneWindow won't do anything with it - return true; - } - return super.onKeyUp(keyCode, event); - } - - @Override - public boolean dispatchTouchEvent(MotionEvent ev) { - if (mDispatchTouchEventListener != null) { - boolean handled = mDispatchTouchEventListener.onTouch(null, ev); - if (handled) { - return true; - } - } - return super.dispatchTouchEvent(ev); - } - - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - switch (keyCode) { - case KeyEvent.KEYCODE_CALL: - boolean handled = InCallPresenter.getInstance().handleCallKey(); - if (!handled) { - Log.w(this, "InCallActivity should always handle KEYCODE_CALL in onKeyDown"); - } - // Always consume CALL to be sure the PhoneWindow won't do anything with it - return true; - - // Note there's no KeyEvent.KEYCODE_ENDCALL case here. - // The standard system-wide handling of the ENDCALL key - // (see PhoneWindowManager's handling of KEYCODE_ENDCALL) - // already implements exactly what the UI spec wants, - // namely (1) "hang up" if there's a current active call, - // or (2) "don't answer" if there's a current ringing call. - - case KeyEvent.KEYCODE_CAMERA: - // Disable the CAMERA button while in-call since it's too - // easy to press accidentally. - return true; - - case KeyEvent.KEYCODE_VOLUME_UP: - case KeyEvent.KEYCODE_VOLUME_DOWN: - case KeyEvent.KEYCODE_VOLUME_MUTE: - // Ringer silencing handled by PhoneWindowManager. - break; - - case KeyEvent.KEYCODE_MUTE: - // toggle mute - TelecomAdapter.getInstance().mute(!AudioModeProvider.getInstance().getMute()); - return true; - - // Various testing/debugging features, enabled ONLY when VERBOSE == true. - case KeyEvent.KEYCODE_SLASH: - if (Log.VERBOSE) { - Log.v(this, "----------- InCallActivity View dump --------------"); - // Dump starting from the top-level view of the entire activity: - Window w = this.getWindow(); - View decorView = w.getDecorView(); - Log.d(this, "View dump:" + decorView); - return true; - } - break; - case KeyEvent.KEYCODE_EQUALS: - // TODO: Dump phone state? - break; - } - - if (event.getRepeatCount() == 0 && handleDialerKeyDown(keyCode, event)) { - return true; - } - return super.onKeyDown(keyCode, event); - } - - private boolean handleDialerKeyDown(int keyCode, KeyEvent event) { - Log.v(this, "handleDialerKeyDown: keyCode " + keyCode + ", event " + event + "..."); - - // As soon as the user starts typing valid dialable keys on the - // keyboard (presumably to type DTMF tones) we start passing the - // key events to the DTMFDialer's onDialerKeyDown. - if (mDialpadFragment != null && mDialpadFragment.isVisible()) { - return mDialpadFragment.onDialerKeyDown(event); - } - - return false; - } - - public CallButtonFragment getCallButtonFragment() { - return mCallButtonFragment; - } - - public CallCardFragment getCallCardFragment() { - return mCallCardFragment; - } - - public AnswerFragment getAnswerFragment() { - return mAnswerFragment; - } - - private void internalResolveIntent(Intent intent) { - final String action = intent.getAction(); - if (action.equals(Intent.ACTION_MAIN)) { - // This action is the normal way to bring up the in-call UI. - // - // But we do check here for one extra that can come along with the - // ACTION_MAIN intent: - - if (intent.hasExtra(SHOW_DIALPAD_EXTRA)) { - // SHOW_DIALPAD_EXTRA can be used here to specify whether the DTMF - // dialpad should be initially visible. If the extra isn't - // present at all, we just leave the dialpad in its previous state. - - final boolean showDialpad = intent.getBooleanExtra(SHOW_DIALPAD_EXTRA, false); - Log.d(this, "- internalResolveIntent: SHOW_DIALPAD_EXTRA: " + showDialpad); - - relaunchedFromDialer(showDialpad); - } - - boolean newOutgoingCall = false; - if (intent.getBooleanExtra(NEW_OUTGOING_CALL_EXTRA, false)) { - intent.removeExtra(NEW_OUTGOING_CALL_EXTRA); - Call call = CallList.getInstance().getOutgoingCall(); - if (call == null) { - call = CallList.getInstance().getPendingOutgoingCall(); - } - - Bundle extras = null; - if (call != null) { - extras = call.getTelecomCall().getDetails().getIntentExtras(); - } - if (extras == null) { - // Initialize the extras bundle to avoid NPE - extras = new Bundle(); - } - - Point touchPoint = null; - if (TouchPointManager.getInstance().hasValidPoint()) { - // Use the most immediate touch point in the InCallUi if available - touchPoint = TouchPointManager.getInstance().getPoint(); - } else { - // Otherwise retrieve the touch point from the call intent - if (call != null) { - touchPoint = (Point) extras.getParcelable(TouchPointManager.TOUCH_POINT); - } - } - - // Start animation for new outgoing call - CircularRevealFragment.startCircularReveal(getFragmentManager(), touchPoint, - InCallPresenter.getInstance()); - - // InCallActivity is responsible for disconnecting a new outgoing call if there - // is no way of making it (i.e. no valid call capable accounts). - // If the version is not MSIM compatible, then ignore this code. - if (CompatUtils.isMSIMCompatible() - && InCallPresenter.isCallWithNoValidAccounts(call)) { - TelecomAdapter.getInstance().disconnectCall(call.getId()); - } - - dismissKeyguard(true); - newOutgoingCall = true; - } - - Call pendingAccountSelectionCall = CallList.getInstance().getWaitingForAccountCall(); - if (pendingAccountSelectionCall != null) { - showCallCardFragment(false); - Bundle extras = - pendingAccountSelectionCall.getTelecomCall().getDetails().getIntentExtras(); - - final List phoneAccountHandles; - if (extras != null) { - phoneAccountHandles = extras.getParcelableArrayList( - android.telecom.Call.AVAILABLE_PHONE_ACCOUNTS); - } else { - phoneAccountHandles = new ArrayList<>(); - } - - DialogFragment dialogFragment = SelectPhoneAccountDialogFragment.newInstance( - R.string.select_phone_account_for_calls, true, phoneAccountHandles, - mSelectAcctListener); - dialogFragment.show(getFragmentManager(), TAG_SELECT_ACCT_FRAGMENT); - } else if (!newOutgoingCall) { - showCallCardFragment(true); - } - return; - } - } - - /** - * When relaunching from the dialer app, {@code showDialpad} indicates whether the dialpad - * should be shown on launch. - * - * @param showDialpad {@code true} to indicate the dialpad should be shown on launch, and - * {@code false} to indicate no change should be made to the - * dialpad visibility. - */ - private void relaunchedFromDialer(boolean showDialpad) { - mShowDialpadRequest = showDialpad ? DIALPAD_REQUEST_SHOW : DIALPAD_REQUEST_NONE; - mAnimateDialpadOnShow = true; - - if (mShowDialpadRequest == DIALPAD_REQUEST_SHOW) { - // If there's only one line in use, AND it's on hold, then we're sure the user - // wants to use the dialpad toward the exact line, so un-hold the holding line. - final Call call = CallList.getInstance().getActiveOrBackgroundCall(); - if (call != null && call.getState() == State.ONHOLD) { - TelecomAdapter.getInstance().unholdCall(call.getId()); - } - } - } - - public void dismissKeyguard(boolean dismiss) { - if (mDismissKeyguard == dismiss) { - return; - } - mDismissKeyguard = dismiss; - if (dismiss) { - getWindow().addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD); - } else { - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD); - } - } - - private void showFragment(String tag, boolean show, boolean executeImmediately) { - Trace.beginSection("showFragment - " + tag); - final FragmentManager fm = getFragmentManagerForTag(tag); - - if (fm == null) { - Log.w(TAG, "Fragment manager is null for : " + tag); - return; - } - - Fragment fragment = fm.findFragmentByTag(tag); - if (!show && fragment == null) { - // Nothing to show, so bail early. - return; - } - - final FragmentTransaction transaction = fm.beginTransaction(); - if (show) { - if (fragment == null) { - fragment = createNewFragmentForTag(tag); - transaction.add(getContainerIdForFragment(tag), fragment, tag); - } else { - transaction.show(fragment); - } - Logger.logScreenView(getScreenTypeForTag(tag), this); - } else { - transaction.hide(fragment); - } - - transaction.commitAllowingStateLoss(); - if (executeImmediately) { - fm.executePendingTransactions(); - } - Trace.endSection(); - } - - private Fragment createNewFragmentForTag(String tag) { - if (TAG_DIALPAD_FRAGMENT.equals(tag)) { - mDialpadFragment = new DialpadFragment(); - return mDialpadFragment; - } else if (TAG_ANSWER_FRAGMENT.equals(tag)) { - if (AccessibilityUtil.isTalkBackEnabled(this)) { - mAnswerFragment = new AccessibleAnswerFragment(); - } else { - mAnswerFragment = new GlowPadAnswerFragment(); - } - return mAnswerFragment; - } else if (TAG_CONFERENCE_FRAGMENT.equals(tag)) { - mConferenceManagerFragment = new ConferenceManagerFragment(); - return mConferenceManagerFragment; - } else if (TAG_CALLCARD_FRAGMENT.equals(tag)) { - mCallCardFragment = new CallCardFragment(); - return mCallCardFragment; - } - throw new IllegalStateException("Unexpected fragment: " + tag); - } - - private FragmentManager getFragmentManagerForTag(String tag) { - if (TAG_DIALPAD_FRAGMENT.equals(tag)) { - return mChildFragmentManager; - } else if (TAG_ANSWER_FRAGMENT.equals(tag)) { - return mChildFragmentManager; - } else if (TAG_CONFERENCE_FRAGMENT.equals(tag)) { - return getFragmentManager(); - } else if (TAG_CALLCARD_FRAGMENT.equals(tag)) { - return getFragmentManager(); - } - throw new IllegalStateException("Unexpected fragment: " + tag); - } - - private int getScreenTypeForTag(String tag) { - switch (tag) { - case TAG_DIALPAD_FRAGMENT: - return ScreenEvent.INCALL_DIALPAD; - case TAG_CALLCARD_FRAGMENT: - return ScreenEvent.INCALL; - case TAG_CONFERENCE_FRAGMENT: - return ScreenEvent.CONFERENCE_MANAGEMENT; - case TAG_ANSWER_FRAGMENT: - return ScreenEvent.INCOMING_CALL; - default: - return ScreenEvent.UNKNOWN; - } - } - - private int getContainerIdForFragment(String tag) { - if (TAG_DIALPAD_FRAGMENT.equals(tag)) { - return R.id.answer_and_dialpad_container; - } else if (TAG_ANSWER_FRAGMENT.equals(tag)) { - return R.id.answer_and_dialpad_container; - } else if (TAG_CONFERENCE_FRAGMENT.equals(tag)) { - return R.id.main; - } else if (TAG_CALLCARD_FRAGMENT.equals(tag)) { - return R.id.main; - } - throw new IllegalStateException("Unexpected fragment: " + tag); - } - - /** - * @return {@code true} while the visibility of the dialpad has actually changed. - */ - public boolean showDialpadFragment(boolean show, boolean animate) { - // If the dialpad is already visible, don't animate in. If it's gone, don't animate out. - if ((show && isDialpadVisible()) || (!show && !isDialpadVisible())) { - return false; - } - // We don't do a FragmentTransaction on the hide case because it will be dealt with when - // the listener is fired after an animation finishes. - if (!animate) { - showFragment(TAG_DIALPAD_FRAGMENT, show, true); - } else { - if (show) { - showFragment(TAG_DIALPAD_FRAGMENT, true, true); - mDialpadFragment.animateShowDialpad(); - } - mDialpadFragment.getView().startAnimation(show ? mSlideIn : mSlideOut); - } - // Note: onDialpadVisibilityChange is called here to ensure that the dialpad FAB - // repositions itself. - mCallCardFragment.onDialpadVisibilityChange(show); - - final ProximitySensor sensor = InCallPresenter.getInstance().getProximitySensor(); - if (sensor != null) { - sensor.onDialpadVisible(show); - } - return true; - } - - public boolean isDialpadVisible() { - return mDialpadFragment != null && mDialpadFragment.isVisible(); - } - - public void showCallCardFragment(boolean show) { - showFragment(TAG_CALLCARD_FRAGMENT, show, true); - } - - /** - * Hides or shows the conference manager fragment. - * - * @param show {@code true} if the conference manager should be shown, {@code false} if it - * should be hidden. - */ - public void showConferenceFragment(boolean show) { - showFragment(TAG_CONFERENCE_FRAGMENT, show, true); - mConferenceManagerFragment.onVisibilityChanged(show); - - // Need to hide the call card fragment to ensure that accessibility service does not try to - // give focus to the call card when the conference manager is visible. - mCallCardFragment.getView().setVisibility(show ? View.GONE : View.VISIBLE); - } - - public void showAnswerFragment(boolean show) { - // CallCardFragment is the parent fragment of AnswerFragment. - // Must create the CallCardFragment first before creating - // AnswerFragment if CallCardFragment is null. - if (show && getCallCardFragment() == null) { - showCallCardFragment(true); - } - showFragment(TAG_ANSWER_FRAGMENT, show, true); - } - - public void showPostCharWaitDialog(String callId, String chars) { - if (isVisible()) { - final PostCharDialogFragment fragment = new PostCharDialogFragment(callId, chars); - fragment.show(getFragmentManager(), "postCharWait"); - - mShowPostCharWaitDialogOnResume = false; - mShowPostCharWaitDialogCallId = null; - mShowPostCharWaitDialogChars = null; - } else { - mShowPostCharWaitDialogOnResume = true; - mShowPostCharWaitDialogCallId = callId; - mShowPostCharWaitDialogChars = chars; - } - } - - @Override - public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { - if (mCallCardFragment != null) { - mCallCardFragment.dispatchPopulateAccessibilityEvent(event); - } - return super.dispatchPopulateAccessibilityEvent(event); - } - - public void maybeShowErrorDialogOnDisconnect(DisconnectCause disconnectCause) { - Log.d(this, "maybeShowErrorDialogOnDisconnect"); - - if (!isFinishing() && !TextUtils.isEmpty(disconnectCause.getDescription()) - && (disconnectCause.getCode() == DisconnectCause.ERROR || - disconnectCause.getCode() == DisconnectCause.RESTRICTED)) { - showErrorDialog(disconnectCause.getDescription()); - } - } - - public void dismissPendingDialogs() { - if (mDialog != null) { - mDialog.dismiss(); - mDialog = null; - } - if (mAnswerFragment != null) { - mAnswerFragment.dismissPendingDialogs(); - } - - SelectPhoneAccountDialogFragment dialogFragment = (SelectPhoneAccountDialogFragment) - getFragmentManager().findFragmentByTag(TAG_SELECT_ACCT_FRAGMENT); - if (dialogFragment != null) { - dialogFragment.dismiss(); - } - } - - /** - * Utility function to bring up a generic "error" dialog. - */ - private void showErrorDialog(CharSequence msg) { - Log.i(this, "Show Dialog: " + msg); - - dismissPendingDialogs(); - - mDialog = new AlertDialog.Builder(this) - .setMessage(msg) - .setPositiveButton(android.R.string.ok, new OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - onDialogDismissed(); - } - }) - .setOnCancelListener(new OnCancelListener() { - @Override - public void onCancel(DialogInterface dialog) { - onDialogDismissed(); - } - }) - .create(); - - mDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); - mDialog.show(); - } - - private void onDialogDismissed() { - mDialog = null; - CallList.getInstance().onErrorDialogDismissed(); - InCallPresenter.getInstance().onDismissDialog(); - } - - public void setExcludeFromRecents(boolean exclude) { - ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); - List tasks = am.getAppTasks(); - int taskId = getTaskId(); - for (int i = 0; i < tasks.size(); i++) { - ActivityManager.AppTask task = tasks.get(i); - if (task.getTaskInfo().id == taskId) { - try { - task.setExcludeFromRecents(exclude); - } catch (RuntimeException e) { - Log.e(TAG, "RuntimeException when excluding task from recents.", e); - } - } - } - } - - - public OnTouchListener getDispatchTouchEventListener() { - return mDispatchTouchEventListener; - } - - public void setDispatchTouchEventListener(OnTouchListener mDispatchTouchEventListener) { - this.mDispatchTouchEventListener = mDispatchTouchEventListener; - } - - /** - * Enables the OrientationEventListener if enable flag is true. Disables it if enable is - * false - * @param enable true or false. - */ - public void enableInCallOrientationEventListener(boolean enable) { - if (enable) { - mInCallOrientationEventListener.enable(enable); - } else { - mInCallOrientationEventListener.disable(); - } - } -} diff --git a/InCallUI/src/com/android/incallui/InCallAnimationUtils.java b/InCallUI/src/com/android/incallui/InCallAnimationUtils.java deleted file mode 100644 index 44bb369e6..000000000 --- a/InCallUI/src/com/android/incallui/InCallAnimationUtils.java +++ /dev/null @@ -1,184 +0,0 @@ -/* - * 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.incallui; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ObjectAnimator; -import android.graphics.Canvas; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.LayerDrawable; -import android.view.ViewPropertyAnimator; -import android.widget.ImageView; - -/** - * Utilities for Animation. - */ -public class InCallAnimationUtils { - private static final String LOG_TAG = InCallAnimationUtils.class.getSimpleName(); - /** - * Turn on when you're interested in fading animation. Intentionally untied from other debug - * settings. - */ - private static final boolean FADE_DBG = false; - - /** - * Duration for animations in msec, which can be used with - * {@link ViewPropertyAnimator#setDuration(long)} for example. - */ - public static final int ANIMATION_DURATION = 250; - - private InCallAnimationUtils() { - } - - /** - * Drawable achieving cross-fade, just like TransitionDrawable. We can have - * call-backs via animator object (see also {@link CrossFadeDrawable#getAnimator()}). - */ - private static class CrossFadeDrawable extends LayerDrawable { - private final ObjectAnimator mAnimator; - - public CrossFadeDrawable(Drawable[] layers) { - super(layers); - mAnimator = ObjectAnimator.ofInt(this, "crossFadeAlpha", 0xff, 0); - } - - private int mCrossFadeAlpha; - - /** - * This will be used from ObjectAnimator. - * Note: this method is protected by proguard.flags so that it won't be removed - * automatically. - */ - @SuppressWarnings("unused") - public void setCrossFadeAlpha(int alpha) { - mCrossFadeAlpha = alpha; - invalidateSelf(); - } - - public ObjectAnimator getAnimator() { - return mAnimator; - } - - @Override - public void draw(Canvas canvas) { - Drawable first = getDrawable(0); - Drawable second = getDrawable(1); - - if (mCrossFadeAlpha > 0) { - first.setAlpha(mCrossFadeAlpha); - first.draw(canvas); - first.setAlpha(255); - } - - if (mCrossFadeAlpha < 0xff) { - second.setAlpha(0xff - mCrossFadeAlpha); - second.draw(canvas); - second.setAlpha(0xff); - } - } - } - - private static CrossFadeDrawable newCrossFadeDrawable(Drawable first, Drawable second) { - Drawable[] layers = new Drawable[2]; - layers[0] = first; - layers[1] = second; - return new CrossFadeDrawable(layers); - } - - /** - * Starts cross-fade animation using TransitionDrawable. Nothing will happen if "from" and "to" - * are the same. - */ - public static void startCrossFade( - final ImageView imageView, final Drawable from, final Drawable to) { - // We skip the cross-fade when those two Drawables are equal, or they are BitmapDrawables - // pointing to the same Bitmap. - final boolean drawableIsEqual = (from != null && to != null && from.equals(to)); - final boolean hasFromImage = ((from instanceof BitmapDrawable) && - ((BitmapDrawable) from).getBitmap() != null); - final boolean hasToImage = ((to instanceof BitmapDrawable) && - ((BitmapDrawable) to).getBitmap() != null); - final boolean areSameImage = drawableIsEqual || (hasFromImage && hasToImage && - ((BitmapDrawable) from).getBitmap().equals(((BitmapDrawable) to).getBitmap())); - - if (!areSameImage) { - if (FADE_DBG) { - log("Start cross-fade animation for " + imageView - + "(" + Integer.toHexString(from.hashCode()) + " -> " - + Integer.toHexString(to.hashCode()) + ")"); - } - - CrossFadeDrawable crossFadeDrawable = newCrossFadeDrawable(from, to); - ObjectAnimator animator = crossFadeDrawable.getAnimator(); - imageView.setImageDrawable(crossFadeDrawable); - animator.setDuration(ANIMATION_DURATION); - animator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - if (FADE_DBG) { - log("cross-fade animation start (" - + Integer.toHexString(from.hashCode()) + " -> " - + Integer.toHexString(to.hashCode()) + ")"); - } - } - - @Override - public void onAnimationEnd(Animator animation) { - if (FADE_DBG) { - log("cross-fade animation ended (" - + Integer.toHexString(from.hashCode()) + " -> " - + Integer.toHexString(to.hashCode()) + ")"); - } - animation.removeAllListeners(); - // Workaround for issue 6300562; this will force the drawable to the - // resultant one regardless of animation glitch. - imageView.setImageDrawable(to); - } - }); - animator.start(); - - /* We could use TransitionDrawable here, but it may cause some weird animation in - * some corner cases. See issue 6300562 - * TODO: decide which to be used in the long run. TransitionDrawable is old but system - * one. Ours uses new animation framework and thus have callback (great for testing), - * while no framework support for the exact class. - - Drawable[] layers = new Drawable[2]; - layers[0] = from; - layers[1] = to; - TransitionDrawable transitionDrawable = new TransitionDrawable(layers); - imageView.setImageDrawable(transitionDrawable); - transitionDrawable.startTransition(ANIMATION_DURATION); */ - imageView.setTag(to); - } else if (!hasFromImage && hasToImage) { - imageView.setImageDrawable(to); - imageView.setTag(to); - } else { - if (FADE_DBG) { - log("*Not* start cross-fade. " + imageView); - } - } - } - - // Debugging / testing code - - private static void log(String msg) { - Log.d(LOG_TAG, msg); - } -} \ No newline at end of file diff --git a/InCallUI/src/com/android/incallui/InCallCameraManager.java b/InCallUI/src/com/android/incallui/InCallCameraManager.java deleted file mode 100644 index 53000f1dd..000000000 --- a/InCallUI/src/com/android/incallui/InCallCameraManager.java +++ /dev/null @@ -1,184 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.content.Context; -import android.graphics.SurfaceTexture; -import android.hardware.camera2.CameraAccessException; -import android.hardware.camera2.CameraCharacteristics; -import android.hardware.camera2.CameraManager; -import android.hardware.camera2.params.StreamConfigurationMap; -import android.util.Size; - -import java.lang.String; -import java.util.Collections; -import java.util.concurrent.ConcurrentHashMap; -import java.util.Set; - -/** - * Used to track which camera is used for outgoing video. - */ -public class InCallCameraManager { - - public interface Listener { - void onActiveCameraSelectionChanged(boolean isUsingFrontFacingCamera); - } - - private final Set mCameraSelectionListeners = Collections. - newSetFromMap(new ConcurrentHashMap(8,0.9f,1)); - - /** - * The camera ID for the front facing camera. - */ - private String mFrontFacingCameraId; - - /** - * The camera ID for the rear facing camera. - */ - private String mRearFacingCameraId; - - /** - * The currently active camera. - */ - private boolean mUseFrontFacingCamera; - - /** - * Indicates whether the list of cameras has been initialized yet. Initialization is delayed - * until a video call is present. - */ - private boolean mIsInitialized = false; - - /** - * The context. - */ - private Context mContext; - - /** - * Initializes the InCall CameraManager. - * - * @param context The current context. - */ - public InCallCameraManager(Context context) { - mUseFrontFacingCamera = true; - mContext = context; - } - - /** - * Sets whether the front facing camera should be used or not. - * - * @param useFrontFacingCamera {@code True} if the front facing camera is to be used. - */ - public void setUseFrontFacingCamera(boolean useFrontFacingCamera) { - mUseFrontFacingCamera = useFrontFacingCamera; - for (Listener listener : mCameraSelectionListeners) { - listener.onActiveCameraSelectionChanged(mUseFrontFacingCamera); - } - } - - /** - * Determines whether the front facing camera is currently in use. - * - * @return {@code True} if the front facing camera is in use. - */ - public boolean isUsingFrontFacingCamera() { - return mUseFrontFacingCamera; - } - - /** - * Determines the active camera ID. - * - * @return The active camera ID. - */ - public String getActiveCameraId() { - maybeInitializeCameraList(mContext); - - if (mUseFrontFacingCamera) { - return mFrontFacingCameraId; - } else { - return mRearFacingCameraId; - } - } - - /** - * Get the list of cameras available for use. - * - * @param context The context. - */ - private void maybeInitializeCameraList(Context context) { - if (mIsInitialized || context == null) { - return; - } - - Log.v(this, "initializeCameraList"); - - CameraManager cameraManager = null; - try { - cameraManager = (CameraManager) context.getSystemService( - Context.CAMERA_SERVICE); - } catch (Exception e) { - Log.e(this, "Could not get camera service."); - return; - } - - if (cameraManager == null) { - return; - } - - String[] cameraIds = {}; - try { - cameraIds = cameraManager.getCameraIdList(); - } catch (CameraAccessException e) { - Log.d(this, "Could not access camera: "+e); - // Camera disabled by device policy. - return; - } - - for (int i = 0; i < cameraIds.length; i++) { - CameraCharacteristics c = null; - try { - c = cameraManager.getCameraCharacteristics(cameraIds[i]); - } catch (IllegalArgumentException e) { - // Device Id is unknown. - } catch (CameraAccessException e) { - // Camera disabled by device policy. - } - if (c != null) { - int facingCharacteristic = c.get(CameraCharacteristics.LENS_FACING); - if (facingCharacteristic == CameraCharacteristics.LENS_FACING_FRONT) { - mFrontFacingCameraId = cameraIds[i]; - } else if (facingCharacteristic == CameraCharacteristics.LENS_FACING_BACK) { - mRearFacingCameraId = cameraIds[i]; - } - } - } - - mIsInitialized = true; - Log.v(this, "initializeCameraList : done"); - } - - public void addCameraSelectionListener(Listener listener) { - if (listener != null) { - mCameraSelectionListeners.add(listener); - } - } - - public void removeCameraSelectionListener(Listener listener) { - if (listener != null) { - mCameraSelectionListeners.remove(listener); - } - } -} diff --git a/InCallUI/src/com/android/incallui/InCallContactInteractions.java b/InCallUI/src/com/android/incallui/InCallContactInteractions.java deleted file mode 100644 index 88070fe37..000000000 --- a/InCallUI/src/com/android/incallui/InCallContactInteractions.java +++ /dev/null @@ -1,399 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import com.google.common.annotations.VisibleForTesting; - -import android.content.Context; -import android.location.Address; -import android.text.TextUtils; -import android.text.format.DateFormat; -import android.util.Pair; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.ImageView; -import android.widget.ListAdapter; -import android.widget.RelativeLayout; -import android.widget.RelativeLayout.LayoutParams; -import android.widget.TextView; - -import com.android.dialer.R; - -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Date; -import java.util.List; -import java.util.Locale; - -/** - * Wrapper class for objects that are used in generating the context about the contact in the InCall - * screen. - * - * This handles generating the appropriate resource for the ListAdapter based on whether the contact - * is a business contact or not and logic for the manipulation of data for the call context. - */ -public class InCallContactInteractions { - private static final String TAG = InCallContactInteractions.class.getSimpleName(); - - private Context mContext; - private InCallContactInteractionsListAdapter mListAdapter; - private boolean mIsBusiness; - private View mBusinessHeaderView; - private LayoutInflater mInflater; - - public InCallContactInteractions(Context context, boolean isBusiness) { - mContext = context; - mInflater = (LayoutInflater) - context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - switchContactType(isBusiness); - } - - public InCallContactInteractionsListAdapter getListAdapter() { - return mListAdapter; - } - - /** - * Switches the "isBusiness" value, if applicable. Recreates the list adapter with the resource - * corresponding to the new isBusiness value if the "isBusiness" value is switched. - * - * @param isBusiness Whether or not the contact is a business. - * - * @return {@code true} if a new list adapter was created, {@code} otherwise. - */ - public boolean switchContactType(boolean isBusiness) { - if (mIsBusiness != isBusiness || mListAdapter == null) { - mIsBusiness = isBusiness; - mListAdapter = new InCallContactInteractionsListAdapter(mContext, - mIsBusiness ? R.layout.business_context_info_list_item - : R.layout.person_context_info_list_item); - return true; - } - return false; - } - - public View getBusinessListHeaderView() { - if (mBusinessHeaderView == null) { - mBusinessHeaderView = mInflater.inflate( - R.layout.business_contact_context_list_header, null); - } - return mBusinessHeaderView; - } - - public void setBusinessInfo(Address address, float distance, - List> openingHours) { - mListAdapter.clear(); - List info = new ArrayList(); - - // Hours of operation - if (openingHours != null) { - BusinessContextInfo hoursInfo = constructHoursInfo(openingHours); - if (hoursInfo != null) { - info.add(hoursInfo); - } - } - - // Location information - if (address != null) { - BusinessContextInfo locationInfo = constructLocationInfo(address, distance); - info.add(locationInfo); - } - - mListAdapter.addAll(info); - } - - /** - * Construct a BusinessContextInfo object containing hours of operation information. - * The format is: - * [Open now/Closed now] - * [Hours] - * - * @param openingHours - * @return BusinessContextInfo object with the schedule icon, the heading set to whether the - * business is open or not and the details set to the hours of operation. - */ - private BusinessContextInfo constructHoursInfo(List> openingHours) { - try { - return constructHoursInfo(Calendar.getInstance(), openingHours); - } catch (Exception e) { - // Catch all exceptions here because we don't want any crashes if something goes wrong. - Log.e(TAG, "Error constructing hours info: ", e); - } - return null; - } - - /** - * Pass in arbitrary current calendar time. - */ - @VisibleForTesting - BusinessContextInfo constructHoursInfo(Calendar currentTime, - List> openingHours) { - if (currentTime == null || openingHours == null || openingHours.size() == 0) { - return null; - } - - BusinessContextInfo hoursInfo = new BusinessContextInfo(); - hoursInfo.iconId = R.drawable.ic_schedule_white_24dp; - - boolean isOpenNow = false; - // This variable records which interval the current time is after. 0 denotes none of the - // intervals, 1 after the first interval, etc. It is also the index of the interval the - // current time is in (if open) or the next interval (if closed). - int afterInterval = 0; - // This variable counts the number of time intervals in today's opening hours. - int todaysIntervalCount = 0; - - for (Pair hours : openingHours) { - if (hours.first.compareTo(currentTime) <= 0 - && currentTime.compareTo(hours.second) < 0) { - // If the current time is on or after the opening time and strictly before the - // closing time, then this business is open. - isOpenNow = true; - } - - if (currentTime.get(Calendar.DAY_OF_YEAR) == hours.first.get(Calendar.DAY_OF_YEAR)) { - todaysIntervalCount += 1; - } - - if (currentTime.compareTo(hours.second) > 0) { - // This assumes that the list of intervals is sorted by time. - afterInterval += 1; - } - } - - hoursInfo.heading = isOpenNow ? mContext.getString(R.string.open_now) - : mContext.getString(R.string.closed_now); - - /* - * The following logic determines what to display in various cases for hours of operation. - * - * - Display all intervals if open now and number of intervals is <=2. - * - Display next closing time if open now and number of intervals is >2. - * - Display next opening time if currently closed but opens later today. - * - Display last time it closed today if closed now and tomorrow's hours are unknown. - * - Display tomorrow's first open time if closed today and tomorrow's hours are known. - * - * NOTE: The logic below assumes that the intervals are sorted by ascending time. Possible - * TODO to modify the logic above and ensure this is true. - */ - if (isOpenNow) { - if (todaysIntervalCount == 1) { - hoursInfo.detail = getTimeSpanStringForHours(openingHours.get(0)); - } else if (todaysIntervalCount == 2) { - hoursInfo.detail = mContext.getString( - R.string.opening_hours, - getTimeSpanStringForHours(openingHours.get(0)), - getTimeSpanStringForHours(openingHours.get(1))); - } else if (afterInterval < openingHours.size()) { - // This check should not be necessary since if it is currently open, we should not - // be after the last interval, but just in case, we don't want to crash. - hoursInfo.detail = mContext.getString( - R.string.closes_today_at, - getFormattedTimeForCalendar(openingHours.get(afterInterval).second)); - } - } else { // Currently closed - final int lastIntervalToday = todaysIntervalCount - 1; - if (todaysIntervalCount == 0) { // closed today - hoursInfo.detail = mContext.getString( - R.string.opens_tomorrow_at, - getFormattedTimeForCalendar(openingHours.get(0).first)); - } else if (currentTime.after(openingHours.get(lastIntervalToday).second)) { - // Passed hours for today - if (todaysIntervalCount < openingHours.size()) { - // If all of today's intervals are exhausted, assume the next are tomorrow's. - hoursInfo.detail = mContext.getString( - R.string.opens_tomorrow_at, - getFormattedTimeForCalendar( - openingHours.get(todaysIntervalCount).first)); - } else { - // Grab the last time it was open today. - hoursInfo.detail = mContext.getString( - R.string.closed_today_at, - getFormattedTimeForCalendar( - openingHours.get(lastIntervalToday).second)); - } - } else if (afterInterval < openingHours.size()) { - // This check should not be necessary since if it is currently before the last - // interval, afterInterval should be less than the count of intervals, but just in - // case, we don't want to crash. - hoursInfo.detail = mContext.getString( - R.string.opens_today_at, - getFormattedTimeForCalendar(openingHours.get(afterInterval).first)); - } - } - - return hoursInfo; - } - - String getFormattedTimeForCalendar(Calendar calendar) { - return DateFormat.getTimeFormat(mContext).format(calendar.getTime()); - } - - String getTimeSpanStringForHours(Pair hours) { - return mContext.getString(R.string.open_time_span, - getFormattedTimeForCalendar(hours.first), - getFormattedTimeForCalendar(hours.second)); - } - - /** - * Construct a BusinessContextInfo object with the location information of the business. - * The format is: - * [Straight line distance in miles or kilometers] - * [Address without state/country/etc.] - * - * @param address An Address object containing address details of the business - * @param distance The distance to the location in meters - * @return A BusinessContextInfo object with the location icon, the heading as the distance to - * the business and the details containing the address. - */ - private BusinessContextInfo constructLocationInfo(Address address, float distance) { - return constructLocationInfo(Locale.getDefault(), address, distance); - } - - @VisibleForTesting - BusinessContextInfo constructLocationInfo(Locale locale, Address address, - float distance) { - if (address == null) { - return null; - } - - BusinessContextInfo locationInfo = new BusinessContextInfo(); - locationInfo.iconId = R.drawable.ic_location_on_white_24dp; - if (distance != DistanceHelper.DISTANCE_NOT_FOUND) { - //TODO: add a setting to allow the user to select "KM" or "MI" as their distance units. - if (Locale.US.equals(locale)) { - locationInfo.heading = mContext.getString(R.string.distance_imperial_away, - distance * DistanceHelper.MILES_PER_METER); - } else { - locationInfo.heading = mContext.getString(R.string.distance_metric_away, - distance * DistanceHelper.KILOMETERS_PER_METER); - } - } - if (address.getLocality() != null) { - locationInfo.detail = mContext.getString( - R.string.display_address, - address.getAddressLine(0), - address.getLocality()); - } else { - locationInfo.detail = address.getAddressLine(0); - } - return locationInfo; - } - - /** - * Get the appropriate title for the context. - * @return The "Business info" title for a business contact and the "Recent messages" title for - * personal contacts. - */ - public String getContactContextTitle() { - return mIsBusiness - ? mContext.getResources().getString(R.string.business_contact_context_title) - : mContext.getResources().getString(R.string.person_contact_context_title); - } - - public static abstract class ContactContextInfo { - public abstract void bindView(View listItem); - } - - public static class BusinessContextInfo extends ContactContextInfo { - int iconId; - String heading; - String detail; - - @Override - public void bindView(View listItem) { - ImageView imageView = (ImageView) listItem.findViewById(R.id.icon); - TextView headingTextView = (TextView) listItem.findViewById(R.id.heading); - TextView detailTextView = (TextView) listItem.findViewById(R.id.detail); - - if (this.iconId == 0 || (this.heading == null && this.detail == null)) { - return; - } - - imageView.setImageDrawable(listItem.getContext().getDrawable(this.iconId)); - - headingTextView.setText(this.heading); - headingTextView.setVisibility(TextUtils.isEmpty(this.heading) - ? View.GONE : View.VISIBLE); - - detailTextView.setText(this.detail); - detailTextView.setVisibility(TextUtils.isEmpty(this.detail) - ? View.GONE : View.VISIBLE); - - } - } - - public static class PersonContextInfo extends ContactContextInfo { - boolean isIncoming; - String message; - String detail; - - @Override - public void bindView(View listItem) { - TextView messageTextView = (TextView) listItem.findViewById(R.id.message); - TextView detailTextView = (TextView) listItem.findViewById(R.id.detail); - - if (this.message == null || this.detail == null) { - return; - } - - messageTextView.setBackgroundResource(this.isIncoming ? - R.drawable.incoming_sms_background : R.drawable.outgoing_sms_background); - messageTextView.setText(this.message); - LayoutParams messageLayoutParams = (LayoutParams) messageTextView.getLayoutParams(); - messageLayoutParams.addRule(this.isIncoming? - RelativeLayout.ALIGN_PARENT_START : RelativeLayout.ALIGN_PARENT_END); - messageTextView.setLayoutParams(messageLayoutParams); - - LayoutParams detailLayoutParams = (LayoutParams) detailTextView.getLayoutParams(); - detailLayoutParams.addRule(this.isIncoming ? - RelativeLayout.ALIGN_PARENT_START : RelativeLayout.ALIGN_PARENT_END); - detailTextView.setLayoutParams(detailLayoutParams); - detailTextView.setText(this.detail); - } - } - - /** - * A list adapter for call context information. We use the same adapter for both business and - * contact context. - */ - private class InCallContactInteractionsListAdapter extends ArrayAdapter { - // The resource id of the list item layout. - int mResId; - - public InCallContactInteractionsListAdapter(Context context, int resource) { - super(context, resource); - mResId = resource; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - View listItem = mInflater.inflate(mResId, null); - ContactContextInfo item = getItem(position); - - if (item == null) { - return listItem; - } - - item.bindView(listItem); - return listItem; - } - } -} diff --git a/InCallUI/src/com/android/incallui/InCallDateUtils.java b/InCallUI/src/com/android/incallui/InCallDateUtils.java deleted file mode 100644 index 3401692ea..000000000 --- a/InCallUI/src/com/android/incallui/InCallDateUtils.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.android.incallui; - -import android.icu.text.MeasureFormat; -import android.icu.text.MeasureFormat.FormatWidth; -import android.icu.util.Measure; -import android.icu.util.MeasureUnit; - -import java.util.ArrayList; -import java.util.Locale; - -/** - * Methods to parse time and date information in the InCallUi - */ -public class InCallDateUtils { - - /** - * Return given duration in a human-friendly format. For example, "4 minutes 3 seconds" or - * "3 hours 1 second". Returns the hours, minutes and seconds in that order if they exist. - */ - public static String formatDuration(long millis) { - int hours = 0; - int minutes = 0; - int seconds = 0; - int elapsedSeconds = (int) (millis / 1000); - if (elapsedSeconds >= 3600) { - hours = elapsedSeconds / 3600; - elapsedSeconds -= hours * 3600; - } - if (elapsedSeconds >= 60) { - minutes = elapsedSeconds / 60; - elapsedSeconds -= minutes * 60; - } - seconds = elapsedSeconds; - - final ArrayList measures = new ArrayList(); - if (hours > 0) { - measures.add(new Measure(hours, MeasureUnit.HOUR)); - } - if (minutes > 0) { - measures.add(new Measure(minutes, MeasureUnit.MINUTE)); - } - if (seconds > 0) { - measures.add(new Measure(seconds, MeasureUnit.SECOND)); - } - - if (measures.isEmpty()) { - return ""; - } else { - return MeasureFormat.getInstance(Locale.getDefault(), FormatWidth.WIDE) - .formatMeasures(measures.toArray(new Measure[measures.size()])); - } - } -} diff --git a/InCallUI/src/com/android/incallui/InCallOrientationEventListener.java b/InCallUI/src/com/android/incallui/InCallOrientationEventListener.java deleted file mode 100644 index 3cab6dc3b..000000000 --- a/InCallUI/src/com/android/incallui/InCallOrientationEventListener.java +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.content.Context; -import android.content.res.Configuration; -import android.view.OrientationEventListener; -import android.hardware.SensorManager; -import android.view.Surface; -import android.content.pm.ActivityInfo; - -/** - * This class listens to Orientation events and overrides onOrientationChanged which gets - * invoked when an orientation change occurs. When that happens, we notify InCallUI registrants - * of the change. - */ -public class InCallOrientationEventListener extends OrientationEventListener { - - /** - * Screen orientation angles one of 0, 90, 180, 270, 360 in degrees. - */ - public static int SCREEN_ORIENTATION_0 = 0; - public static int SCREEN_ORIENTATION_90 = 90; - public static int SCREEN_ORIENTATION_180 = 180; - public static int SCREEN_ORIENTATION_270 = 270; - public static int SCREEN_ORIENTATION_360 = 360; - - public static int FULL_SENSOR_SCREEN_ORIENTATION = - ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR; - - public static int NO_SENSOR_SCREEN_ORIENTATION = - ActivityInfo.SCREEN_ORIENTATION_NOSENSOR; - - /** - * This is to identify dead zones where we won't notify others of orientation changed. - * Say for e.g our threshold is x degrees. We will only notify UI when our current rotation is - * within x degrees right or left of the screen orientation angles. If it's not within those - * ranges, we return SCREEN_ORIENTATION_UNKNOWN and ignore it. - */ - private static int SCREEN_ORIENTATION_UNKNOWN = -1; - - // Rotation threshold is 10 degrees. So if the rotation angle is within 10 degrees of any of - // the above angles, we will notify orientation changed. - private static int ROTATION_THRESHOLD = 10; - - - /** - * Cache the current rotation of the device. - */ - private static int sCurrentOrientation = SCREEN_ORIENTATION_0; - private boolean mEnabled = false; - - public InCallOrientationEventListener(Context context) { - super(context); - } - - /** - * Handles changes in device orientation. Notifies InCallPresenter of orientation changes. - * - * Note that this API receives sensor rotation in degrees as a param and we convert that to - * one of our screen orientation constants - (one of: {@link SCREEN_ORIENTATION_0}, - * {@link SCREEN_ORIENTATION_90}, {@link SCREEN_ORIENTATION_180}, - * {@link SCREEN_ORIENTATION_270}). - * - * @param rotation The new device sensor rotation in degrees - */ - @Override - public void onOrientationChanged(int rotation) { - if (rotation == OrientationEventListener.ORIENTATION_UNKNOWN) { - return; - } - - final int orientation = toScreenOrientation(rotation); - - if (orientation != SCREEN_ORIENTATION_UNKNOWN && sCurrentOrientation != orientation) { - sCurrentOrientation = orientation; - InCallPresenter.getInstance().onDeviceOrientationChange(sCurrentOrientation); - } - } - - /** - * Enables the OrientationEventListener and notifies listeners of current orientation if - * notify flag is true - * @param notify true or false. Notify device orientation changed if true. - */ - public void enable(boolean notify) { - if (mEnabled) { - Log.v(this, "enable: Orientation listener is already enabled. Ignoring..."); - return; - } - - super.enable(); - mEnabled = true; - if (notify) { - InCallPresenter.getInstance().onDeviceOrientationChange(sCurrentOrientation); - } - } - - /** - * Enables the OrientationEventListener with notify flag defaulting to false. - */ - public void enable() { - enable(false); - } - - /** - * Disables the OrientationEventListener. - */ - public void disable() { - if (!mEnabled) { - Log.v(this, "enable: Orientation listener is already disabled. Ignoring..."); - return; - } - - mEnabled = false; - super.disable(); - } - - /** - * Returns true the OrientationEventListener is enabled, false otherwise. - */ - public boolean isEnabled() { - return mEnabled; - } - - /** - * Converts sensor rotation in degrees to screen orientation constants. - * @param rotation sensor rotation angle in degrees - * @return Screen orientation angle in degrees (0, 90, 180, 270). Returns -1 for degrees not - * within threshold to identify zones where orientation change should not be trigerred. - */ - private int toScreenOrientation(int rotation) { - // Sensor orientation 90 is equivalent to screen orientation 270 and vice versa. This - // function returns the screen orientation. So we convert sensor rotation 90 to 270 and - // vice versa here. - if (isInLeftRange(rotation, SCREEN_ORIENTATION_360, ROTATION_THRESHOLD) || - isInRightRange(rotation, SCREEN_ORIENTATION_0, ROTATION_THRESHOLD)) { - return SCREEN_ORIENTATION_0; - } else if (isWithinThreshold(rotation, SCREEN_ORIENTATION_90, ROTATION_THRESHOLD)) { - return SCREEN_ORIENTATION_270; - } else if (isWithinThreshold(rotation, SCREEN_ORIENTATION_180, ROTATION_THRESHOLD)) { - return SCREEN_ORIENTATION_180; - } else if (isWithinThreshold(rotation, SCREEN_ORIENTATION_270, ROTATION_THRESHOLD)) { - return SCREEN_ORIENTATION_90; - } - return SCREEN_ORIENTATION_UNKNOWN; - } - - private static boolean isWithinRange(int value, int begin, int end) { - return value >= begin && value < end; - } - - private static boolean isWithinThreshold(int value, int center, int threshold) { - return isWithinRange(value, center - threshold, center + threshold); - } - - private static boolean isInLeftRange(int value, int center, int threshold) { - return isWithinRange(value, center - threshold, center); - } - - private static boolean isInRightRange(int value, int center, int threshold) { - return isWithinRange(value, center, center + threshold); - } -} diff --git a/InCallUI/src/com/android/incallui/InCallPresenter.java b/InCallUI/src/com/android/incallui/InCallPresenter.java deleted file mode 100644 index 0103f61ed..000000000 --- a/InCallUI/src/com/android/incallui/InCallPresenter.java +++ /dev/null @@ -1,1938 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui; - -import com.google.common.base.Preconditions; - -import android.app.ActivityManager.TaskDescription; -import android.app.FragmentManager; -import android.content.Context; -import android.content.Intent; -import android.content.res.Resources; -import android.database.ContentObserver; -import android.graphics.Point; -import android.os.Bundle; -import android.os.Handler; -import android.os.SystemClock; -import android.provider.CallLog; -import android.telecom.DisconnectCause; -import android.telecom.PhoneAccount; -import android.telecom.PhoneAccountHandle; -import android.telecom.TelecomManager; -import android.telecom.VideoProfile; -import android.telephony.PhoneStateListener; -import android.telephony.TelephonyManager; -import android.text.TextUtils; -import android.view.View; -import android.view.Window; -import android.view.WindowManager; - -import com.android.contacts.common.GeoUtil; -import com.android.contacts.common.compat.CallSdkCompat; -import com.android.contacts.common.compat.CompatUtils; -import com.android.contacts.common.compat.telecom.TelecomManagerCompat; -import com.android.contacts.common.interactions.TouchPointManager; -import com.android.contacts.common.testing.NeededForTesting; -import com.android.contacts.common.util.MaterialColorMapUtils.MaterialPalette; -import com.android.dialer.R; -import com.android.dialer.calllog.CallLogAsyncTaskUtil; -import com.android.dialer.calllog.CallLogAsyncTaskUtil.OnCallLogQueryFinishedListener; -import com.android.dialer.database.FilteredNumberAsyncQueryHandler; -import com.android.dialer.database.FilteredNumberAsyncQueryHandler.OnCheckBlockedListener; -import com.android.dialer.filterednumber.FilteredNumbersUtil; -import com.android.dialer.logging.InteractionEvent; -import com.android.dialer.logging.Logger; -import com.android.dialer.util.TelecomUtil; -import com.android.incallui.spam.SpamCallListListener; -import com.android.incallui.util.TelecomCallUtil; -import com.android.incalluibind.ObjectFactory; - -import java.util.Collections; -import java.util.List; -import java.util.Locale; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * Takes updates from the CallList and notifies the InCallActivity (UI) - * of the changes. - * Responsible for starting the activity for a new call and finishing the activity when all calls - * are disconnected. - * Creates and manages the in-call state and provides a listener pattern for the presenters - * that want to listen in on the in-call state changes. - * TODO: This class has become more of a state machine at this point. Consider renaming. - */ -public class InCallPresenter implements CallList.Listener, - CircularRevealFragment.OnCircularRevealCompleteListener, - InCallVideoCallCallbackNotifier.SessionModificationListener { - - private static final String EXTRA_FIRST_TIME_SHOWN = - "com.android.incallui.intent.extra.FIRST_TIME_SHOWN"; - - private static final long BLOCK_QUERY_TIMEOUT_MS = 1000; - - private static final Bundle EMPTY_EXTRAS = new Bundle(); - - private static InCallPresenter sInCallPresenter; - - /** - * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is - * load factor before resizing, 1 means we only expect a single thread to - * access the map so make only a single shard - */ - private final Set mListeners = Collections.newSetFromMap( - new ConcurrentHashMap(8, 0.9f, 1)); - private final List mIncomingCallListeners = new CopyOnWriteArrayList<>(); - private final Set mDetailsListeners = Collections.newSetFromMap( - new ConcurrentHashMap(8, 0.9f, 1)); - private final Set mCanAddCallListeners = Collections.newSetFromMap( - new ConcurrentHashMap(8, 0.9f, 1)); - private final Set mInCallUiListeners = Collections.newSetFromMap( - new ConcurrentHashMap(8, 0.9f, 1)); - private final Set mOrientationListeners = Collections.newSetFromMap( - new ConcurrentHashMap(8, 0.9f, 1)); - private final Set mInCallEventListeners = Collections.newSetFromMap( - new ConcurrentHashMap(8, 0.9f, 1)); - - private AudioModeProvider mAudioModeProvider; - private StatusBarNotifier mStatusBarNotifier; - private ExternalCallNotifier mExternalCallNotifier; - private ContactInfoCache mContactInfoCache; - private Context mContext; - private CallList mCallList; - private ExternalCallList mExternalCallList; - private InCallActivity mInCallActivity; - private InCallState mInCallState = InCallState.NO_CALLS; - private ProximitySensor mProximitySensor; - private boolean mServiceConnected = false; - private boolean mAccountSelectionCancelled = false; - private InCallCameraManager mInCallCameraManager = null; - private AnswerPresenter mAnswerPresenter = new AnswerPresenter(); - private FilteredNumberAsyncQueryHandler mFilteredQueryHandler; - private CallList.Listener mSpamCallListListener; - - /** - * Whether or not we are currently bound and waiting for Telecom to send us a new call. - */ - private boolean mBoundAndWaitingForOutgoingCall; - - /** - * If there is no actual call currently in the call list, this will be used as a fallback - * to determine the theme color for InCallUI. - */ - private PhoneAccountHandle mPendingPhoneAccountHandle; - - /** - * Determines if the InCall UI is in fullscreen mode or not. - */ - private boolean mIsFullScreen = false; - - private final android.telecom.Call.Callback mCallCallback = new android.telecom.Call.Callback() { - @Override - public void onPostDialWait(android.telecom.Call telecomCall, - String remainingPostDialSequence) { - final Call call = mCallList.getCallByTelecomCall(telecomCall); - if (call == null) { - Log.w(this, "Call not found in call list: " + telecomCall); - return; - } - onPostDialCharWait(call.getId(), remainingPostDialSequence); - } - - @Override - public void onDetailsChanged(android.telecom.Call telecomCall, - android.telecom.Call.Details details) { - final Call call = mCallList.getCallByTelecomCall(telecomCall); - if (call == null) { - Log.w(this, "Call not found in call list: " + telecomCall); - return; - } - for (InCallDetailsListener listener : mDetailsListeners) { - listener.onDetailsChanged(call, details); - } - } - - @Override - public void onConferenceableCallsChanged(android.telecom.Call telecomCall, - List conferenceableCalls) { - Log.i(this, "onConferenceableCallsChanged: " + telecomCall); - onDetailsChanged(telecomCall, telecomCall.getDetails()); - } - }; - - private PhoneStateListener mPhoneStateListener = new PhoneStateListener() { - public void onCallStateChanged(int state, String incomingNumber) { - if (state == TelephonyManager.CALL_STATE_RINGING) { - if (FilteredNumbersUtil.hasRecentEmergencyCall(mContext)) { - return; - } - // Check if the number is blocked, to silence the ringer. - String countryIso = GeoUtil.getCurrentCountryIso(mContext); - mFilteredQueryHandler.isBlockedNumber( - mOnCheckBlockedListener, incomingNumber, countryIso); - } - } - }; - - private final OnCheckBlockedListener mOnCheckBlockedListener = new OnCheckBlockedListener() { - @Override - public void onCheckComplete(final Integer id) { - if (id != null) { - // Silence the ringer now to prevent ringing and vibration before the call is - // terminated when Telecom attempts to add it. - TelecomUtil.silenceRinger(mContext); - } - } - }; - - /** - * Observes the CallLog to delete the call log entry for the blocked call after it is added. - * Times out if too much time has passed. - */ - private class BlockedNumberContentObserver extends ContentObserver { - private static final int TIMEOUT_MS = 5000; - - private Handler mHandler; - private String mNumber; - private long mTimeAddedMs; - - private Runnable mTimeoutRunnable = new Runnable() { - @Override - public void run() { - unregister(); - } - }; - - public BlockedNumberContentObserver(Handler handler, String number, long timeAddedMs) { - super(handler); - - mHandler = handler; - mNumber = number; - mTimeAddedMs = timeAddedMs; - } - - @Override - public void onChange(boolean selfChange) { - CallLogAsyncTaskUtil.deleteBlockedCall(mContext, mNumber, mTimeAddedMs, - new OnCallLogQueryFinishedListener() { - @Override - public void onQueryFinished(boolean hasEntry) { - if (mContext != null && hasEntry) { - unregister(); - } - } - }); - } - - public void register() { - if (mContext != null) { - mContext.getContentResolver().registerContentObserver( - CallLog.CONTENT_URI, true, this); - mHandler.postDelayed(mTimeoutRunnable, TIMEOUT_MS); - } - } - - private void unregister() { - if (mContext != null) { - mHandler.removeCallbacks(mTimeoutRunnable); - mContext.getContentResolver().unregisterContentObserver(this); - } - } - }; - - /** - * Is true when the activity has been previously started. Some code needs to know not just if - * the activity is currently up, but if it had been previously shown in foreground for this - * in-call session (e.g., StatusBarNotifier). This gets reset when the session ends in the - * tear-down method. - */ - private boolean mIsActivityPreviouslyStarted = false; - - /** - * Whether or not InCallService is bound to Telecom. - */ - private boolean mServiceBound = false; - - /** - * When configuration changes Android kills the current activity and starts a new one. - * The flag is used to check if full clean up is necessary (activity is stopped and new - * activity won't be started), or if a new activity will be started right after the current one - * is destroyed, and therefore no need in release all resources. - */ - private boolean mIsChangingConfigurations = false; - - /** Display colors for the UI. Consists of a primary color and secondary (darker) color */ - private MaterialPalette mThemeColors; - - private TelecomManager mTelecomManager; - private TelephonyManager mTelephonyManager; - - public static synchronized InCallPresenter getInstance() { - if (sInCallPresenter == null) { - sInCallPresenter = new InCallPresenter(); - } - return sInCallPresenter; - } - - @NeededForTesting - static synchronized void setInstance(InCallPresenter inCallPresenter) { - sInCallPresenter = inCallPresenter; - } - - public InCallState getInCallState() { - return mInCallState; - } - - public CallList getCallList() { - return mCallList; - } - - public void setUp(Context context, - CallList callList, - ExternalCallList externalCallList, - AudioModeProvider audioModeProvider, - StatusBarNotifier statusBarNotifier, - ExternalCallNotifier externalCallNotifier, - ContactInfoCache contactInfoCache, - ProximitySensor proximitySensor) { - if (mServiceConnected) { - Log.i(this, "New service connection replacing existing one."); - // retain the current resources, no need to create new ones. - Preconditions.checkState(context == mContext); - Preconditions.checkState(callList == mCallList); - Preconditions.checkState(audioModeProvider == mAudioModeProvider); - return; - } - - Preconditions.checkNotNull(context); - mContext = context; - - mContactInfoCache = contactInfoCache; - - mStatusBarNotifier = statusBarNotifier; - mExternalCallNotifier = externalCallNotifier; - addListener(mStatusBarNotifier); - - mAudioModeProvider = audioModeProvider; - - mProximitySensor = proximitySensor; - addListener(mProximitySensor); - - addIncomingCallListener(mAnswerPresenter); - addInCallUiListener(mAnswerPresenter); - - mCallList = callList; - mExternalCallList = externalCallList; - externalCallList.addExternalCallListener(mExternalCallNotifier); - - // This only gets called by the service so this is okay. - mServiceConnected = true; - - // The final thing we do in this set up is add ourselves as a listener to CallList. This - // will kick off an update and the whole process can start. - mCallList.addListener(this); - - // Create spam call list listener and add it to the list of listeners - mSpamCallListListener = new SpamCallListListener(context); - mCallList.addListener(mSpamCallListListener); - - VideoPauseController.getInstance().setUp(this); - InCallVideoCallCallbackNotifier.getInstance().addSessionModificationListener(this); - - mFilteredQueryHandler = new FilteredNumberAsyncQueryHandler(context.getContentResolver()); - mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); - mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); - mCallList.setExtendedCallInfoService( - com.android.dialerbind.ObjectFactory.newExtendedCallInfoService(context)); - - Log.d(this, "Finished InCallPresenter.setUp"); - } - - /** - * Called when the telephony service has disconnected from us. This will happen when there are - * no more active calls. However, we may still want to continue showing the UI for - * certain cases like showing "Call Ended". - * What we really want is to wait for the activity and the service to both disconnect before we - * tear things down. This method sets a serviceConnected boolean and calls a secondary method - * that performs the aforementioned logic. - */ - public void tearDown() { - Log.d(this, "tearDown"); - mCallList.clearOnDisconnect(); - - mServiceConnected = false; - attemptCleanup(); - - mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE); - VideoPauseController.getInstance().tearDown(); - InCallVideoCallCallbackNotifier.getInstance().removeSessionModificationListener(this); - } - - private void attemptFinishActivity() { - final boolean doFinish = (mInCallActivity != null && isActivityStarted()); - Log.i(this, "Hide in call UI: " + doFinish); - if (doFinish) { - mInCallActivity.setExcludeFromRecents(true); - mInCallActivity.finish(); - - if (mAccountSelectionCancelled) { - // This finish is a result of account selection cancellation - // do not include activity ending transition - mInCallActivity.overridePendingTransition(0, 0); - } - } - } - - /** - * Called when the UI begins, and starts the callstate callbacks if necessary. - */ - public void setActivity(InCallActivity inCallActivity) { - if (inCallActivity == null) { - throw new IllegalArgumentException("registerActivity cannot be called with null"); - } - if (mInCallActivity != null && mInCallActivity != inCallActivity) { - Log.w(this, "Setting a second activity before destroying the first."); - } - updateActivity(inCallActivity); - } - - /** - * Called when the UI ends. Attempts to tear down everything if necessary. See - * {@link #tearDown()} for more insight on the tear-down process. - */ - public void unsetActivity(InCallActivity inCallActivity) { - if (inCallActivity == null) { - throw new IllegalArgumentException("unregisterActivity cannot be called with null"); - } - if (mInCallActivity == null) { - Log.i(this, "No InCallActivity currently set, no need to unset."); - return; - } - if (mInCallActivity != inCallActivity) { - Log.w(this, "Second instance of InCallActivity is trying to unregister when another" - + " instance is active. Ignoring."); - return; - } - updateActivity(null); - } - - /** - * Updates the current instance of {@link InCallActivity} with the provided one. If a - * {@code null} activity is provided, it means that the activity was finished and we should - * attempt to cleanup. - */ - private void updateActivity(InCallActivity inCallActivity) { - boolean updateListeners = false; - boolean doAttemptCleanup = false; - - if (inCallActivity != null) { - if (mInCallActivity == null) { - updateListeners = true; - Log.i(this, "UI Initialized"); - } else { - // since setActivity is called onStart(), it can be called multiple times. - // This is fine and ignorable, but we do not want to update the world every time - // this happens (like going to/from background) so we do not set updateListeners. - } - - mInCallActivity = inCallActivity; - mInCallActivity.setExcludeFromRecents(false); - - // By the time the UI finally comes up, the call may already be disconnected. - // If that's the case, we may need to show an error dialog. - if (mCallList != null && mCallList.getDisconnectedCall() != null) { - maybeShowErrorDialogOnDisconnect(mCallList.getDisconnectedCall()); - } - - // When the UI comes up, we need to first check the in-call state. - // If we are showing NO_CALLS, that means that a call probably connected and - // then immediately disconnected before the UI was able to come up. - // If we dont have any calls, start tearing down the UI instead. - // NOTE: This code relies on {@link #mInCallActivity} being set so we run it after - // it has been set. - if (mInCallState == InCallState.NO_CALLS) { - Log.i(this, "UI Initialized, but no calls left. shut down."); - attemptFinishActivity(); - return; - } - } else { - Log.i(this, "UI Destroyed"); - updateListeners = true; - mInCallActivity = null; - - // We attempt cleanup for the destroy case but only after we recalculate the state - // to see if we need to come back up or stay shut down. This is why we do the - // cleanup after the call to onCallListChange() instead of directly here. - doAttemptCleanup = true; - } - - // Messages can come from the telephony layer while the activity is coming up - // and while the activity is going down. So in both cases we need to recalculate what - // state we should be in after they complete. - // Examples: (1) A new incoming call could come in and then get disconnected before - // the activity is created. - // (2) All calls could disconnect and then get a new incoming call before the - // activity is destroyed. - // - // b/1122139 - We previously had a check for mServiceConnected here as well, but there are - // cases where we need to recalculate the current state even if the service in not - // connected. In particular the case where startOrFinish() is called while the app is - // already finish()ing. In that case, we skip updating the state with the knowledge that - // we will check again once the activity has finished. That means we have to recalculate the - // state here even if the service is disconnected since we may not have finished a state - // transition while finish()ing. - if (updateListeners) { - onCallListChange(mCallList); - } - - if (doAttemptCleanup) { - attemptCleanup(); - } - } - - private boolean mAwaitingCallListUpdate = false; - - public void onBringToForeground(boolean showDialpad) { - Log.i(this, "Bringing UI to foreground."); - bringToForeground(showDialpad); - } - - public void onCallAdded(final android.telecom.Call call) { - LatencyReport latencyReport = new LatencyReport(call); - if (shouldAttemptBlocking(call)) { - maybeBlockCall(call, latencyReport); - } else { - latencyReport.onCallBlockingDone(); - if (call.getDetails() - .hasProperty(CallSdkCompat.Details.PROPERTY_IS_EXTERNAL_CALL)) { - mExternalCallList.onCallAdded(call); - } else { - mCallList.onCallAdded(call, latencyReport); - } - } - - // Since a call has been added we are no longer waiting for Telecom to send us a call. - setBoundAndWaitingForOutgoingCall(false, null); - call.registerCallback(mCallCallback); - } - - private boolean shouldAttemptBlocking(android.telecom.Call call) { - if (call.getState() != android.telecom.Call.STATE_RINGING) { - return false; - } - if (TelecomCallUtil.isEmergencyCall(call)) { - Log.i(this, "Not attempting to block incoming emergency call"); - return false; - } - if (FilteredNumbersUtil.hasRecentEmergencyCall(mContext)) { - Log.i(this, "Not attempting to block incoming call due to recent emergency call"); - return false; - } - if (call.getDetails().hasProperty(CallSdkCompat.Details.PROPERTY_IS_EXTERNAL_CALL)) { - return false; - } - - return true; - } - - /** - * Checks whether a call should be blocked, and blocks it if so. Otherwise, it adds the call - * to the CallList so it can proceed as normal. There is a timeout, so if the function for - * checking whether a function is blocked does not return in a reasonable time, we proceed - * with adding the call anyways. - */ - private void maybeBlockCall(final android.telecom.Call call, - final LatencyReport latencyReport) { - final String countryIso = GeoUtil.getCurrentCountryIso(mContext); - final String number = TelecomCallUtil.getNumber(call); - final long timeAdded = System.currentTimeMillis(); - - // Though AtomicBoolean's can be scary, don't fear, as in this case it is only used on the - // main UI thread. It is needed so we can change its value within different scopes, since - // that cannot be done with a final boolean. - final AtomicBoolean hasTimedOut = new AtomicBoolean(false); - - final Handler handler = new Handler(); - - // Proceed if the query is slow; the call may still be blocked after the query returns. - final Runnable runnable = new Runnable() { - public void run() { - hasTimedOut.set(true); - latencyReport.onCallBlockingDone(); - mCallList.onCallAdded(call, latencyReport); - } - }; - handler.postDelayed(runnable, BLOCK_QUERY_TIMEOUT_MS); - - OnCheckBlockedListener onCheckBlockedListener = new OnCheckBlockedListener() { - @Override - public void onCheckComplete(final Integer id) { - if (!hasTimedOut.get()) { - handler.removeCallbacks(runnable); - } - if (id == null) { - if (!hasTimedOut.get()) { - latencyReport.onCallBlockingDone(); - mCallList.onCallAdded(call, latencyReport); - } - } else { - Log.i(this, "Rejecting incoming call from blocked number"); - call.reject(false, null); - Logger.logInteraction(InteractionEvent.CALL_BLOCKED); - - mFilteredQueryHandler.incrementFilteredCount(id); - - // Register observer to update the call log. - // BlockedNumberContentObserver will unregister after successful log or timeout. - BlockedNumberContentObserver contentObserver = - new BlockedNumberContentObserver(new Handler(), number, timeAdded); - contentObserver.register(); - } - } - }; - - final boolean success = mFilteredQueryHandler.isBlockedNumber( - onCheckBlockedListener, number, countryIso); - if (!success) { - Log.d(this, "checkForBlockedCall: invalid number, skipping block checking"); - if (!hasTimedOut.get()) { - handler.removeCallbacks(runnable); - - latencyReport.onCallBlockingDone(); - mCallList.onCallAdded(call, latencyReport); - } - } - } - - public void onCallRemoved(android.telecom.Call call) { - if (call.getDetails() - .hasProperty(CallSdkCompat.Details.PROPERTY_IS_EXTERNAL_CALL)) { - mExternalCallList.onCallRemoved(call); - } else { - mCallList.onCallRemoved(call); - call.unregisterCallback(mCallCallback); - } - } - - public void onCanAddCallChanged(boolean canAddCall) { - for (CanAddCallListener listener : mCanAddCallListeners) { - listener.onCanAddCallChanged(canAddCall); - } - } - - /** - * Called when there is a change to the call list. - * Sets the In-Call state for the entire in-call app based on the information it gets from - * CallList. Dispatches the in-call state to all listeners. Can trigger the creation or - * destruction of the UI based on the states that is calculates. - */ - @Override - public void onCallListChange(CallList callList) { - if (mInCallActivity != null && mInCallActivity.getCallCardFragment() != null && - mInCallActivity.getCallCardFragment().isAnimating()) { - mAwaitingCallListUpdate = true; - return; - } - if (callList == null) { - return; - } - - mAwaitingCallListUpdate = false; - - InCallState newState = getPotentialStateFromCallList(callList); - InCallState oldState = mInCallState; - Log.d(this, "onCallListChange oldState= " + oldState + " newState=" + newState); - newState = startOrFinishUi(newState); - Log.d(this, "onCallListChange newState changed to " + newState); - - // Set the new state before announcing it to the world - Log.i(this, "Phone switching state: " + oldState + " -> " + newState); - mInCallState = newState; - - // notify listeners of new state - for (InCallStateListener listener : mListeners) { - Log.d(this, "Notify " + listener + " of state " + mInCallState.toString()); - listener.onStateChange(oldState, mInCallState, callList); - } - - if (isActivityStarted()) { - final boolean hasCall = callList.getActiveOrBackgroundCall() != null || - callList.getOutgoingCall() != null; - mInCallActivity.dismissKeyguard(hasCall); - } - } - - /** - * Called when there is a new incoming call. - * - * @param call - */ - @Override - public void onIncomingCall(Call call) { - InCallState newState = startOrFinishUi(InCallState.INCOMING); - InCallState oldState = mInCallState; - - Log.i(this, "Phone switching state: " + oldState + " -> " + newState); - mInCallState = newState; - - for (IncomingCallListener listener : mIncomingCallListeners) { - listener.onIncomingCall(oldState, mInCallState, call); - } - } - - @Override - public void onUpgradeToVideo(Call call) { - //NO-OP - } - /** - * Called when a call becomes disconnected. Called everytime an existing call - * changes from being connected (incoming/outgoing/active) to disconnected. - */ - @Override - public void onDisconnect(Call call) { - maybeShowErrorDialogOnDisconnect(call); - - // We need to do the run the same code as onCallListChange. - onCallListChange(mCallList); - - if (isActivityStarted()) { - mInCallActivity.dismissKeyguard(false); - } - - if (call.isEmergencyCall()) { - FilteredNumbersUtil.recordLastEmergencyCallTime(mContext); - } - } - - @Override - public void onUpgradeToVideoRequest(Call call, int videoState) { - Log.d(this, "onUpgradeToVideoRequest call = " + call + " video state = " + videoState); - - if (call == null) { - return; - } - - call.setRequestedVideoState(videoState); - } - - /** - * Given the call list, return the state in which the in-call screen should be. - */ - public InCallState getPotentialStateFromCallList(CallList callList) { - - InCallState newState = InCallState.NO_CALLS; - - if (callList == null) { - return newState; - } - if (callList.getIncomingCall() != null) { - newState = InCallState.INCOMING; - } else if (callList.getWaitingForAccountCall() != null) { - newState = InCallState.WAITING_FOR_ACCOUNT; - } else if (callList.getPendingOutgoingCall() != null) { - newState = InCallState.PENDING_OUTGOING; - } else if (callList.getOutgoingCall() != null) { - newState = InCallState.OUTGOING; - } else if (callList.getActiveCall() != null || - callList.getBackgroundCall() != null || - callList.getDisconnectedCall() != null || - callList.getDisconnectingCall() != null) { - newState = InCallState.INCALL; - } - - if (newState == InCallState.NO_CALLS) { - if (mBoundAndWaitingForOutgoingCall) { - return InCallState.OUTGOING; - } - } - - return newState; - } - - public boolean isBoundAndWaitingForOutgoingCall() { - return mBoundAndWaitingForOutgoingCall; - } - - public void setBoundAndWaitingForOutgoingCall(boolean isBound, PhoneAccountHandle handle) { - // NOTE: It is possible for there to be a race and have handle become null before - // the circular reveal starts. This should not cause any problems because CallCardFragment - // should fallback to the actual call in the CallList at that point in time to determine - // the theme color. - Log.i(this, "setBoundAndWaitingForOutgoingCall: " + isBound); - mBoundAndWaitingForOutgoingCall = isBound; - mPendingPhoneAccountHandle = handle; - if (isBound && mInCallState == InCallState.NO_CALLS) { - mInCallState = InCallState.OUTGOING; - } - } - - @Override - public void onCircularRevealComplete(FragmentManager fm) { - if (mInCallActivity != null) { - mInCallActivity.showCallCardFragment(true); - mInCallActivity.getCallCardFragment().animateForNewOutgoingCall(); - CircularRevealFragment.endCircularReveal(mInCallActivity.getFragmentManager()); - } - } - - public void onShrinkAnimationComplete() { - if (mAwaitingCallListUpdate) { - onCallListChange(mCallList); - } - } - - public void addIncomingCallListener(IncomingCallListener listener) { - Preconditions.checkNotNull(listener); - mIncomingCallListeners.add(listener); - } - - public void removeIncomingCallListener(IncomingCallListener listener) { - if (listener != null) { - mIncomingCallListeners.remove(listener); - } - } - - public void addListener(InCallStateListener listener) { - Preconditions.checkNotNull(listener); - mListeners.add(listener); - } - - public void removeListener(InCallStateListener listener) { - if (listener != null) { - mListeners.remove(listener); - } - } - - public void addDetailsListener(InCallDetailsListener listener) { - Preconditions.checkNotNull(listener); - mDetailsListeners.add(listener); - } - - public void removeDetailsListener(InCallDetailsListener listener) { - if (listener != null) { - mDetailsListeners.remove(listener); - } - } - - public void addCanAddCallListener(CanAddCallListener listener) { - Preconditions.checkNotNull(listener); - mCanAddCallListeners.add(listener); - } - - public void removeCanAddCallListener(CanAddCallListener listener) { - if (listener != null) { - mCanAddCallListeners.remove(listener); - } - } - - public void addOrientationListener(InCallOrientationListener listener) { - Preconditions.checkNotNull(listener); - mOrientationListeners.add(listener); - } - - public void removeOrientationListener(InCallOrientationListener listener) { - if (listener != null) { - mOrientationListeners.remove(listener); - } - } - - public void addInCallEventListener(InCallEventListener listener) { - Preconditions.checkNotNull(listener); - mInCallEventListeners.add(listener); - } - - public void removeInCallEventListener(InCallEventListener listener) { - if (listener != null) { - mInCallEventListeners.remove(listener); - } - } - - public ProximitySensor getProximitySensor() { - return mProximitySensor; - } - - public void handleAccountSelection(PhoneAccountHandle accountHandle, boolean setDefault) { - if (mCallList != null) { - Call call = mCallList.getWaitingForAccountCall(); - if (call != null) { - String callId = call.getId(); - TelecomAdapter.getInstance().phoneAccountSelected(callId, accountHandle, setDefault); - } - } - } - - public void cancelAccountSelection() { - mAccountSelectionCancelled = true; - if (mCallList != null) { - Call call = mCallList.getWaitingForAccountCall(); - if (call != null) { - String callId = call.getId(); - TelecomAdapter.getInstance().disconnectCall(callId); - } - } - } - - /** - * Hangs up any active or outgoing calls. - */ - public void hangUpOngoingCall(Context context) { - // By the time we receive this intent, we could be shut down and call list - // could be null. Bail in those cases. - if (mCallList == null) { - if (mStatusBarNotifier == null) { - // The In Call UI has crashed but the notification still stayed up. We should not - // come to this stage. - StatusBarNotifier.clearAllCallNotifications(context); - } - return; - } - - Call call = mCallList.getOutgoingCall(); - if (call == null) { - call = mCallList.getActiveOrBackgroundCall(); - } - - if (call != null) { - TelecomAdapter.getInstance().disconnectCall(call.getId()); - call.setState(Call.State.DISCONNECTING); - mCallList.onUpdate(call); - } - } - - /** - * Answers any incoming call. - */ - public void answerIncomingCall(Context context, int videoState) { - // By the time we receive this intent, we could be shut down and call list - // could be null. Bail in those cases. - if (mCallList == null) { - StatusBarNotifier.clearAllCallNotifications(context); - return; - } - - Call call = mCallList.getIncomingCall(); - if (call != null) { - TelecomAdapter.getInstance().answerCall(call.getId(), videoState); - showInCall(false, false/* newOutgoingCall */); - } - } - - /** - * Declines any incoming call. - */ - public void declineIncomingCall(Context context) { - // By the time we receive this intent, we could be shut down and call list - // could be null. Bail in those cases. - if (mCallList == null) { - StatusBarNotifier.clearAllCallNotifications(context); - return; - } - - Call call = mCallList.getIncomingCall(); - if (call != null) { - TelecomAdapter.getInstance().rejectCall(call.getId(), false, null); - } - } - - public void acceptUpgradeRequest(int videoState, Context context) { - Log.d(this, " acceptUpgradeRequest videoState " + videoState); - // Bail if we have been shut down and the call list is null. - if (mCallList == null) { - StatusBarNotifier.clearAllCallNotifications(context); - Log.e(this, " acceptUpgradeRequest mCallList is empty so returning"); - return; - } - - Call call = mCallList.getVideoUpgradeRequestCall(); - if (call != null) { - VideoProfile videoProfile = new VideoProfile(videoState); - call.getVideoCall().sendSessionModifyResponse(videoProfile); - call.setSessionModificationState(Call.SessionModificationState.NO_REQUEST); - } - } - - public void declineUpgradeRequest(Context context) { - Log.d(this, " declineUpgradeRequest"); - // Bail if we have been shut down and the call list is null. - if (mCallList == null) { - StatusBarNotifier.clearAllCallNotifications(context); - Log.e(this, " declineUpgradeRequest mCallList is empty so returning"); - return; - } - - Call call = mCallList.getVideoUpgradeRequestCall(); - if (call != null) { - VideoProfile videoProfile = - new VideoProfile(call.getVideoState()); - call.getVideoCall().sendSessionModifyResponse(videoProfile); - call.setSessionModificationState(Call.SessionModificationState.NO_REQUEST); - } - } - - /*package*/ - void declineUpgradeRequest() { - // Pass mContext if InCallActivity is destroyed. - // Ex: When user pressed back key while in active call and - // then modify request is received followed by MT call. - declineUpgradeRequest(mInCallActivity != null ? mInCallActivity : mContext); - } - - /** - * Returns true if the incall app is the foreground application. - */ - public boolean isShowingInCallUi() { - return (isActivityStarted() && mInCallActivity.isVisible()); - } - - /** - * Returns true if the activity has been created and is running. - * Returns true as long as activity is not destroyed or finishing. This ensures that we return - * true even if the activity is paused (not in foreground). - */ - public boolean isActivityStarted() { - return (mInCallActivity != null && - !mInCallActivity.isDestroyed() && - !mInCallActivity.isFinishing()); - } - - public boolean isActivityPreviouslyStarted() { - return mIsActivityPreviouslyStarted; - } - - /** - * Determines if the In-Call app is currently changing configuration. - * - * @return {@code true} if the In-Call app is changing configuration. - */ - public boolean isChangingConfigurations() { - return mIsChangingConfigurations; - } - - /** - * Tracks whether the In-Call app is currently in the process of changing configuration (i.e. - * screen orientation). - */ - /*package*/ - void updateIsChangingConfigurations() { - mIsChangingConfigurations = false; - if (mInCallActivity != null) { - mIsChangingConfigurations = mInCallActivity.isChangingConfigurations(); - } - Log.v(this, "updateIsChangingConfigurations = " + mIsChangingConfigurations); - } - - - /** - * Called when the activity goes in/out of the foreground. - */ - public void onUiShowing(boolean showing) { - // We need to update the notification bar when we leave the UI because that - // could trigger it to show again. - if (mStatusBarNotifier != null) { - mStatusBarNotifier.updateNotification(mInCallState, mCallList); - } - - if (mProximitySensor != null) { - mProximitySensor.onInCallShowing(showing); - } - - Intent broadcastIntent = ObjectFactory.getUiReadyBroadcastIntent(mContext); - if (broadcastIntent != null) { - broadcastIntent.putExtra(EXTRA_FIRST_TIME_SHOWN, !mIsActivityPreviouslyStarted); - - if (showing) { - Log.d(this, "Sending sticky broadcast: ", broadcastIntent); - mContext.sendStickyBroadcast(broadcastIntent); - } else { - Log.d(this, "Removing sticky broadcast: ", broadcastIntent); - mContext.removeStickyBroadcast(broadcastIntent); - } - } - - if (showing) { - mIsActivityPreviouslyStarted = true; - } else { - updateIsChangingConfigurations(); - } - - for (InCallUiListener listener : mInCallUiListeners) { - listener.onUiShowing(showing); - } - } - - public void addInCallUiListener(InCallUiListener listener) { - mInCallUiListeners.add(listener); - } - - public boolean removeInCallUiListener(InCallUiListener listener) { - return mInCallUiListeners.remove(listener); - } - - /*package*/ - void onActivityStarted() { - Log.d(this, "onActivityStarted"); - notifyVideoPauseController(true); - } - - /*package*/ - void onActivityStopped() { - Log.d(this, "onActivityStopped"); - notifyVideoPauseController(false); - } - - private void notifyVideoPauseController(boolean showing) { - Log.d(this, "notifyVideoPauseController: mIsChangingConfigurations=" + - mIsChangingConfigurations); - if (!mIsChangingConfigurations) { - VideoPauseController.getInstance().onUiShowing(showing); - } - } - - /** - * Brings the app into the foreground if possible. - */ - public void bringToForeground(boolean showDialpad) { - // Before we bring the incall UI to the foreground, we check to see if: - // 1. It is not currently in the foreground - // 2. We are in a state where we want to show the incall ui (i.e. there are calls to - // be displayed) - // If the activity hadn't actually been started previously, yet there are still calls - // present (e.g. a call was accepted by a bluetooth or wired headset), we want to - // bring it up the UI regardless. - if (!isShowingInCallUi() && mInCallState != InCallState.NO_CALLS) { - showInCall(showDialpad, false /* newOutgoingCall */); - } - } - - public void onPostDialCharWait(String callId, String chars) { - if (isActivityStarted()) { - mInCallActivity.showPostCharWaitDialog(callId, chars); - } - } - - /** - * Handles the green CALL key while in-call. - * @return true if we consumed the event. - */ - public boolean handleCallKey() { - Log.v(this, "handleCallKey"); - - // The green CALL button means either "Answer", "Unhold", or - // "Swap calls", or can be a no-op, depending on the current state - // of the Phone. - - /** - * INCOMING CALL - */ - final CallList calls = mCallList; - final Call incomingCall = calls.getIncomingCall(); - Log.v(this, "incomingCall: " + incomingCall); - - // (1) Attempt to answer a call - if (incomingCall != null) { - TelecomAdapter.getInstance().answerCall( - incomingCall.getId(), VideoProfile.STATE_AUDIO_ONLY); - return true; - } - - /** - * STATE_ACTIVE CALL - */ - final Call activeCall = calls.getActiveCall(); - if (activeCall != null) { - // TODO: This logic is repeated from CallButtonPresenter.java. We should - // consolidate this logic. - final boolean canMerge = activeCall.can( - android.telecom.Call.Details.CAPABILITY_MERGE_CONFERENCE); - final boolean canSwap = activeCall.can( - android.telecom.Call.Details.CAPABILITY_SWAP_CONFERENCE); - - Log.v(this, "activeCall: " + activeCall + ", canMerge: " + canMerge + - ", canSwap: " + canSwap); - - // (2) Attempt actions on conference calls - if (canMerge) { - TelecomAdapter.getInstance().merge(activeCall.getId()); - return true; - } else if (canSwap) { - TelecomAdapter.getInstance().swap(activeCall.getId()); - return true; - } - } - - /** - * BACKGROUND CALL - */ - final Call heldCall = calls.getBackgroundCall(); - if (heldCall != null) { - // We have a hold call so presumeable it will always support HOLD...but - // there is no harm in double checking. - final boolean canHold = heldCall.can(android.telecom.Call.Details.CAPABILITY_HOLD); - - Log.v(this, "heldCall: " + heldCall + ", canHold: " + canHold); - - // (4) unhold call - if (heldCall.getState() == Call.State.ONHOLD && canHold) { - TelecomAdapter.getInstance().unholdCall(heldCall.getId()); - return true; - } - } - - // Always consume hard keys - return true; - } - - /** - * A dialog could have prevented in-call screen from being previously finished. - * This function checks to see if there should be any UI left and if not attempts - * to tear down the UI. - */ - public void onDismissDialog() { - Log.i(this, "Dialog dismissed"); - if (mInCallState == InCallState.NO_CALLS) { - attemptFinishActivity(); - attemptCleanup(); - } - } - - /** - * Toggles whether the application is in fullscreen mode or not. - * - * @return {@code true} if in-call is now in fullscreen mode. - */ - public boolean toggleFullscreenMode() { - boolean isFullScreen = !mIsFullScreen; - Log.v(this, "toggleFullscreenMode = " + isFullScreen); - setFullScreen(isFullScreen); - return mIsFullScreen; - } - - /** - * Clears the previous fullscreen state. - */ - public void clearFullscreen() { - mIsFullScreen = false; - } - - /** - * Changes the fullscreen mode of the in-call UI. - * - * @param isFullScreen {@code true} if in-call should be in fullscreen mode, {@code false} - * otherwise. - */ - public void setFullScreen(boolean isFullScreen) { - setFullScreen(isFullScreen, false /* force */); - } - - /** - * Changes the fullscreen mode of the in-call UI. - * - * @param isFullScreen {@code true} if in-call should be in fullscreen mode, {@code false} - * otherwise. - * @param force {@code true} if fullscreen mode should be set regardless of its current state. - */ - public void setFullScreen(boolean isFullScreen, boolean force) { - Log.v(this, "setFullScreen = " + isFullScreen); - - // As a safeguard, ensure we cannot enter fullscreen if the dialpad is shown. - if (isDialpadVisible()) { - isFullScreen = false; - Log.v(this, "setFullScreen overridden as dialpad is shown = " + isFullScreen); - } - - if (mIsFullScreen == isFullScreen && !force) { - Log.v(this, "setFullScreen ignored as already in that state."); - return; - } - mIsFullScreen = isFullScreen; - notifyFullscreenModeChange(mIsFullScreen); - } - - /** - * @return {@code true} if the in-call ui is currently in fullscreen mode, {@code false} - * otherwise. - */ - public boolean isFullscreen() { - return mIsFullScreen; - } - - - /** - * Called by the {@link VideoCallPresenter} to inform of a change in full screen video status. - * - * @param isFullscreenMode {@code True} if entering full screen mode. - */ - public void notifyFullscreenModeChange(boolean isFullscreenMode) { - for (InCallEventListener listener : mInCallEventListeners) { - listener.onFullscreenModeChanged(isFullscreenMode); - } - } - - /** - * Called by the {@link CallCardPresenter} to inform of a change in visibility of the secondary - * caller info bar. - * - * @param isVisible {@code true} if the secondary caller info is visible, {@code false} - * otherwise. - * @param height the height of the secondary caller info bar. - */ - public void notifySecondaryCallerInfoVisibilityChanged(boolean isVisible, int height) { - for (InCallEventListener listener : mInCallEventListeners) { - listener.onSecondaryCallerInfoVisibilityChanged(isVisible, height); - } - } - - - /** - * For some disconnected causes, we show a dialog. This calls into the activity to show - * the dialog if appropriate for the call. - */ - private void maybeShowErrorDialogOnDisconnect(Call call) { - // For newly disconnected calls, we may want to show a dialog on specific error conditions - if (isActivityStarted() && call.getState() == Call.State.DISCONNECTED) { - if (call.getAccountHandle() == null && !call.isConferenceCall()) { - setDisconnectCauseForMissingAccounts(call); - } - mInCallActivity.maybeShowErrorDialogOnDisconnect(call.getDisconnectCause()); - } - } - - /** - * When the state of in-call changes, this is the first method to get called. It determines if - * the UI needs to be started or finished depending on the new state and does it. - */ - private InCallState startOrFinishUi(InCallState newState) { - Log.d(this, "startOrFinishUi: " + mInCallState + " -> " + newState); - - // TODO: Consider a proper state machine implementation - - // If the state isn't changing we have already done any starting/stopping of activities in - // a previous pass...so lets cut out early - if (newState == mInCallState) { - return newState; - } - - // A new Incoming call means that the user needs to be notified of the the call (since - // it wasn't them who initiated it). We do this through full screen notifications and - // happens indirectly through {@link StatusBarNotifier}. - // - // The process for incoming calls is as follows: - // - // 1) CallList - Announces existence of new INCOMING call - // 2) InCallPresenter - Gets announcement and calculates that the new InCallState - // - should be set to INCOMING. - // 3) InCallPresenter - This method is called to see if we need to start or finish - // the app given the new state. - // 4) StatusBarNotifier - Listens to InCallState changes. InCallPresenter calls - // StatusBarNotifier explicitly to issue a FullScreen Notification - // that will either start the InCallActivity or show the user a - // top-level notification dialog if the user is in an immersive app. - // That notification can also start the InCallActivity. - // 5) InCallActivity - Main activity starts up and at the end of its onCreate will - // call InCallPresenter::setActivity() to let the presenter - // know that start-up is complete. - // - // [ AND NOW YOU'RE IN THE CALL. voila! ] - // - // Our app is started using a fullScreen notification. We need to do this whenever - // we get an incoming call. Depending on the current context of the device, either a - // incoming call HUN or the actual InCallActivity will be shown. - final boolean startIncomingCallSequence = (InCallState.INCOMING == newState); - - // A dialog to show on top of the InCallUI to select a PhoneAccount - final boolean showAccountPicker = (InCallState.WAITING_FOR_ACCOUNT == newState); - - // A new outgoing call indicates that the user just now dialed a number and when that - // happens we need to display the screen immediately or show an account picker dialog if - // no default is set. However, if the main InCallUI is already visible, we do not want to - // re-initiate the start-up animation, so we do not need to do anything here. - // - // It is also possible to go into an intermediate state where the call has been initiated - // but Telecom has not yet returned with the details of the call (handle, gateway, etc.). - // This pending outgoing state can also launch the call screen. - // - // This is different from the incoming call sequence because we do not need to shock the - // user with a top-level notification. Just show the call UI normally. - final boolean mainUiNotVisible = !isShowingInCallUi() || !getCallCardFragmentVisible(); - boolean showCallUi = InCallState.OUTGOING == newState && mainUiNotVisible; - - // Direct transition from PENDING_OUTGOING -> INCALL means that there was an error in the - // outgoing call process, so the UI should be brought up to show an error dialog. - showCallUi |= (InCallState.PENDING_OUTGOING == mInCallState - && InCallState.INCALL == newState && !isShowingInCallUi()); - - // Another exception - InCallActivity is in charge of disconnecting a call with no - // valid accounts set. Bring the UI up if this is true for the current pending outgoing - // call so that: - // 1) The call can be disconnected correctly - // 2) The UI comes up and correctly displays the error dialog. - // TODO: Remove these special case conditions by making InCallPresenter a true state - // machine. Telecom should also be the component responsible for disconnecting a call - // with no valid accounts. - showCallUi |= InCallState.PENDING_OUTGOING == newState && mainUiNotVisible - && isCallWithNoValidAccounts(mCallList.getPendingOutgoingCall()); - - // The only time that we have an instance of mInCallActivity and it isn't started is - // when it is being destroyed. In that case, lets avoid bringing up another instance of - // the activity. When it is finally destroyed, we double check if we should bring it back - // up so we aren't going to lose anything by avoiding a second startup here. - boolean activityIsFinishing = mInCallActivity != null && !isActivityStarted(); - if (activityIsFinishing) { - Log.i(this, "Undo the state change: " + newState + " -> " + mInCallState); - return mInCallState; - } - - if (showCallUi || showAccountPicker) { - Log.i(this, "Start in call UI"); - showInCall(false /* showDialpad */, !showAccountPicker /* newOutgoingCall */); - } else if (startIncomingCallSequence) { - Log.i(this, "Start Full Screen in call UI"); - - // We're about the bring up the in-call UI for an incoming call. If we still have - // dialogs up, we need to clear them out before showing incoming screen. - if (isActivityStarted()) { - mInCallActivity.dismissPendingDialogs(); - } - if (!startUi(newState)) { - // startUI refused to start the UI. This indicates that it needed to restart the - // activity. When it finally restarts, it will call us back, so we do not actually - // change the state yet (we return mInCallState instead of newState). - return mInCallState; - } - } else if (newState == InCallState.NO_CALLS) { - // The new state is the no calls state. Tear everything down. - attemptFinishActivity(); - attemptCleanup(); - } - - return newState; - } - - /** - * Determines whether or not a call has no valid phone accounts that can be used to make the - * call with. Emergency calls do not require a phone account. - * - * @param call to check accounts for. - * @return {@code true} if the call has no call capable phone accounts set, {@code false} if - * the call contains a phone account that could be used to initiate it with, or is an emergency - * call. - */ - public static boolean isCallWithNoValidAccounts(Call call) { - if (call != null && !call.isEmergencyCall()) { - Bundle extras = call.getIntentExtras(); - - if (extras == null) { - extras = EMPTY_EXTRAS; - } - - final List phoneAccountHandles = extras - .getParcelableArrayList(android.telecom.Call.AVAILABLE_PHONE_ACCOUNTS); - - if ((call.getAccountHandle() == null && - (phoneAccountHandles == null || phoneAccountHandles.isEmpty()))) { - Log.i(InCallPresenter.getInstance(), "No valid accounts for call " + call); - return true; - } - } - return false; - } - - /** - * Sets the DisconnectCause for a call that was disconnected because it was missing a - * PhoneAccount or PhoneAccounts to select from. - * @param call - */ - private void setDisconnectCauseForMissingAccounts(Call call) { - android.telecom.Call telecomCall = call.getTelecomCall(); - - Bundle extras = telecomCall.getDetails().getIntentExtras(); - // Initialize the extras bundle to avoid NPE - if (extras == null) { - extras = new Bundle(); - } - - final List phoneAccountHandles = extras.getParcelableArrayList( - android.telecom.Call.AVAILABLE_PHONE_ACCOUNTS); - - if (phoneAccountHandles == null || phoneAccountHandles.isEmpty()) { - String scheme = telecomCall.getDetails().getHandle().getScheme(); - final String errorMsg = PhoneAccount.SCHEME_TEL.equals(scheme) ? - mContext.getString(R.string.callFailed_simError) : - mContext.getString(R.string.incall_error_supp_service_unknown); - DisconnectCause disconnectCause = - new DisconnectCause(DisconnectCause.ERROR, null, errorMsg, errorMsg); - call.setDisconnectCause(disconnectCause); - } - } - - private boolean startUi(InCallState inCallState) { - boolean isCallWaiting = mCallList.getActiveCall() != null && - mCallList.getIncomingCall() != null; - - // If the screen is off, we need to make sure it gets turned on for incoming calls. - // This normally works just fine thanks to FLAG_TURN_SCREEN_ON but that only works - // when the activity is first created. Therefore, to ensure the screen is turned on - // for the call waiting case, we finish() the current activity and start a new one. - // There should be no jank from this since the screen is already off and will remain so - // until our new activity is up. - - if (isCallWaiting) { - if (mProximitySensor.isScreenReallyOff() && isActivityStarted()) { - Log.i(this, "Restarting InCallActivity to turn screen on for call waiting"); - mInCallActivity.finish(); - // When the activity actually finishes, we will start it again if there are - // any active calls, so we do not need to start it explicitly here. Note, we - // actually get called back on this function to restart it. - - // We return false to indicate that we did not actually start the UI. - return false; - } else { - showInCall(false, false); - } - } else { - mStatusBarNotifier.updateNotification(inCallState, mCallList); - } - return true; - } - - /** - * Checks to see if both the UI is gone and the service is disconnected. If so, tear it all - * down. - */ - private void attemptCleanup() { - boolean shouldCleanup = (mInCallActivity == null && !mServiceConnected && - mInCallState == InCallState.NO_CALLS); - Log.i(this, "attemptCleanup? " + shouldCleanup); - - if (shouldCleanup) { - mIsActivityPreviouslyStarted = false; - mIsChangingConfigurations = false; - - // blow away stale contact info so that we get fresh data on - // the next set of calls - if (mContactInfoCache != null) { - mContactInfoCache.clearCache(); - } - mContactInfoCache = null; - - if (mProximitySensor != null) { - removeListener(mProximitySensor); - mProximitySensor.tearDown(); - } - mProximitySensor = null; - - mAudioModeProvider = null; - - if (mStatusBarNotifier != null) { - removeListener(mStatusBarNotifier); - } - if (mExternalCallNotifier != null && mExternalCallList != null) { - mExternalCallList.removeExternalCallListener(mExternalCallNotifier); - } - mStatusBarNotifier = null; - - if (mCallList != null) { - mCallList.removeListener(this); - mCallList.removeListener(mSpamCallListListener); - } - mCallList = null; - - mContext = null; - mInCallActivity = null; - - mListeners.clear(); - mIncomingCallListeners.clear(); - mDetailsListeners.clear(); - mCanAddCallListeners.clear(); - mOrientationListeners.clear(); - mInCallEventListeners.clear(); - - Log.d(this, "Finished InCallPresenter.CleanUp"); - } - } - - public void showInCall(final boolean showDialpad, final boolean newOutgoingCall) { - Log.i(this, "Showing InCallActivity"); - mContext.startActivity(getInCallIntent(showDialpad, newOutgoingCall)); - } - - public void onServiceBind() { - mServiceBound = true; - } - - public void onServiceUnbind() { - InCallPresenter.getInstance().setBoundAndWaitingForOutgoingCall(false, null); - mServiceBound = false; - } - - public boolean isServiceBound() { - return mServiceBound; - } - - public void maybeStartRevealAnimation(Intent intent) { - if (intent == null || mInCallActivity != null) { - return; - } - final Bundle extras = intent.getBundleExtra(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS); - if (extras == null) { - // Incoming call, just show the in-call UI directly. - return; - } - - if (extras.containsKey(android.telecom.Call.AVAILABLE_PHONE_ACCOUNTS)) { - // Account selection dialog will show up so don't show the animation. - return; - } - - final PhoneAccountHandle accountHandle = - intent.getParcelableExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE); - final Point touchPoint = extras.getParcelable(TouchPointManager.TOUCH_POINT); - - InCallPresenter.getInstance().setBoundAndWaitingForOutgoingCall(true, accountHandle); - - final Intent incallIntent = getInCallIntent(false, true); - incallIntent.putExtra(TouchPointManager.TOUCH_POINT, touchPoint); - mContext.startActivity(incallIntent); - } - - public Intent getInCallIntent(boolean showDialpad, boolean newOutgoingCall) { - final Intent intent = new Intent(Intent.ACTION_MAIN, null); - intent.setFlags(Intent.FLAG_ACTIVITY_NO_USER_ACTION | Intent.FLAG_ACTIVITY_NEW_TASK); - - intent.setClass(mContext, InCallActivity.class); - if (showDialpad) { - intent.putExtra(InCallActivity.SHOW_DIALPAD_EXTRA, true); - } - intent.putExtra(InCallActivity.NEW_OUTGOING_CALL_EXTRA, newOutgoingCall); - return intent; - } - - /** - * Retrieves the current in-call camera manager instance, creating if necessary. - * - * @return The {@link InCallCameraManager}. - */ - public InCallCameraManager getInCallCameraManager() { - synchronized(this) { - if (mInCallCameraManager == null) { - mInCallCameraManager = new InCallCameraManager(mContext); - } - - return mInCallCameraManager; - } - } - - /** - * Notifies listeners of changes in orientation and notify calls of rotation angle change. - * - * @param orientation The screen orientation of the device (one of: - * {@link InCallOrientationEventListener#SCREEN_ORIENTATION_0}, - * {@link InCallOrientationEventListener#SCREEN_ORIENTATION_90}, - * {@link InCallOrientationEventListener#SCREEN_ORIENTATION_180}, - * {@link InCallOrientationEventListener#SCREEN_ORIENTATION_270}). - */ - public void onDeviceOrientationChange(int orientation) { - Log.d(this, "onDeviceOrientationChange: orientation= " + orientation); - - if (mCallList != null) { - mCallList.notifyCallsOfDeviceRotation(orientation); - } else { - Log.w(this, "onDeviceOrientationChange: CallList is null."); - } - - // Notify listeners of device orientation changed. - for (InCallOrientationListener listener : mOrientationListeners) { - listener.onDeviceOrientationChanged(orientation); - } - } - - /** - * Configures the in-call UI activity so it can change orientations or not. Enables the - * orientation event listener if allowOrientationChange is true, disables it if false. - * - * @param allowOrientationChange {@code True} if the in-call UI can change between portrait - * and landscape. {@Code False} if the in-call UI should be locked in portrait. - */ - public void setInCallAllowsOrientationChange(boolean allowOrientationChange) { - if (mInCallActivity == null) { - Log.e(this, "InCallActivity is null. Can't set requested orientation."); - return; - } - - if (!allowOrientationChange) { - mInCallActivity.setRequestedOrientation( - InCallOrientationEventListener.NO_SENSOR_SCREEN_ORIENTATION); - } else { - // Using SCREEN_ORIENTATION_FULL_SENSOR allows for reverse-portrait orientation, where - // SCREEN_ORIENTATION_SENSOR does not. - mInCallActivity.setRequestedOrientation( - InCallOrientationEventListener.FULL_SENSOR_SCREEN_ORIENTATION); - } - mInCallActivity.enableInCallOrientationEventListener(allowOrientationChange); - } - - public void enableScreenTimeout(boolean enable) { - Log.v(this, "enableScreenTimeout: value=" + enable); - if (mInCallActivity == null) { - Log.e(this, "enableScreenTimeout: InCallActivity is null."); - return; - } - - final Window window = mInCallActivity.getWindow(); - if (enable) { - window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } else { - window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } - } - - /** - * Returns the space available beside the call card. - * - * @return The space beside the call card. - */ - public float getSpaceBesideCallCard() { - if (mInCallActivity != null && mInCallActivity.getCallCardFragment() != null) { - return mInCallActivity.getCallCardFragment().getSpaceBesideCallCard(); - } - return 0; - } - - /** - * Returns whether the call card fragment is currently visible. - * - * @return True if the call card fragment is visible. - */ - public boolean getCallCardFragmentVisible() { - if (mInCallActivity != null && mInCallActivity.getCallCardFragment() != null) { - return mInCallActivity.getCallCardFragment().isVisible(); - } - return false; - } - - /** - * Hides or shows the conference manager fragment. - * - * @param show {@code true} if the conference manager should be shown, {@code false} if it - * should be hidden. - */ - public void showConferenceCallManager(boolean show) { - if (mInCallActivity == null) { - return; - } - - mInCallActivity.showConferenceFragment(show); - } - - /** - * Determines if the dialpad is visible. - * - * @return {@code true} if the dialpad is visible, {@code false} otherwise. - */ - public boolean isDialpadVisible() { - if (mInCallActivity == null) { - return false; - } - return mInCallActivity.isDialpadVisible(); - } - - /** - * @return True if the application is currently running in a right-to-left locale. - */ - public static boolean isRtl() { - return TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == - View.LAYOUT_DIRECTION_RTL; - } - - /** - * Extract background color from call object. The theme colors will include a primary color - * and a secondary color. - */ - public void setThemeColors() { - // This method will set the background to default if the color is PhoneAccount.NO_COLOR. - mThemeColors = getColorsFromCall(mCallList.getFirstCall()); - - if (mInCallActivity == null) { - return; - } - - final Resources resources = mInCallActivity.getResources(); - final int color; - if (resources.getBoolean(R.bool.is_layout_landscape)) { - // TODO use ResourcesCompat.getColor(Resources, int, Resources.Theme) when available - // {@link Resources#getColor(int)} used for compatibility - color = resources.getColor(R.color.statusbar_background_color); - } else { - color = mThemeColors.mSecondaryColor; - } - - mInCallActivity.getWindow().setStatusBarColor(color); - final TaskDescription td = new TaskDescription( - resources.getString(R.string.notification_ongoing_call), null, color); - mInCallActivity.setTaskDescription(td); - } - - /** - * @return A palette for colors to display in the UI. - */ - public MaterialPalette getThemeColors() { - return mThemeColors; - } - - private MaterialPalette getColorsFromCall(Call call) { - if (call == null) { - return getColorsFromPhoneAccountHandle(mPendingPhoneAccountHandle); - } else { - if (call.isSpam()) { - Resources resources = mContext.getResources(); - return new InCallUIMaterialColorMapUtils( - resources).calculatePrimaryAndSecondaryColor( - resources.getColor(R.color.incall_call_spam_background_color)); - } else { - return getColorsFromPhoneAccountHandle(call.getAccountHandle()); - } - } - } - - private MaterialPalette getColorsFromPhoneAccountHandle(PhoneAccountHandle phoneAccountHandle) { - int highlightColor = PhoneAccount.NO_HIGHLIGHT_COLOR; - if (phoneAccountHandle != null) { - final TelecomManager tm = getTelecomManager(); - - if (tm != null) { - final PhoneAccount account = - TelecomManagerCompat.getPhoneAccount(tm, phoneAccountHandle); - // For single-sim devices, there will be no selected highlight color, so the phone - // account will default to NO_HIGHLIGHT_COLOR. - if (account != null && CompatUtils.isLollipopMr1Compatible()) { - highlightColor = account.getHighlightColor(); - } - } - } - return new InCallUIMaterialColorMapUtils( - mContext.getResources()).calculatePrimaryAndSecondaryColor(highlightColor); - } - - /** - * @return An instance of TelecomManager. - */ - public TelecomManager getTelecomManager() { - if (mTelecomManager == null) { - mTelecomManager = (TelecomManager) - mContext.getSystemService(Context.TELECOM_SERVICE); - } - return mTelecomManager; - } - - /** - * @return An instance of TelephonyManager - */ - public TelephonyManager getTelephonyManager() { - return mTelephonyManager; - } - - InCallActivity getActivity() { - return mInCallActivity; - } - - AnswerPresenter getAnswerPresenter() { - return mAnswerPresenter; - } - - ExternalCallNotifier getExternalCallNotifier() { - return mExternalCallNotifier; - } - - /** - * Private constructor. Must use getInstance() to get this singleton. - */ - private InCallPresenter() { - } - - /** - * All the main states of InCallActivity. - */ - public enum InCallState { - // InCall Screen is off and there are no calls - NO_CALLS, - - // Incoming-call screen is up - INCOMING, - - // In-call experience is showing - INCALL, - - // Waiting for user input before placing outgoing call - WAITING_FOR_ACCOUNT, - - // UI is starting up but no call has been initiated yet. - // The UI is waiting for Telecom to respond. - PENDING_OUTGOING, - - // User is dialing out - OUTGOING; - - public boolean isIncoming() { - return (this == INCOMING); - } - - public boolean isConnectingOrConnected() { - return (this == INCOMING || - this == OUTGOING || - this == INCALL); - } - } - - /** - * Interface implemented by classes that need to know about the InCall State. - */ - public interface InCallStateListener { - // TODO: Enhance state to contain the call objects instead of passing CallList - public void onStateChange(InCallState oldState, InCallState newState, CallList callList); - } - - public interface IncomingCallListener { - public void onIncomingCall(InCallState oldState, InCallState newState, Call call); - } - - public interface CanAddCallListener { - public void onCanAddCallChanged(boolean canAddCall); - } - - public interface InCallDetailsListener { - public void onDetailsChanged(Call call, android.telecom.Call.Details details); - } - - public interface InCallOrientationListener { - public void onDeviceOrientationChanged(int orientation); - } - - /** - * Interface implemented by classes that need to know about events which occur within the - * In-Call UI. Used as a means of communicating between fragments that make up the UI. - */ - public interface InCallEventListener { - public void onFullscreenModeChanged(boolean isFullscreenMode); - public void onSecondaryCallerInfoVisibilityChanged(boolean isVisible, int height); - } - - public interface InCallUiListener { - void onUiShowing(boolean showing); - } -} diff --git a/InCallUI/src/com/android/incallui/InCallServiceImpl.java b/InCallUI/src/com/android/incallui/InCallServiceImpl.java deleted file mode 100644 index 1414bc51d..000000000 --- a/InCallUI/src/com/android/incallui/InCallServiceImpl.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.content.Context; -import android.content.Intent; -import android.os.IBinder; -import android.telecom.Call; -import android.telecom.CallAudioState; -import android.telecom.InCallService; - -/** - * Used to receive updates about calls from the Telecom component. This service is bound to - * Telecom while there exist calls which potentially require UI. This includes ringing (incoming), - * dialing (outgoing), and active calls. When the last call is disconnected, Telecom will unbind to - * the service triggering InCallActivity (via CallList) to finish soon after. - */ -public class InCallServiceImpl extends InCallService { - - @Override - public void onCallAudioStateChanged(CallAudioState audioState) { - AudioModeProvider.getInstance().onAudioStateChanged(audioState.isMuted(), - audioState.getRoute(), audioState.getSupportedRouteMask()); - } - - @Override - public void onBringToForeground(boolean showDialpad) { - InCallPresenter.getInstance().onBringToForeground(showDialpad); - } - - @Override - public void onCallAdded(Call call) { - InCallPresenter.getInstance().onCallAdded(call); - } - - @Override - public void onCallRemoved(Call call) { - InCallPresenter.getInstance().onCallRemoved(call); - } - - @Override - public void onCanAddCallChanged(boolean canAddCall) { - InCallPresenter.getInstance().onCanAddCallChanged(canAddCall); - } - - @Override - public IBinder onBind(Intent intent) { - final Context context = getApplicationContext(); - final ContactInfoCache contactInfoCache = ContactInfoCache.getInstance(context); - InCallPresenter.getInstance().setUp( - getApplicationContext(), - CallList.getInstance(), - new ExternalCallList(), - AudioModeProvider.getInstance(), - new StatusBarNotifier(context, contactInfoCache), - new ExternalCallNotifier(context, contactInfoCache), - contactInfoCache, - new ProximitySensor( - context, - AudioModeProvider.getInstance(), - new AccelerometerListener(context)) - ); - InCallPresenter.getInstance().onServiceBind(); - InCallPresenter.getInstance().maybeStartRevealAnimation(intent); - TelecomAdapter.getInstance().setInCallService(this); - - return super.onBind(intent); - } - - @Override - public boolean onUnbind(Intent intent) { - super.onUnbind(intent); - - InCallPresenter.getInstance().onServiceUnbind(); - tearDown(); - - return false; - } - - private void tearDown() { - Log.v(this, "tearDown"); - // Tear down the InCall system - TelecomAdapter.getInstance().clearInCallService(); - InCallPresenter.getInstance().tearDown(); - } -} diff --git a/InCallUI/src/com/android/incallui/InCallServiceListener.java b/InCallUI/src/com/android/incallui/InCallServiceListener.java deleted file mode 100644 index 11a5b08ef..000000000 --- a/InCallUI/src/com/android/incallui/InCallServiceListener.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui; - -import android.telecom.InCallService; - -/** - * Interface implemented by In-Call components that maintain a reference to the Telecom API - * {@code InCallService} object. Clarifies the expectations associated with the relevant method - * calls. - */ -public interface InCallServiceListener { - - /** - * Called once at {@code InCallService} startup time with a valid instance. At - * that time, there will be no existing {@code Call}s. - * - * @param inCallService The {@code InCallService} object. - */ - void setInCallService(InCallService inCallService); - - /** - * Called once at {@code InCallService} shutdown time. At that time, any {@code Call}s - * will have transitioned through the disconnected state and will no longer exist. - */ - void clearInCallService(); -} diff --git a/InCallUI/src/com/android/incallui/InCallUIMaterialColorMapUtils.java b/InCallUI/src/com/android/incallui/InCallUIMaterialColorMapUtils.java deleted file mode 100644 index 9c108b855..000000000 --- a/InCallUI/src/com/android/incallui/InCallUIMaterialColorMapUtils.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.android.incallui; - -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.telecom.PhoneAccount; - -import com.android.contacts.common.util.MaterialColorMapUtils; -import com.android.contacts.common.util.MaterialColorMapUtils.MaterialPalette; -import com.android.dialer.R; - -public class InCallUIMaterialColorMapUtils extends MaterialColorMapUtils { - private final TypedArray sPrimaryColors; - private final TypedArray sSecondaryColors; - private final Resources mResources; - - public InCallUIMaterialColorMapUtils(Resources resources) { - super(resources); - sPrimaryColors = resources.obtainTypedArray(R.array.background_colors); - sSecondaryColors = resources.obtainTypedArray(R.array.background_colors_dark); - mResources = resources; - } - - /** - * Currently the InCallUI color will only vary by SIM color which is a list of colors - * defined in the background_colors array, so first search the list for the matching color and - * fall back to the closest matching color if an exact match does not exist. - */ - @Override - public MaterialPalette calculatePrimaryAndSecondaryColor(int color) { - if (color == PhoneAccount.NO_HIGHLIGHT_COLOR) { - return getDefaultPrimaryAndSecondaryColors(mResources); - } - - for (int i = 0; i < sPrimaryColors.length(); i++) { - if (sPrimaryColors.getColor(i, 0) == color) { - return new MaterialPalette( - sPrimaryColors.getColor(i, 0), - sSecondaryColors.getColor(i, 0)); - } - } - - // The color isn't in the list, so use the superclass to find an approximate color. - return super.calculatePrimaryAndSecondaryColor(color); - } - - /** - * {@link Resources#getColor(int) used for compatibility - */ - @SuppressWarnings("deprecation") - public static MaterialPalette getDefaultPrimaryAndSecondaryColors(Resources resources) { - final int primaryColor = resources.getColor(R.color.dialer_theme_color); - final int secondaryColor = resources.getColor(R.color.dialer_theme_color_dark); - return new MaterialPalette(primaryColor, secondaryColor); - } -} diff --git a/InCallUI/src/com/android/incallui/InCallVideoCallCallback.java b/InCallUI/src/com/android/incallui/InCallVideoCallCallback.java deleted file mode 100644 index 99e6d5129..000000000 --- a/InCallUI/src/com/android/incallui/InCallVideoCallCallback.java +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.telecom.Connection; -import android.telecom.Connection.VideoProvider; -import android.telecom.InCallService.VideoCall; -import android.telecom.VideoProfile; -import android.telecom.VideoProfile.CameraCapabilities; - -/** - * Implements the InCallUI VideoCall Callback. - */ -public class InCallVideoCallCallback extends VideoCall.Callback { - - /** - * The call associated with this {@link InCallVideoCallCallback}. - */ - private Call mCall; - - /** - * Creates an instance of the call video client, specifying the call it is related to. - * - * @param call The call. - */ - public InCallVideoCallCallback(Call call) { - mCall = call; - } - - /** - * Handles an incoming session modification request. - * - * @param videoProfile The requested video call profile. - */ - @Override - public void onSessionModifyRequestReceived(VideoProfile videoProfile) { - Log.d(this, " onSessionModifyRequestReceived videoProfile=" + videoProfile); - int previousVideoState = VideoUtils.getUnPausedVideoState(mCall.getVideoState()); - int newVideoState = VideoUtils.getUnPausedVideoState(videoProfile.getVideoState()); - - boolean wasVideoCall = VideoUtils.isVideoCall(previousVideoState); - boolean isVideoCall = VideoUtils.isVideoCall(newVideoState); - - // Check for upgrades to video. - if (!wasVideoCall && isVideoCall && previousVideoState != newVideoState) { - InCallVideoCallCallbackNotifier.getInstance().upgradeToVideoRequest(mCall, - newVideoState); - } - } - - /** - * Handles a session modification response. - * - * @param status Status of the session modify request. Valid values are - * {@link Connection.VideoProvider#SESSION_MODIFY_REQUEST_SUCCESS}, - * {@link Connection.VideoProvider#SESSION_MODIFY_REQUEST_FAIL}, - * {@link Connection.VideoProvider#SESSION_MODIFY_REQUEST_INVALID} - * @param requestedProfile - * @param responseProfile The actual profile changes made by the peer device. - */ - @Override - public void onSessionModifyResponseReceived(int status, VideoProfile requestedProfile, - VideoProfile responseProfile) { - Log.d(this, "onSessionModifyResponseReceived status=" + status + " requestedProfile=" - + requestedProfile + " responseProfile=" + responseProfile); - if (status != VideoProvider.SESSION_MODIFY_REQUEST_SUCCESS) { - // Report the reason the upgrade failed as the new session modification state. - if (status == VideoProvider.SESSION_MODIFY_REQUEST_TIMED_OUT) { - mCall.setSessionModificationState( - Call.SessionModificationState.UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT); - } else { - if (status == VideoProvider.SESSION_MODIFY_REQUEST_REJECTED_BY_REMOTE) { - mCall.setSessionModificationState( - Call.SessionModificationState.REQUEST_REJECTED); - } else { - mCall.setSessionModificationState( - Call.SessionModificationState.REQUEST_FAILED); - } - } - } - - // Finally clear the outstanding request. - mCall.setSessionModificationState(Call.SessionModificationState.NO_REQUEST); - } - - /** - * Handles a call session event. - * - * @param event The event. - */ - @Override - public void onCallSessionEvent(int event) { - InCallVideoCallCallbackNotifier.getInstance().callSessionEvent(event); - } - - /** - * Handles a change to the peer video dimensions. - * - * @param width The updated peer video width. - * @param height The updated peer video height. - */ - @Override - public void onPeerDimensionsChanged(int width, int height) { - InCallVideoCallCallbackNotifier.getInstance().peerDimensionsChanged(mCall, width, height); - } - - /** - * Handles a change to the video quality of the call. - * - * @param videoQuality The updated video call quality. - */ - @Override - public void onVideoQualityChanged(int videoQuality) { - InCallVideoCallCallbackNotifier.getInstance().videoQualityChanged(mCall, videoQuality); - } - - /** - * Handles a change to the call data usage. No implementation as the in-call UI does not - * display data usage. - * - * @param dataUsage The updated data usage. - */ - @Override - public void onCallDataUsageChanged(long dataUsage) { - Log.d(this, "onCallDataUsageChanged: dataUsage = " + dataUsage); - InCallVideoCallCallbackNotifier.getInstance().callDataUsageChanged(dataUsage); - } - - /** - * Handles changes to the camera capabilities. No implementation as the in-call UI does not - * make use of camera capabilities. - * - * @param cameraCapabilities The changed camera capabilities. - */ - @Override - public void onCameraCapabilitiesChanged(CameraCapabilities cameraCapabilities) { - if (cameraCapabilities != null) { - InCallVideoCallCallbackNotifier.getInstance().cameraDimensionsChanged( - mCall, cameraCapabilities.getWidth(), cameraCapabilities.getHeight()); - } - } -} diff --git a/InCallUI/src/com/android/incallui/InCallVideoCallCallbackNotifier.java b/InCallUI/src/com/android/incallui/InCallVideoCallCallbackNotifier.java deleted file mode 100644 index bb7529205..000000000 --- a/InCallUI/src/com/android/incallui/InCallVideoCallCallbackNotifier.java +++ /dev/null @@ -1,284 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import com.google.common.base.Preconditions; - -import java.util.Collections; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; - -/** - * Class used by {@link InCallService.VideoCallCallback} to notify interested parties of incoming - * events. - */ -public class InCallVideoCallCallbackNotifier { - /** - * Singleton instance of this class. - */ - private static InCallVideoCallCallbackNotifier sInstance = - new InCallVideoCallCallbackNotifier(); - - /** - * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is - * load factor before resizing, 1 means we only expect a single thread to - * access the map so make only a single shard - */ - private final Set mSessionModificationListeners = - Collections.newSetFromMap(new ConcurrentHashMap - (8, 0.9f, 1)); - private final Set mVideoEventListeners = Collections.newSetFromMap( - new ConcurrentHashMap(8, 0.9f, 1)); - private final Set mSurfaceChangeListeners = Collections.newSetFromMap( - new ConcurrentHashMap(8, 0.9f, 1)); - - /** - * Static singleton accessor method. - */ - public static InCallVideoCallCallbackNotifier getInstance() { - return sInstance; - } - - /** - * Private constructor. Instance should only be acquired through getInstance(). - */ - private InCallVideoCallCallbackNotifier() { - } - - /** - * Adds a new {@link SessionModificationListener}. - * - * @param listener The listener. - */ - public void addSessionModificationListener(SessionModificationListener listener) { - Preconditions.checkNotNull(listener); - mSessionModificationListeners.add(listener); - } - - /** - * Remove a {@link SessionModificationListener}. - * - * @param listener The listener. - */ - public void removeSessionModificationListener(SessionModificationListener listener) { - if (listener != null) { - mSessionModificationListeners.remove(listener); - } - } - - /** - * Adds a new {@link VideoEventListener}. - * - * @param listener The listener. - */ - public void addVideoEventListener(VideoEventListener listener) { - Preconditions.checkNotNull(listener); - mVideoEventListeners.add(listener); - } - - /** - * Remove a {@link VideoEventListener}. - * - * @param listener The listener. - */ - public void removeVideoEventListener(VideoEventListener listener) { - if (listener != null) { - mVideoEventListeners.remove(listener); - } - } - - /** - * Adds a new {@link SurfaceChangeListener}. - * - * @param listener The listener. - */ - public void addSurfaceChangeListener(SurfaceChangeListener listener) { - Preconditions.checkNotNull(listener); - mSurfaceChangeListeners.add(listener); - } - - /** - * Remove a {@link SurfaceChangeListener}. - * - * @param listener The listener. - */ - public void removeSurfaceChangeListener(SurfaceChangeListener listener) { - if (listener != null) { - mSurfaceChangeListeners.remove(listener); - } - } - - /** - * Inform listeners of an upgrade to video request for a call. - * @param call The call. - * @param videoState The video state we want to upgrade to. - */ - public void upgradeToVideoRequest(Call call, int videoState) { - Log.d(this, "upgradeToVideoRequest call = " + call + " new video state = " + videoState); - for (SessionModificationListener listener : mSessionModificationListeners) { - listener.onUpgradeToVideoRequest(call, videoState); - } - } - - /** - * Inform listeners of a call session event. - * - * @param event The call session event. - */ - public void callSessionEvent(int event) { - for (VideoEventListener listener : mVideoEventListeners) { - listener.onCallSessionEvent(event); - } - } - - /** - * Inform listeners of a downgrade to audio. - * - * @param call The call. - * @param paused The paused state. - */ - public void peerPausedStateChanged(Call call, boolean paused) { - for (VideoEventListener listener : mVideoEventListeners) { - listener.onPeerPauseStateChanged(call, paused); - } - } - - /** - * Inform listeners of any change in the video quality of the call - * - * @param call The call. - * @param videoQuality The updated video quality of the call. - */ - public void videoQualityChanged(Call call, int videoQuality) { - for (VideoEventListener listener : mVideoEventListeners) { - listener.onVideoQualityChanged(call, videoQuality); - } - } - - /** - * Inform listeners of a change to peer dimensions. - * - * @param call The call. - * @param width New peer width. - * @param height New peer height. - */ - public void peerDimensionsChanged(Call call, int width, int height) { - for (SurfaceChangeListener listener : mSurfaceChangeListeners) { - listener.onUpdatePeerDimensions(call, width, height); - } - } - - /** - * Inform listeners of a change to camera dimensions. - * - * @param call The call. - * @param width The new camera video width. - * @param height The new camera video height. - */ - public void cameraDimensionsChanged(Call call, int width, int height) { - for (SurfaceChangeListener listener : mSurfaceChangeListeners) { - listener.onCameraDimensionsChange(call, width, height); - } - } - - /** - * Inform listeners of a change to call data usage. - * - * @param dataUsage data usage value - */ - public void callDataUsageChanged(long dataUsage) { - for (VideoEventListener listener : mVideoEventListeners) { - listener.onCallDataUsageChange(dataUsage); - } - } - - /** - * Listener interface for any class that wants to be notified of upgrade to video request. - */ - public interface SessionModificationListener { - /** - * Called when a peer request is received to upgrade an audio-only call to a video call. - * - * @param call The call the request was received for. - * @param videoState The requested video state. - */ - public void onUpgradeToVideoRequest(Call call, int videoState); - } - - /** - * Listener interface for any class that wants to be notified of video events, including pause - * and un-pause of peer video, video quality changes. - */ - public interface VideoEventListener { - /** - * Called when the peer pauses or un-pauses video transmission. - * - * @param call The call which paused or un-paused video transmission. - * @param paused {@code True} when the video transmission is paused, {@code false} - * otherwise. - */ - public void onPeerPauseStateChanged(Call call, boolean paused); - - /** - * Called when the video quality changes. - * - * @param call The call whose video quality changes. - * @param videoCallQuality - values are QUALITY_HIGH, MEDIUM, LOW and UNKNOWN. - */ - public void onVideoQualityChanged(Call call, int videoCallQuality); - - /* - * Called when call data usage value is requested or when call data usage value is updated - * because of a call state change - * - * @param dataUsage call data usage value - */ - public void onCallDataUsageChange(long dataUsage); - - /** - * Called when call session event is raised. - * - * @param event The call session event. - */ - public void onCallSessionEvent(int event); - } - - /** - * Listener interface for any class that wants to be notified of changes to the video surfaces. - */ - public interface SurfaceChangeListener { - /** - * Called when the peer video feed changes dimensions. This can occur when the peer rotates - * their device, changing the aspect ratio of the video signal. - * - * @param call The call which experienced a peer video - * @param width - * @param height - */ - public void onUpdatePeerDimensions(Call call, int width, int height); - - /** - * Called when the local camera changes dimensions. This occurs when a change in camera - * occurs. - * - * @param call The call which experienced the camera dimension change. - * @param width The new camera video width. - * @param height The new camera video height. - */ - public void onCameraDimensionsChange(Call call, int width, int height); - } -} diff --git a/InCallUI/src/com/android/incallui/LatencyReport.java b/InCallUI/src/com/android/incallui/LatencyReport.java deleted file mode 100644 index 655372a8f..000000000 --- a/InCallUI/src/com/android/incallui/LatencyReport.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.os.Bundle; -import android.os.SystemClock; - -import com.android.incalluibind.ObjectFactory; - -/** - * Tracks latency information for a call. - */ -public class LatencyReport { - // The following are hidden constants from android.telecom.TelecomManager. - private static final String EXTRA_CALL_CREATED_TIME_MILLIS = - "android.telecom.extra.CALL_CREATED_TIME_MILLIS"; - private static final String EXTRA_CALL_TELECOM_ROUTING_START_TIME_MILLIS = - "android.telecom.extra.CALL_TELECOM_ROUTING_START_TIME_MILLIS"; - private static final String EXTRA_CALL_TELECOM_ROUTING_END_TIME_MILLIS = - "android.telecom.extra.CALL_TELECOM_ROUTING_END_TIME_MILLIS"; - - public static final long INVALID_TIME = -1; - - private final boolean mWasIncoming; - - // Time elapsed since boot when the call was created by the connection service. - private final long mCreatedTimeMillis; - - // Time elapsed since boot when telecom began processing the call. - private final long mTelecomRoutingStartTimeMillis; - - // Time elapsed since boot when telecom finished processing the call. This includes things like - // looking up contact info and call blocking but before showing any UI. - private final long mTelecomRoutingEndTimeMillis; - - // Time elapsed since boot when the call was added to the InCallUi. - private final long mCallAddedTimeMillis; - - // Time elapsed since boot when the call was added and call blocking evaluation was completed. - private long mCallBlockingTimeMillis = INVALID_TIME; - - // Time elapsed since boot when the call notification was shown. - private long mCallNotificationTimeMillis = INVALID_TIME; - - // Time elapsed since boot when the InCallUI was shown. - private long mInCallUiShownTimeMillis = INVALID_TIME; - - // Whether the call was shown to the user as a heads up notification instead of a full screen - // UI. - private boolean mDidDisplayHeadsUpNotification; - - public LatencyReport() { - mWasIncoming = false; - mCreatedTimeMillis = INVALID_TIME; - mTelecomRoutingStartTimeMillis = INVALID_TIME; - mTelecomRoutingEndTimeMillis = INVALID_TIME; - mCallAddedTimeMillis = SystemClock.elapsedRealtime(); - } - - public LatencyReport(android.telecom.Call telecomCall) { - mWasIncoming = telecomCall.getState() == android.telecom.Call.STATE_RINGING; - Bundle extras = telecomCall.getDetails().getIntentExtras(); - if (extras == null) { - mCreatedTimeMillis = INVALID_TIME; - mTelecomRoutingStartTimeMillis = INVALID_TIME; - mTelecomRoutingEndTimeMillis = INVALID_TIME; - } else { - mCreatedTimeMillis = extras.getLong(EXTRA_CALL_CREATED_TIME_MILLIS, INVALID_TIME); - mTelecomRoutingStartTimeMillis = extras.getLong( - EXTRA_CALL_TELECOM_ROUTING_START_TIME_MILLIS, INVALID_TIME); - mTelecomRoutingEndTimeMillis = extras.getLong( - EXTRA_CALL_TELECOM_ROUTING_END_TIME_MILLIS, INVALID_TIME); - } - mCallAddedTimeMillis = SystemClock.elapsedRealtime(); - } - - public boolean getWasIncoming() { - return mWasIncoming; - } - - public long getCreatedTimeMillis() { - return mCreatedTimeMillis; - } - - public long getTelecomRoutingStartTimeMillis() { - return mTelecomRoutingStartTimeMillis; - } - - public long getTelecomRoutingEndTimeMillis() { - return mTelecomRoutingEndTimeMillis; - } - - public long getCallAddedTimeMillis() { - return mCallAddedTimeMillis; - } - - public long getCallBlockingTimeMillis() { - return mCallBlockingTimeMillis; - } - - public void onCallBlockingDone() { - if (mCallBlockingTimeMillis == INVALID_TIME) { - mCallBlockingTimeMillis = SystemClock.elapsedRealtime(); - } - } - - public long getCallNotificationTimeMillis() { - return mCallNotificationTimeMillis; - } - - public void onNotificationShown() { - if (mCallNotificationTimeMillis == INVALID_TIME) { - mCallNotificationTimeMillis = SystemClock.elapsedRealtime(); - } - } - - public long getInCallUiShownTimeMillis() { - return mInCallUiShownTimeMillis; - } - - public void onInCallUiShown(boolean forFullScreenIntent) { - if (mInCallUiShownTimeMillis == INVALID_TIME) { - mInCallUiShownTimeMillis = SystemClock.elapsedRealtime(); - mDidDisplayHeadsUpNotification = mWasIncoming && !forFullScreenIntent; - } - } - - public boolean getDidDisplayHeadsUpNotification() { - return mDidDisplayHeadsUpNotification; - } -} diff --git a/InCallUI/src/com/android/incallui/Log.java b/InCallUI/src/com/android/incallui/Log.java deleted file mode 100644 index 07a0e61ca..000000000 --- a/InCallUI/src/com/android/incallui/Log.java +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui; - -import android.net.Uri; -import android.telecom.PhoneAccount; -import android.telephony.PhoneNumberUtils; - -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; - -/** - * Manages logging for the entire class. - */ -public class Log { - - // Generic tag for all In Call logging - public static final String TAG = "InCall"; - - public static final boolean FORCE_DEBUG = false; /* STOPSHIP if true */ - public static final boolean DEBUG = FORCE_DEBUG || - android.util.Log.isLoggable(TAG, android.util.Log.DEBUG); - public static final boolean VERBOSE = FORCE_DEBUG || - android.util.Log.isLoggable(TAG, android.util.Log.VERBOSE); - public static final String TAG_DELIMETER = " - "; - - public static void d(String tag, String msg) { - if (DEBUG) { - android.util.Log.d(TAG, delimit(tag) + msg); - } - } - - public static void d(Object obj, String msg) { - if (DEBUG) { - android.util.Log.d(TAG, getPrefix(obj) + msg); - } - } - - public static void d(Object obj, String str1, Object str2) { - if (DEBUG) { - android.util.Log.d(TAG, getPrefix(obj) + str1 + str2); - } - } - - public static void v(Object obj, String msg) { - if (VERBOSE) { - android.util.Log.v(TAG, getPrefix(obj) + msg); - } - } - - public static void v(Object obj, String str1, Object str2) { - if (VERBOSE) { - android.util.Log.d(TAG, getPrefix(obj) + str1 + str2); - } - } - - public static void e(String tag, String msg, Exception e) { - android.util.Log.e(TAG, delimit(tag) + msg, e); - } - - public static void e(String tag, String msg) { - android.util.Log.e(TAG, delimit(tag) + msg); - } - - public static void e(Object obj, String msg, Exception e) { - android.util.Log.e(TAG, getPrefix(obj) + msg, e); - } - - public static void e(Object obj, String msg) { - android.util.Log.e(TAG, getPrefix(obj) + msg); - } - - public static void i(String tag, String msg) { - android.util.Log.i(TAG, delimit(tag) + msg); - } - - public static void i(Object obj, String msg) { - android.util.Log.i(TAG, getPrefix(obj) + msg); - } - - public static void w(Object obj, String msg) { - android.util.Log.w(TAG, getPrefix(obj) + msg); - } - - public static void wtf(Object obj, String msg) { - android.util.Log.wtf(TAG, getPrefix(obj) + msg); - } - - public static String piiHandle(Object pii) { - if (pii == null || VERBOSE) { - return String.valueOf(pii); - } - - if (pii instanceof Uri) { - Uri uri = (Uri) pii; - - // All Uri's which are not "tel" go through normal pii() method. - if (!PhoneAccount.SCHEME_TEL.equals(uri.getScheme())) { - return pii(pii); - } else { - pii = uri.getSchemeSpecificPart(); - } - } - - String originalString = String.valueOf(pii); - StringBuilder stringBuilder = new StringBuilder(originalString.length()); - for (char c : originalString.toCharArray()) { - if (PhoneNumberUtils.isDialable(c)) { - stringBuilder.append('*'); - } else { - stringBuilder.append(c); - } - } - return stringBuilder.toString(); - } - - /** - * Redact personally identifiable information for production users. - * If we are running in verbose mode, return the original string, otherwise - * return a SHA-1 hash of the input string. - */ - public static String pii(Object pii) { - if (pii == null || VERBOSE) { - return String.valueOf(pii); - } - return "[" + secureHash(String.valueOf(pii).getBytes()) + "]"; - } - - private static String secureHash(byte[] input) { - MessageDigest messageDigest; - try { - messageDigest = MessageDigest.getInstance("SHA-1"); - } catch (NoSuchAlgorithmException e) { - return null; - } - messageDigest.update(input); - byte[] result = messageDigest.digest(); - return encodeHex(result); - } - - private static String encodeHex(byte[] bytes) { - StringBuffer hex = new StringBuffer(bytes.length * 2); - - for (int i = 0; i < bytes.length; i++) { - int byteIntValue = bytes[i] & 0xff; - if (byteIntValue < 0x10) { - hex.append("0"); - } - hex.append(Integer.toString(byteIntValue, 16)); - } - - return hex.toString(); - } - - private static String getPrefix(Object obj) { - return (obj == null ? "" : (obj.getClass().getSimpleName() + TAG_DELIMETER)); - } - - private static String delimit(String tag) { - return tag + TAG_DELIMETER; - } -} diff --git a/InCallUI/src/com/android/incallui/NeededForReflection.java b/InCallUI/src/com/android/incallui/NeededForReflection.java deleted file mode 100644 index 363a0a548..000000000 --- a/InCallUI/src/com/android/incallui/NeededForReflection.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Denotes that the class, constructor, method or field is used for reflection and therefore cannot - * be removed by tools like ProGuard. - */ -@Retention(RetentionPolicy.CLASS) -@Target({ElementType.TYPE, ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.FIELD}) -public @interface NeededForReflection {} diff --git a/InCallUI/src/com/android/incallui/NotificationBroadcastReceiver.java b/InCallUI/src/com/android/incallui/NotificationBroadcastReceiver.java deleted file mode 100644 index 27f71159d..000000000 --- a/InCallUI/src/com/android/incallui/NotificationBroadcastReceiver.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.telecom.VideoProfile; - -/** - * Accepts broadcast Intents which will be prepared by {@link StatusBarNotifier} and thus - * sent from the notification manager. - * This should be visible from outside, but shouldn't be exported. - */ -public class NotificationBroadcastReceiver extends BroadcastReceiver { - - /** - * Intent Action used for hanging up the current call from Notification bar. This will - * choose first ringing call, first active call, or first background call (typically in - * STATE_HOLDING state). - */ - public static final String ACTION_DECLINE_INCOMING_CALL = - "com.android.incallui.ACTION_DECLINE_INCOMING_CALL"; - public static final String ACTION_HANG_UP_ONGOING_CALL = - "com.android.incallui.ACTION_HANG_UP_ONGOING_CALL"; - public static final String ACTION_ANSWER_VIDEO_INCOMING_CALL = - "com.android.incallui.ACTION_ANSWER_VIDEO_INCOMING_CALL"; - public static final String ACTION_ANSWER_VOICE_INCOMING_CALL = - "com.android.incallui.ACTION_ANSWER_VOICE_INCOMING_CALL"; - public static final String ACTION_ACCEPT_VIDEO_UPGRADE_REQUEST = - "com.android.incallui.ACTION_ACCEPT_VIDEO_UPGRADE_REQUEST"; - public static final String ACTION_DECLINE_VIDEO_UPGRADE_REQUEST = - "com.android.incallui.ACTION_DECLINE_VIDEO_UPGRADE_REQUEST"; - public static final String ACTION_PULL_EXTERNAL_CALL = - "com.android.incallui.ACTION_PULL_EXTERNAL_CALL"; - public static final String EXTRA_NOTIFICATION_ID = - "com.android.incallui.extra.EXTRA_NOTIFICATION_ID"; - - @Override - public void onReceive(Context context, Intent intent) { - final String action = intent.getAction(); - Log.i(this, "Broadcast from Notification: " + action); - - // TODO: Commands of this nature should exist in the CallList. - if (action.equals(ACTION_ANSWER_VIDEO_INCOMING_CALL)) { - InCallPresenter.getInstance().answerIncomingCall( - context, VideoProfile.STATE_BIDIRECTIONAL); - } else if (action.equals(ACTION_ANSWER_VOICE_INCOMING_CALL)) { - InCallPresenter.getInstance().answerIncomingCall( - context, VideoProfile.STATE_AUDIO_ONLY); - } else if (action.equals(ACTION_DECLINE_INCOMING_CALL)) { - InCallPresenter.getInstance().declineIncomingCall(context); - } else if (action.equals(ACTION_HANG_UP_ONGOING_CALL)) { - InCallPresenter.getInstance().hangUpOngoingCall(context); - } else if (action.equals(ACTION_ACCEPT_VIDEO_UPGRADE_REQUEST)) { - //TODO: Change calltype after adding support for TX and RX - InCallPresenter.getInstance().acceptUpgradeRequest( - VideoProfile.STATE_BIDIRECTIONAL, context); - } else if (action.equals(ACTION_DECLINE_VIDEO_UPGRADE_REQUEST)) { - InCallPresenter.getInstance().declineUpgradeRequest(context); - } else if (action.equals(ACTION_PULL_EXTERNAL_CALL)) { - int notificationId = intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1); - InCallPresenter.getInstance().getExternalCallNotifier() - .pullExternalCall(notificationId); - } - } - -} diff --git a/InCallUI/src/com/android/incallui/PostCharDialogFragment.java b/InCallUI/src/com/android/incallui/PostCharDialogFragment.java deleted file mode 100644 index 6f904ad9e..000000000 --- a/InCallUI/src/com/android/incallui/PostCharDialogFragment.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui; - -import android.app.AlertDialog; -import android.app.Dialog; -import android.app.DialogFragment; -import android.content.DialogInterface; -import android.os.Bundle; -import android.view.WindowManager; - -import com.android.dialer.R; - -/** - * Pop up an alert dialog with OK and Cancel buttons to allow user to Accept or Reject the WAIT - * inserted as part of the Dial string. - */ -public class PostCharDialogFragment extends DialogFragment { - - private static final String STATE_CALL_ID = "CALL_ID"; - private static final String STATE_POST_CHARS = "POST_CHARS"; - - private String mCallId; - private String mPostDialStr; - - public PostCharDialogFragment() { - } - - public PostCharDialogFragment(String callId, String postDialStr) { - mCallId = callId; - mPostDialStr = postDialStr; - } - - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - super.onCreateDialog(savedInstanceState); - - if (mPostDialStr == null && savedInstanceState != null) { - mCallId = savedInstanceState.getString(STATE_CALL_ID); - mPostDialStr = savedInstanceState.getString(STATE_POST_CHARS); - } - - final StringBuilder buf = new StringBuilder(); - buf.append(getResources().getText(R.string.wait_prompt_str)); - buf.append(mPostDialStr); - - final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - builder.setMessage(buf.toString()); - - builder.setPositiveButton(R.string.pause_prompt_yes, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int whichButton) { - TelecomAdapter.getInstance().postDialContinue(mCallId, true); - } - }); - builder.setNegativeButton(R.string.pause_prompt_no, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int whichButton) { - dialog.cancel(); - } - }); - - final AlertDialog dialog = builder.create(); - return dialog; - } - - @Override - public void onCancel(DialogInterface dialog) { - super.onCancel(dialog); - - TelecomAdapter.getInstance().postDialContinue(mCallId, false); - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - - outState.putString(STATE_CALL_ID, mCallId); - outState.putString(STATE_POST_CHARS, mPostDialStr); - } -} diff --git a/InCallUI/src/com/android/incallui/Presenter.java b/InCallUI/src/com/android/incallui/Presenter.java deleted file mode 100644 index 4e1fa978d..000000000 --- a/InCallUI/src/com/android/incallui/Presenter.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.os.Bundle; - -/** - * Base class for Presenters. - */ -public abstract class Presenter { - - private U mUi; - - /** - * Called after the UI view has been created. That is when fragment.onViewCreated() is called. - * - * @param ui The Ui implementation that is now ready to be used. - */ - public void onUiReady(U ui) { - mUi = ui; - } - - /** - * Called when the UI view is destroyed in Fragment.onDestroyView(). - */ - public final void onUiDestroy(U ui) { - onUiUnready(ui); - mUi = null; - } - - /** - * To be overriden by Presenter implementations. Called when the fragment is being - * destroyed but before ui is set to null. - */ - public void onUiUnready(U ui) { - } - - public void onSaveInstanceState(Bundle outState) {} - - public void onRestoreInstanceState(Bundle savedInstanceState) {} - - public U getUi() { - return mUi; - } -} diff --git a/InCallUI/src/com/android/incallui/ProximitySensor.java b/InCallUI/src/com/android/incallui/ProximitySensor.java deleted file mode 100644 index 3c9fd9370..000000000 --- a/InCallUI/src/com/android/incallui/ProximitySensor.java +++ /dev/null @@ -1,317 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import com.google.common.base.Objects; - -import android.content.Context; -import android.content.res.Configuration; -import android.hardware.display.DisplayManager; -import android.hardware.display.DisplayManager.DisplayListener; -import android.os.PowerManager; -import android.telecom.CallAudioState; -import android.view.Display; - -import com.android.incallui.AudioModeProvider.AudioModeListener; -import com.android.incallui.InCallPresenter.InCallState; -import com.android.incallui.InCallPresenter.InCallStateListener; - -/** - * Class manages the proximity sensor for the in-call UI. - * We enable the proximity sensor while the user in a phone call. The Proximity sensor turns off - * the touchscreen and display when the user is close to the screen to prevent user's cheek from - * causing touch events. - * The class requires special knowledge of the activity and device state to know when the proximity - * sensor should be enabled and disabled. Most of that state is fed into this class through - * public methods. - */ -public class ProximitySensor implements AccelerometerListener.OrientationListener, - InCallStateListener, AudioModeListener { - private static final String TAG = ProximitySensor.class.getSimpleName(); - - private final PowerManager mPowerManager; - private final PowerManager.WakeLock mProximityWakeLock; - private final AudioModeProvider mAudioModeProvider; - private final AccelerometerListener mAccelerometerListener; - private final ProximityDisplayListener mDisplayListener; - private int mOrientation = AccelerometerListener.ORIENTATION_UNKNOWN; - private boolean mUiShowing = false; - private boolean mIsPhoneOffhook = false; - private boolean mDialpadVisible; - - // True if the keyboard is currently *not* hidden - // Gets updated whenever there is a Configuration change - private boolean mIsHardKeyboardOpen; - - public ProximitySensor(Context context, AudioModeProvider audioModeProvider, - AccelerometerListener accelerometerListener) { - mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); - if (mPowerManager.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK)) { - mProximityWakeLock = mPowerManager.newWakeLock( - PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, TAG); - } else { - Log.w(TAG, "Device does not support proximity wake lock."); - mProximityWakeLock = null; - } - mAccelerometerListener = accelerometerListener; - mAccelerometerListener.setListener(this); - - mDisplayListener = new ProximityDisplayListener( - (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE)); - mDisplayListener.register(); - - mAudioModeProvider = audioModeProvider; - mAudioModeProvider.addListener(this); - } - - public void tearDown() { - mAudioModeProvider.removeListener(this); - - mAccelerometerListener.enable(false); - mDisplayListener.unregister(); - - turnOffProximitySensor(true); - } - - /** - * Called to identify when the device is laid down flat. - */ - @Override - public void orientationChanged(int orientation) { - mOrientation = orientation; - updateProximitySensorMode(); - } - - /** - * Called to keep track of the overall UI state. - */ - @Override - public void onStateChange(InCallState oldState, InCallState newState, CallList callList) { - // We ignore incoming state because we do not want to enable proximity - // sensor during incoming call screen. We check hasLiveCall() because a disconnected call - // can also put the in-call screen in the INCALL state. - boolean hasOngoingCall = InCallState.INCALL == newState && callList.hasLiveCall(); - boolean isOffhook = (InCallState.OUTGOING == newState) || hasOngoingCall; - - if (isOffhook != mIsPhoneOffhook) { - mIsPhoneOffhook = isOffhook; - - mOrientation = AccelerometerListener.ORIENTATION_UNKNOWN; - mAccelerometerListener.enable(mIsPhoneOffhook); - - updateProximitySensorMode(); - } - } - - @Override - public void onSupportedAudioMode(int modeMask) { - } - - @Override - public void onMute(boolean muted) { - } - - /** - * Called when the audio mode changes during a call. - */ - @Override - public void onAudioMode(int mode) { - updateProximitySensorMode(); - } - - public void onDialpadVisible(boolean visible) { - mDialpadVisible = visible; - updateProximitySensorMode(); - } - - /** - * Called by InCallActivity to listen for hard keyboard events. - */ - public void onConfigurationChanged(Configuration newConfig) { - mIsHardKeyboardOpen = newConfig.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_NO; - - // Update the Proximity sensor based on keyboard state - updateProximitySensorMode(); - } - - /** - * Used to save when the UI goes in and out of the foreground. - */ - public void onInCallShowing(boolean showing) { - if (showing) { - mUiShowing = true; - - // We only consider the UI not showing for instances where another app took the foreground. - // If we stopped showing because the screen is off, we still consider that showing. - } else if (mPowerManager.isScreenOn()) { - mUiShowing = false; - } - updateProximitySensorMode(); - } - - void onDisplayStateChanged(boolean isDisplayOn) { - Log.i(this, "isDisplayOn: " + isDisplayOn); - mAccelerometerListener.enable(isDisplayOn); - } - - /** - * TODO: There is no way to determine if a screen is off due to proximity or if it is - * legitimately off, but if ever we can do that in the future, it would be useful here. - * Until then, this function will simply return true of the screen is off. - * TODO: Investigate whether this can be replaced with the ProximityDisplayListener. - */ - public boolean isScreenReallyOff() { - return !mPowerManager.isScreenOn(); - } - - private void turnOnProximitySensor() { - if (mProximityWakeLock != null) { - if (!mProximityWakeLock.isHeld()) { - Log.i(this, "Acquiring proximity wake lock"); - mProximityWakeLock.acquire(); - } else { - Log.i(this, "Proximity wake lock already acquired"); - } - } - } - - private void turnOffProximitySensor(boolean screenOnImmediately) { - if (mProximityWakeLock != null) { - if (mProximityWakeLock.isHeld()) { - Log.i(this, "Releasing proximity wake lock"); - int flags = - (screenOnImmediately ? 0 : PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY); - mProximityWakeLock.release(flags); - } else { - Log.i(this, "Proximity wake lock already released"); - } - } - } - - /** - * Updates the wake lock used to control proximity sensor behavior, - * based on the current state of the phone. - * - * On devices that have a proximity sensor, to avoid false touches - * during a call, we hold a PROXIMITY_SCREEN_OFF_WAKE_LOCK wake lock - * whenever the phone is off hook. (When held, that wake lock causes - * the screen to turn off automatically when the sensor detects an - * object close to the screen.) - * - * This method is a no-op for devices that don't have a proximity - * sensor. - * - * Proximity wake lock will *not* be held if any one of the - * conditions is true while on a call: - * 1) If the audio is routed via Bluetooth - * 2) If a wired headset is connected - * 3) if the speaker is ON - * 4) If the slider is open(i.e. the hardkeyboard is *not* hidden) - */ - private synchronized void updateProximitySensorMode() { - final int audioMode = mAudioModeProvider.getAudioMode(); - - // turn proximity sensor off and turn screen on immediately if - // we are using a headset, the keyboard is open, or the device - // is being held in a horizontal position. - boolean screenOnImmediately = (CallAudioState.ROUTE_WIRED_HEADSET == audioMode - || CallAudioState.ROUTE_SPEAKER == audioMode - || CallAudioState.ROUTE_BLUETOOTH == audioMode - || mIsHardKeyboardOpen); - - // We do not keep the screen off when the user is outside in-call screen and we are - // horizontal, but we do not force it on when we become horizontal until the - // proximity sensor goes negative. - final boolean horizontal = - (mOrientation == AccelerometerListener.ORIENTATION_HORIZONTAL); - screenOnImmediately |= !mUiShowing && horizontal; - - // We do not keep the screen off when dialpad is visible, we are horizontal, and - // the in-call screen is being shown. - // At that moment we're pretty sure users want to use it, instead of letting the - // proximity sensor turn off the screen by their hands. - screenOnImmediately |= mDialpadVisible && horizontal; - - Log.v(this, "screenonImmediately: ", screenOnImmediately); - - Log.i(this, Objects.toStringHelper(this) - .add("keybrd", mIsHardKeyboardOpen ? 1 : 0) - .add("dpad", mDialpadVisible ? 1 : 0) - .add("offhook", mIsPhoneOffhook ? 1 : 0) - .add("hor", horizontal ? 1 : 0) - .add("ui", mUiShowing ? 1 : 0) - .add("aud", CallAudioState.audioRouteToString(audioMode)) - .toString()); - - if (mIsPhoneOffhook && !screenOnImmediately) { - Log.d(this, "Turning on proximity sensor"); - // Phone is in use! Arrange for the screen to turn off - // automatically when the sensor detects a close object. - turnOnProximitySensor(); - } else { - Log.d(this, "Turning off proximity sensor"); - // Phone is either idle, or ringing. We don't want any special proximity sensor - // behavior in either case. - turnOffProximitySensor(screenOnImmediately); - } - } - - /** - * Implementation of a {@link DisplayListener} that maintains a binary state: - * Screen on vs screen off. Used by the proximity sensor manager to decide whether or not - * it needs to listen to accelerometer events. - */ - public class ProximityDisplayListener implements DisplayListener { - private DisplayManager mDisplayManager; - private boolean mIsDisplayOn = true; - - ProximityDisplayListener(DisplayManager displayManager) { - mDisplayManager = displayManager; - } - - void register() { - mDisplayManager.registerDisplayListener(this, null); - } - - void unregister() { - mDisplayManager.unregisterDisplayListener(this); - } - - @Override - public void onDisplayRemoved(int displayId) { - } - - @Override - public void onDisplayChanged(int displayId) { - if (displayId == Display.DEFAULT_DISPLAY) { - final Display display = mDisplayManager.getDisplay(displayId); - - final boolean isDisplayOn = display.getState() != Display.STATE_OFF; - // For call purposes, we assume that as long as the screen is not truly off, it is - // considered on, even if it is in an unknown or low power idle state. - if (isDisplayOn != mIsDisplayOn) { - mIsDisplayOn = isDisplayOn; - onDisplayStateChanged(mIsDisplayOn); - } - } - } - - @Override - public void onDisplayAdded(int displayId) { - } - } -} diff --git a/InCallUI/src/com/android/incallui/StatusBarNotifier.java b/InCallUI/src/com/android/incallui/StatusBarNotifier.java deleted file mode 100644 index cc87dd414..000000000 --- a/InCallUI/src/com/android/incallui/StatusBarNotifier.java +++ /dev/null @@ -1,793 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui; - -import static com.android.contacts.common.compat.CallSdkCompat.Details.PROPERTY_ENTERPRISE_CALL; -import static com.android.incallui.NotificationBroadcastReceiver.ACTION_ACCEPT_VIDEO_UPGRADE_REQUEST; -import static com.android.incallui.NotificationBroadcastReceiver.ACTION_ANSWER_VIDEO_INCOMING_CALL; -import static com.android.incallui.NotificationBroadcastReceiver.ACTION_ANSWER_VOICE_INCOMING_CALL; -import static com.android.incallui.NotificationBroadcastReceiver.ACTION_DECLINE_INCOMING_CALL; -import static com.android.incallui.NotificationBroadcastReceiver.ACTION_DECLINE_VIDEO_UPGRADE_REQUEST; -import static com.android.incallui.NotificationBroadcastReceiver.ACTION_HANG_UP_ONGOING_CALL; - -import com.google.common.base.Preconditions; - -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.Canvas; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.media.AudioAttributes; -import android.net.Uri; -import android.provider.ContactsContract.Contacts; -import android.support.annotation.Nullable; -import android.telecom.Call.Details; -import android.telecom.PhoneAccount; -import android.telecom.TelecomManager; -import android.text.BidiFormatter; -import android.text.TextDirectionHeuristics; -import android.text.TextUtils; - -import com.android.contacts.common.ContactsUtils; -import com.android.contacts.common.ContactsUtils.UserType; -import com.android.contacts.common.preference.ContactsPreferences; -import com.android.contacts.common.testing.NeededForTesting; -import com.android.contacts.common.util.BitmapUtil; -import com.android.contacts.common.util.ContactDisplayUtils; -import com.android.dialer.R; -import com.android.dialer.service.ExtendedCallInfoService; -import com.android.incallui.ContactInfoCache.ContactCacheEntry; -import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback; -import com.android.incallui.InCallPresenter.InCallState; -import com.android.incallui.async.PausableExecutorImpl; -import com.android.incallui.ringtone.DialerRingtoneManager; -import com.android.incallui.ringtone.InCallTonePlayer; -import com.android.incallui.ringtone.ToneGeneratorFactory; - -import java.util.Objects; - -/** - * This class adds Notifications to the status bar for the in-call experience. - */ -public class StatusBarNotifier implements InCallPresenter.InCallStateListener, - CallList.CallUpdateListener { - - // Notification types - // Indicates that no notification is currently showing. - private static final int NOTIFICATION_NONE = 0; - // Notification for an active call. This is non-interruptive, but cannot be dismissed. - private static final int NOTIFICATION_IN_CALL = 1; - // Notification for incoming calls. This is interruptive and will show up as a HUN. - private static final int NOTIFICATION_INCOMING_CALL = 2; - - private static final int PENDING_INTENT_REQUEST_CODE_NON_FULL_SCREEN = 0; - private static final int PENDING_INTENT_REQUEST_CODE_FULL_SCREEN = 1; - - private static final long[] VIBRATE_PATTERN = new long[] {0, 1000, 1000}; - - private final Context mContext; - @Nullable private ContactsPreferences mContactsPreferences; - private final ContactInfoCache mContactInfoCache; - private final NotificationManager mNotificationManager; - private final DialerRingtoneManager mDialerRingtoneManager; - private int mCurrentNotification = NOTIFICATION_NONE; - private int mCallState = Call.State.INVALID; - private int mSavedIcon = 0; - private String mSavedContent = null; - private Bitmap mSavedLargeIcon; - private String mSavedContentTitle; - private String mCallId = null; - private InCallState mInCallState; - private Uri mRingtone; - - public StatusBarNotifier(Context context, ContactInfoCache contactInfoCache) { - Preconditions.checkNotNull(context); - mContext = context; - mContactsPreferences = ContactsPreferencesFactory.newContactsPreferences(mContext); - mContactInfoCache = contactInfoCache; - mNotificationManager = - (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); - mDialerRingtoneManager = new DialerRingtoneManager( - new InCallTonePlayer(new ToneGeneratorFactory(), new PausableExecutorImpl()), - CallList.getInstance()); - mCurrentNotification = NOTIFICATION_NONE; - } - - /** - * Creates notifications according to the state we receive from {@link InCallPresenter}. - */ - @Override - public void onStateChange(InCallState oldState, InCallState newState, CallList callList) { - Log.d(this, "onStateChange"); - mInCallState = newState; - updateNotification(newState, callList); - } - - /** - * Updates the phone app's status bar notification *and* launches the - * incoming call UI in response to a new incoming call. - * - * If an incoming call is ringing (or call-waiting), the notification - * will also include a "fullScreenIntent" that will cause the - * InCallScreen to be launched, unless the current foreground activity - * is marked as "immersive". - * - * (This is the mechanism that actually brings up the incoming call UI - * when we receive a "new ringing connection" event from the telephony - * layer.) - * - * Also note that this method is safe to call even if the phone isn't - * actually ringing (or, more likely, if an incoming call *was* - * ringing briefly but then disconnected). In that case, we'll simply - * update or cancel the in-call notification based on the current - * phone state. - * - * @see #updateInCallNotification(InCallState,CallList) - */ - public void updateNotification(InCallState state, CallList callList) { - updateInCallNotification(state, callList); - } - - /** - * Take down the in-call notification. - * @see #updateInCallNotification(InCallState,CallList) - */ - private void cancelNotification() { - if (!TextUtils.isEmpty(mCallId)) { - CallList.getInstance().removeCallUpdateListener(mCallId, this); - mCallId = null; - } - if (mCurrentNotification != NOTIFICATION_NONE) { - Log.d(this, "cancelInCall()..."); - mNotificationManager.cancel(mCurrentNotification); - } - mCurrentNotification = NOTIFICATION_NONE; - } - - /** - * Should only be called from a irrecoverable state where it is necessary to dismiss all - * notifications. - */ - static void clearAllCallNotifications(Context backupContext) { - Log.i(StatusBarNotifier.class.getSimpleName(), - "Something terrible happened. Clear all InCall notifications"); - - NotificationManager notificationManager = - (NotificationManager) backupContext.getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.cancel(NOTIFICATION_IN_CALL); - notificationManager.cancel(NOTIFICATION_INCOMING_CALL); - } - - /** - * Helper method for updateInCallNotification() and - * updateNotification(): Update the phone app's - * status bar notification based on the current telephony state, or - * cancels the notification if the phone is totally idle. - */ - private void updateInCallNotification(final InCallState state, CallList callList) { - Log.d(this, "updateInCallNotification..."); - - final Call call = getCallToShow(callList); - - if (call != null) { - showNotification(call); - } else { - cancelNotification(); - } - } - - private void showNotification(final Call call) { - final boolean isIncoming = (call.getState() == Call.State.INCOMING || - call.getState() == Call.State.CALL_WAITING); - if (!TextUtils.isEmpty(mCallId)) { - CallList.getInstance().removeCallUpdateListener(mCallId, this); - } - mCallId = call.getId(); - CallList.getInstance().addCallUpdateListener(call.getId(), this); - - // we make a call to the contact info cache to query for supplemental data to what the - // call provides. This includes the contact name and photo. - // This callback will always get called immediately and synchronously with whatever data - // it has available, and may make a subsequent call later (same thread) if it had to - // call into the contacts provider for more data. - mContactInfoCache.findInfo(call, isIncoming, new ContactInfoCacheCallback() { - @Override - public void onContactInfoComplete(String callId, ContactCacheEntry entry) { - Call call = CallList.getInstance().getCallById(callId); - if (call != null) { - call.getLogState().contactLookupResult = entry.contactLookupResult; - buildAndSendNotification(call, entry); - } - } - - @Override - public void onImageLoadComplete(String callId, ContactCacheEntry entry) { - Call call = CallList.getInstance().getCallById(callId); - if (call != null) { - buildAndSendNotification(call, entry); - } - } - - @Override - public void onContactInteractionsInfoComplete(String callId, ContactCacheEntry entry) {} - }); - } - - /** - * Sets up the main Ui for the notification - */ - private void buildAndSendNotification(Call originalCall, ContactCacheEntry contactInfo) { - // This can get called to update an existing notification after contact information has come - // back. However, it can happen much later. Before we continue, we need to make sure that - // the call being passed in is still the one we want to show in the notification. - final Call call = getCallToShow(CallList.getInstance()); - if (call == null || !call.getId().equals(originalCall.getId())) { - return; - } - - final int callState = call.getState(); - // Dont' show as spam if the number is in local contact. - if (contactInfo.contactLookupResult == Call.LogState.LOOKUP_LOCAL_CONTACT) { - call.setSpam(false); - } - - // Check if data has changed; if nothing is different, don't issue another notification. - final int iconResId = getIconToDisplay(call); - Bitmap largeIcon = getLargeIconToDisplay(contactInfo, call); - final String content = - getContentString(call, contactInfo.userType); - final String contentTitle = getContentTitle(contactInfo, call); - - final boolean isVideoUpgradeRequest = call.getSessionModificationState() - == Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST; - final int notificationType; - if (callState == Call.State.INCOMING || callState == Call.State.CALL_WAITING - || isVideoUpgradeRequest) { - notificationType = NOTIFICATION_INCOMING_CALL; - } else { - notificationType = NOTIFICATION_IN_CALL; - } - - if (!checkForChangeAndSaveData(iconResId, content, largeIcon, contentTitle, callState, - notificationType, contactInfo.contactRingtoneUri)) { - return; - } - - if (largeIcon != null) { - largeIcon = getRoundedIcon(largeIcon); - } - - /* - * This builder is used for the notification shown when the device is locked and the user - * has set their notification settings to 'hide sensitive content' - * {@see Notification.Builder#setPublicVersion}. - */ - Notification.Builder publicBuilder = new Notification.Builder(mContext); - publicBuilder.setSmallIcon(iconResId) - .setColor(mContext.getResources().getColor(R.color.dialer_theme_color)) - // Hide work call state for the lock screen notification - .setContentTitle(getContentString(call, ContactsUtils.USER_TYPE_CURRENT)); - setNotificationWhen(call, callState, publicBuilder); - - /* - * Builder for the notification shown when the device is unlocked or the user has set their - * notification settings to 'show all notification content'. - */ - final Notification.Builder builder = getNotificationBuilder(); - builder.setPublicVersion(publicBuilder.build()); - - // Set up the main intent to send the user to the in-call screen - builder.setContentIntent(createLaunchPendingIntent(false /* isFullScreen */)); - - // Set the intent as a full screen intent as well if a call is incoming - if (notificationType == NOTIFICATION_INCOMING_CALL - && !InCallPresenter.getInstance().isShowingInCallUi()) { - configureFullScreenIntent( - builder, createLaunchPendingIntent(true /* isFullScreen */), call); - // Set the notification category for incoming calls - builder.setCategory(Notification.CATEGORY_CALL); - } - - // Set the content - builder.setContentText(content); - builder.setSmallIcon(iconResId); - builder.setContentTitle(contentTitle); - builder.setLargeIcon(largeIcon); - builder.setColor(mContext.getResources().getColor(R.color.dialer_theme_color)); - - if (isVideoUpgradeRequest) { - builder.setUsesChronometer(false); - addDismissUpgradeRequestAction(builder); - addAcceptUpgradeRequestAction(builder); - } else { - createIncomingCallNotification(call, callState, builder); - } - - addPersonReference(builder, contactInfo, call); - - /* - * Fire off the notification - */ - Notification notification = builder.build(); - - if (mDialerRingtoneManager.shouldPlayRingtone(callState, contactInfo.contactRingtoneUri)) { - notification.flags |= Notification.FLAG_INSISTENT; - notification.sound = contactInfo.contactRingtoneUri; - AudioAttributes.Builder audioAttributes = new AudioAttributes.Builder(); - audioAttributes.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC); - audioAttributes.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE); - notification.audioAttributes = audioAttributes.build(); - if (mDialerRingtoneManager.shouldVibrate(mContext.getContentResolver())) { - notification.vibrate = VIBRATE_PATTERN; - } - } - if (mDialerRingtoneManager.shouldPlayCallWaitingTone(callState)) { - Log.v(this, "Playing call waiting tone"); - mDialerRingtoneManager.playCallWaitingTone(); - } - if (mCurrentNotification != notificationType && mCurrentNotification != NOTIFICATION_NONE) { - Log.i(this, "Previous notification already showing - cancelling " - + mCurrentNotification); - mNotificationManager.cancel(mCurrentNotification); - } - - Log.i(this, "Displaying notification for " + notificationType); - mNotificationManager.notify(notificationType, notification); - call.getLatencyReport().onNotificationShown(); - mCurrentNotification = notificationType; - } - - private void createIncomingCallNotification( - Call call, int state, Notification.Builder builder) { - setNotificationWhen(call, state, builder); - - // Add hang up option for any active calls (active | onhold), outgoing calls (dialing). - if (state == Call.State.ACTIVE || - state == Call.State.ONHOLD || - Call.State.isDialing(state)) { - addHangupAction(builder); - } else if (state == Call.State.INCOMING || state == Call.State.CALL_WAITING) { - addDismissAction(builder); - if (call.isVideoCall(mContext)) { - addVoiceAction(builder); - addVideoCallAction(builder); - } else { - addAnswerAction(builder); - } - } - } - - /* - * Sets the notification's when section as needed. For active calls, this is explicitly set as - * the duration of the call. For all other states, the notification will automatically show the - * time at which the notification was created. - */ - private void setNotificationWhen(Call call, int state, Notification.Builder builder) { - if (state == Call.State.ACTIVE) { - builder.setUsesChronometer(true); - builder.setWhen(call.getConnectTimeMillis()); - } else { - builder.setUsesChronometer(false); - } - } - - /** - * Checks the new notification data and compares it against any notification that we - * are already displaying. If the data is exactly the same, we return false so that - * we do not issue a new notification for the exact same data. - */ - private boolean checkForChangeAndSaveData(int icon, String content, Bitmap largeIcon, - String contentTitle, int state, int notificationType, Uri ringtone) { - - // The two are different: - // if new title is not null, it should be different from saved version OR - // if new title is null, the saved version should not be null - final boolean contentTitleChanged = - (contentTitle != null && !contentTitle.equals(mSavedContentTitle)) || - (contentTitle == null && mSavedContentTitle != null); - - // any change means we are definitely updating - boolean retval = (mSavedIcon != icon) || !Objects.equals(mSavedContent, content) - || (mCallState != state) || (mSavedLargeIcon != largeIcon) - || contentTitleChanged || !Objects.equals(mRingtone, ringtone); - - // If we aren't showing a notification right now or the notification type is changing, - // definitely do an update. - if (mCurrentNotification != notificationType) { - if (mCurrentNotification == NOTIFICATION_NONE) { - Log.d(this, "Showing notification for first time."); - } - retval = true; - } - - mSavedIcon = icon; - mSavedContent = content; - mCallState = state; - mSavedLargeIcon = largeIcon; - mSavedContentTitle = contentTitle; - mRingtone = ringtone; - - if (retval) { - Log.d(this, "Data changed. Showing notification"); - } - - return retval; - } - - /** - * Returns the main string to use in the notification. - */ - @NeededForTesting - String getContentTitle(ContactCacheEntry contactInfo, Call call) { - if (call.isConferenceCall() && !call.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE)) { - return mContext.getResources().getString(R.string.card_title_conf_call); - } - - String preferredName = ContactDisplayUtils.getPreferredDisplayName(contactInfo.namePrimary, - contactInfo.nameAlternative, mContactsPreferences); - if (TextUtils.isEmpty(preferredName)) { - return TextUtils.isEmpty(contactInfo.number) ? null : BidiFormatter.getInstance() - .unicodeWrap(contactInfo.number, TextDirectionHeuristics.LTR); - } - return preferredName; - } - - private void addPersonReference(Notification.Builder builder, ContactCacheEntry contactInfo, - Call call) { - // Query {@link Contacts#CONTENT_LOOKUP_URI} directly with work lookup key is not allowed. - // So, do not pass {@link Contacts#CONTENT_LOOKUP_URI} to NotificationManager to avoid - // NotificationManager using it. - if (contactInfo.lookupUri != null && contactInfo.userType != ContactsUtils.USER_TYPE_WORK) { - builder.addPerson(contactInfo.lookupUri.toString()); - } else if (!TextUtils.isEmpty(call.getNumber())) { - builder.addPerson(Uri.fromParts(PhoneAccount.SCHEME_TEL, - call.getNumber(), null).toString()); - } - } - - /** - * Gets a large icon from the contact info object to display in the notification. - */ - private Bitmap getLargeIconToDisplay(ContactCacheEntry contactInfo, Call call) { - Bitmap largeIcon = null; - if (call.isConferenceCall() && !call.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE)) { - largeIcon = BitmapFactory.decodeResource(mContext.getResources(), - R.drawable.img_conference); - } - if (contactInfo.photo != null && (contactInfo.photo instanceof BitmapDrawable)) { - largeIcon = ((BitmapDrawable) contactInfo.photo).getBitmap(); - } - if (call.isSpam()) { - Drawable drawable = mContext.getResources().getDrawable(R.drawable.blocked_contact); - largeIcon = CallCardFragment.drawableToBitmap(drawable); - } - return largeIcon; - } - - private Bitmap getRoundedIcon(Bitmap bitmap) { - if (bitmap == null) { - return null; - } - final int height = (int) mContext.getResources().getDimension( - android.R.dimen.notification_large_icon_height); - final int width = (int) mContext.getResources().getDimension( - android.R.dimen.notification_large_icon_width); - return BitmapUtil.getRoundedBitmap(bitmap, width, height); - } - - /** - * Returns the appropriate icon res Id to display based on the call for which - * we want to display information. - */ - private int getIconToDisplay(Call call) { - // Even if both lines are in use, we only show a single item in - // the expanded Notifications UI. It's labeled "Ongoing call" - // (or "On hold" if there's only one call, and it's on hold.) - // Also, we don't have room to display caller-id info from two - // different calls. So if both lines are in use, display info - // from the foreground call. And if there's a ringing call, - // display that regardless of the state of the other calls. - if (call.getState() == Call.State.ONHOLD) { - return R.drawable.ic_phone_paused_white_24dp; - } else if (call.getSessionModificationState() - == Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) { - return R.drawable.ic_videocam; - } - return R.drawable.ic_call_white_24dp; - } - - /** - * Returns the message to use with the notification. - */ - private String getContentString(Call call, @UserType long userType) { - boolean isIncomingOrWaiting = call.getState() == Call.State.INCOMING || - call.getState() == Call.State.CALL_WAITING; - - if (isIncomingOrWaiting && - call.getNumberPresentation() == TelecomManager.PRESENTATION_ALLOWED) { - - if (!TextUtils.isEmpty(call.getChildNumber())) { - return mContext.getString(R.string.child_number, call.getChildNumber()); - } else if (!TextUtils.isEmpty(call.getCallSubject()) && call.isCallSubjectSupported()) { - return call.getCallSubject(); - } - } - - int resId = R.string.notification_ongoing_call; - if (call.hasProperty(Details.PROPERTY_WIFI)) { - resId = R.string.notification_ongoing_call_wifi; - } - - if (isIncomingOrWaiting) { - if (call.hasProperty(Details.PROPERTY_WIFI)) { - resId = R.string.notification_incoming_call_wifi; - } else { - if (call.isSpam()) { - resId = R.string.notification_incoming_spam_call; - } else { - resId = R.string.notification_incoming_call; - } - } - } else if (call.getState() == Call.State.ONHOLD) { - resId = R.string.notification_on_hold; - } else if (Call.State.isDialing(call.getState())) { - resId = R.string.notification_dialing; - } else if (call.getSessionModificationState() - == Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) { - resId = R.string.notification_requesting_video_call; - } - - // Is the call placed through work connection service. - boolean isWorkCall = call.hasProperty(PROPERTY_ENTERPRISE_CALL); - if(userType == ContactsUtils.USER_TYPE_WORK || isWorkCall) { - resId = getWorkStringFromPersonalString(resId); - } - - return mContext.getString(resId); - } - - private static int getWorkStringFromPersonalString(int resId) { - if (resId == R.string.notification_ongoing_call) { - return R.string.notification_ongoing_work_call; - } else if (resId == R.string.notification_ongoing_call_wifi) { - return R.string.notification_ongoing_work_call_wifi; - } else if (resId == R.string.notification_incoming_call_wifi) { - return R.string.notification_incoming_work_call_wifi; - } else if (resId == R.string.notification_incoming_call) { - return R.string.notification_incoming_work_call; - } else { - return resId; - } - } - - /** - * Gets the most relevant call to display in the notification. - */ - private Call getCallToShow(CallList callList) { - if (callList == null) { - return null; - } - Call call = callList.getIncomingCall(); - if (call == null) { - call = callList.getOutgoingCall(); - } - if (call == null) { - call = callList.getVideoUpgradeRequestCall(); - } - if (call == null) { - call = callList.getActiveOrBackgroundCall(); - } - return call; - } - - private void addAnswerAction(Notification.Builder builder) { - Log.d(this, "Will show \"answer\" action in the incoming call Notification"); - - PendingIntent answerVoicePendingIntent = createNotificationPendingIntent( - mContext, ACTION_ANSWER_VOICE_INCOMING_CALL); - builder.addAction(R.drawable.ic_call_white_24dp, - mContext.getText(R.string.notification_action_answer), - answerVoicePendingIntent); - } - - private void addDismissAction(Notification.Builder builder) { - Log.d(this, "Will show \"dismiss\" action in the incoming call Notification"); - - PendingIntent declinePendingIntent = - createNotificationPendingIntent(mContext, ACTION_DECLINE_INCOMING_CALL); - builder.addAction(R.drawable.ic_close_dk, - mContext.getText(R.string.notification_action_dismiss), - declinePendingIntent); - } - - private void addHangupAction(Notification.Builder builder) { - Log.d(this, "Will show \"hang-up\" action in the ongoing active call Notification"); - - PendingIntent hangupPendingIntent = - createNotificationPendingIntent(mContext, ACTION_HANG_UP_ONGOING_CALL); - builder.addAction(R.drawable.ic_call_end_white_24dp, - mContext.getText(R.string.notification_action_end_call), - hangupPendingIntent); - } - - private void addVideoCallAction(Notification.Builder builder) { - Log.i(this, "Will show \"video\" action in the incoming call Notification"); - - PendingIntent answerVideoPendingIntent = createNotificationPendingIntent( - mContext, ACTION_ANSWER_VIDEO_INCOMING_CALL); - builder.addAction(R.drawable.ic_videocam, - mContext.getText(R.string.notification_action_answer_video), - answerVideoPendingIntent); - } - - private void addVoiceAction(Notification.Builder builder) { - Log.d(this, "Will show \"voice\" action in the incoming call Notification"); - - PendingIntent answerVoicePendingIntent = createNotificationPendingIntent( - mContext, ACTION_ANSWER_VOICE_INCOMING_CALL); - builder.addAction(R.drawable.ic_call_white_24dp, - mContext.getText(R.string.notification_action_answer_voice), - answerVoicePendingIntent); - } - - private void addAcceptUpgradeRequestAction(Notification.Builder builder) { - Log.i(this, "Will show \"accept upgrade\" action in the incoming call Notification"); - - PendingIntent acceptVideoPendingIntent = createNotificationPendingIntent( - mContext, ACTION_ACCEPT_VIDEO_UPGRADE_REQUEST); - builder.addAction(0, mContext.getText(R.string.notification_action_accept), - acceptVideoPendingIntent); - } - - private void addDismissUpgradeRequestAction(Notification.Builder builder) { - Log.i(this, "Will show \"dismiss upgrade\" action in the incoming call Notification"); - - PendingIntent declineVideoPendingIntent = createNotificationPendingIntent( - mContext, ACTION_DECLINE_VIDEO_UPGRADE_REQUEST); - builder.addAction(0, mContext.getText(R.string.notification_action_dismiss), - declineVideoPendingIntent); - } - - /** - * Adds fullscreen intent to the builder. - */ - private void configureFullScreenIntent(Notification.Builder builder, PendingIntent intent, - Call call) { - // Ok, we actually want to launch the incoming call - // UI at this point (in addition to simply posting a notification - // to the status bar). Setting fullScreenIntent will cause - // the InCallScreen to be launched immediately *unless* the - // current foreground activity is marked as "immersive". - Log.d(this, "- Setting fullScreenIntent: " + intent); - builder.setFullScreenIntent(intent, true); - - // Ugly hack alert: - // - // The NotificationManager has the (undocumented) behavior - // that it will *ignore* the fullScreenIntent field if you - // post a new Notification that matches the ID of one that's - // already active. Unfortunately this is exactly what happens - // when you get an incoming call-waiting call: the - // "ongoing call" notification is already visible, so the - // InCallScreen won't get launched in this case! - // (The result: if you bail out of the in-call UI while on a - // call and then get a call-waiting call, the incoming call UI - // won't come up automatically.) - // - // The workaround is to just notice this exact case (this is a - // call-waiting call *and* the InCallScreen is not in the - // foreground) and manually cancel the in-call notification - // before (re)posting it. - // - // TODO: there should be a cleaner way of avoiding this - // problem (see discussion in bug 3184149.) - - // If a call is onhold during an incoming call, the call actually comes in as - // INCOMING. For that case *and* traditional call-waiting, we want to - // cancel the notification. - boolean isCallWaiting = (call.getState() == Call.State.CALL_WAITING || - (call.getState() == Call.State.INCOMING && - CallList.getInstance().getBackgroundCall() != null)); - - if (isCallWaiting) { - Log.i(this, "updateInCallNotification: call-waiting! force relaunch..."); - // Cancel the IN_CALL_NOTIFICATION immediately before - // (re)posting it; this seems to force the - // NotificationManager to launch the fullScreenIntent. - mNotificationManager.cancel(NOTIFICATION_IN_CALL); - } - } - - private Notification.Builder getNotificationBuilder() { - final Notification.Builder builder = new Notification.Builder(mContext); - builder.setOngoing(true); - - // Make the notification prioritized over the other normal notifications. - builder.setPriority(Notification.PRIORITY_HIGH); - - return builder; - } - - private PendingIntent createLaunchPendingIntent(boolean isFullScreen) { - Intent intent = InCallPresenter.getInstance().getInCallIntent( - false /* showDialpad */, false /* newOutgoingCall */); - - int requestCode = PENDING_INTENT_REQUEST_CODE_NON_FULL_SCREEN; - if (isFullScreen) { - intent.putExtra(InCallActivity.FOR_FULL_SCREEN_INTENT, true); - // Use a unique request code so that the pending intent isn't clobbered by the - // non-full screen pending intent. - requestCode = PENDING_INTENT_REQUEST_CODE_FULL_SCREEN; - } - - // PendingIntent that can be used to launch the InCallActivity. The - // system fires off this intent if the user pulls down the windowshade - // and clicks the notification's expanded view. It's also used to - // launch the InCallActivity immediately when when there's an incoming - // call (see the "fullScreenIntent" field below). - return PendingIntent.getActivity(mContext, requestCode, intent, 0); - } - - /** - * Returns PendingIntent for answering a phone call. This will typically be used from - * Notification context. - */ - private static PendingIntent createNotificationPendingIntent(Context context, String action) { - final Intent intent = new Intent(action, null, - context, NotificationBroadcastReceiver.class); - return PendingIntent.getBroadcast(context, 0, intent, 0); - } - - @Override - public void onCallChanged(Call call) { - if (CallList.getInstance().getIncomingCall() == null) { - mDialerRingtoneManager.stopCallWaitingTone(); - } - } - - /** - * Responds to changes in the session modification state for the call by dismissing the - * status bar notification as required. - * - * @param sessionModificationState The new session modification state. - */ - @Override - public void onSessionModificationStateChange(int sessionModificationState) { - if (sessionModificationState == Call.SessionModificationState.NO_REQUEST) { - if (mCallId != null) { - CallList.getInstance().removeCallUpdateListener(mCallId, this); - } - - updateNotification(mInCallState, CallList.getInstance()); - } - } - - @Override - public void onLastForwardedNumberChange() { - // no-op - } - - @Override - public void onChildNumberChange() { - // no-op - } -} diff --git a/InCallUI/src/com/android/incallui/TelecomAdapter.java b/InCallUI/src/com/android/incallui/TelecomAdapter.java deleted file mode 100644 index f172270dd..000000000 --- a/InCallUI/src/com/android/incallui/TelecomAdapter.java +++ /dev/null @@ -1,226 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import com.google.common.base.Preconditions; - -import android.content.ActivityNotFoundException; -import android.content.Intent; -import android.os.Looper; -import android.telecom.InCallService; -import android.telecom.PhoneAccountHandle; - -import java.util.List; - -final class TelecomAdapter implements InCallServiceListener { - private static final String ADD_CALL_MODE_KEY = "add_call_mode"; - - private static TelecomAdapter sInstance; - private InCallService mInCallService; - - static TelecomAdapter getInstance() { - Preconditions.checkState(Looper.getMainLooper().getThread() == Thread.currentThread()); - if (sInstance == null) { - sInstance = new TelecomAdapter(); - } - return sInstance; - } - - private TelecomAdapter() { - } - - @Override - public void setInCallService(InCallService inCallService) { - mInCallService = inCallService; - } - - @Override - public void clearInCallService() { - mInCallService = null; - } - - private android.telecom.Call getTelecomCallById(String callId) { - Call call = CallList.getInstance().getCallById(callId); - return call == null ? null : call.getTelecomCall(); - } - - void answerCall(String callId, int videoState) { - android.telecom.Call call = getTelecomCallById(callId); - if (call != null) { - call.answer(videoState); - } else { - Log.e(this, "error answerCall, call not in call list: " + callId); - } - } - - void rejectCall(String callId, boolean rejectWithMessage, String message) { - android.telecom.Call call = getTelecomCallById(callId); - if (call != null) { - call.reject(rejectWithMessage, message); - } else { - Log.e(this, "error rejectCall, call not in call list: " + callId); - } - } - - void disconnectCall(String callId) { - android.telecom.Call call = getTelecomCallById(callId); - if (call != null) { - call.disconnect(); - } else { - Log.e(this, "error disconnectCall, call not in call list " + callId); - } - } - - void holdCall(String callId) { - android.telecom.Call call = getTelecomCallById(callId); - if (call != null) { - call.hold(); - } else { - Log.e(this, "error holdCall, call not in call list " + callId); - } - } - - void unholdCall(String callId) { - android.telecom.Call call = getTelecomCallById(callId); - if (call != null) { - call.unhold(); - } else { - Log.e(this, "error unholdCall, call not in call list " + callId); - } - } - - void mute(boolean shouldMute) { - if (mInCallService != null) { - mInCallService.setMuted(shouldMute); - } else { - Log.e(this, "error mute, mInCallService is null"); - } - } - - void setAudioRoute(int route) { - if (mInCallService != null) { - mInCallService.setAudioRoute(route); - } else { - Log.e(this, "error setAudioRoute, mInCallService is null"); - } - } - - void separateCall(String callId) { - android.telecom.Call call = getTelecomCallById(callId); - if (call != null) { - call.splitFromConference(); - } else { - Log.e(this, "error separateCall, call not in call list " + callId); - } - } - - void merge(String callId) { - android.telecom.Call call = getTelecomCallById(callId); - if (call != null) { - List conferenceable = call.getConferenceableCalls(); - if (!conferenceable.isEmpty()) { - call.conference(conferenceable.get(0)); - } else { - if (call.getDetails().can(android.telecom.Call.Details.CAPABILITY_MERGE_CONFERENCE)) { - call.mergeConference(); - } - } - } else { - Log.e(this, "error merge, call not in call list " + callId); - } - } - - void swap(String callId) { - android.telecom.Call call = getTelecomCallById(callId); - if (call != null) { - if (call.getDetails().can(android.telecom.Call.Details.CAPABILITY_SWAP_CONFERENCE)) { - call.swapConference(); - } - } else { - Log.e(this, "error swap, call not in call list " + callId); - } - } - - void addCall() { - if (mInCallService != null) { - Intent intent = new Intent(Intent.ACTION_DIAL); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - - // when we request the dialer come up, we also want to inform - // it that we're going through the "add call" option from the - // InCallScreen / PhoneUtils. - intent.putExtra(ADD_CALL_MODE_KEY, true); - try { - Log.d(this, "Sending the add Call intent"); - mInCallService.startActivity(intent); - } catch (ActivityNotFoundException e) { - // This is rather rare but possible. - // Note: this method is used even when the phone is encrypted. At that moment - // the system may not find any Activity which can accept this Intent. - Log.e(this, "Activity for adding calls isn't found.", e); - } - } - } - - void playDtmfTone(String callId, char digit) { - android.telecom.Call call = getTelecomCallById(callId); - if (call != null) { - call.playDtmfTone(digit); - } else { - Log.e(this, "error playDtmfTone, call not in call list " + callId); - } - } - - void stopDtmfTone(String callId) { - android.telecom.Call call = getTelecomCallById(callId); - if (call != null) { - call.stopDtmfTone(); - } else { - Log.e(this, "error stopDtmfTone, call not in call list " + callId); - } - } - - void postDialContinue(String callId, boolean proceed) { - android.telecom.Call call = getTelecomCallById(callId); - if (call != null) { - call.postDialContinue(proceed); - } else { - Log.e(this, "error postDialContinue, call not in call list " + callId); - } - } - - void phoneAccountSelected(String callId, PhoneAccountHandle accountHandle, boolean setDefault) { - if (accountHandle == null) { - Log.e(this, "error phoneAccountSelected, accountHandle is null"); - // TODO: Do we really want to send null accountHandle? - } - - android.telecom.Call call = getTelecomCallById(callId); - if (call != null) { - call.phoneAccountSelected(accountHandle, setDefault); - } else { - Log.e(this, "error phoneAccountSelected, call not in call list " + callId); - } - } - - boolean canAddCall() { - if (mInCallService != null) { - return mInCallService.canAddCall(); - } - return false; - } -} diff --git a/InCallUI/src/com/android/incallui/Ui.java b/InCallUI/src/com/android/incallui/Ui.java deleted file mode 100644 index e453ccb1c..000000000 --- a/InCallUI/src/com/android/incallui/Ui.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -/** - * Base class for all presenter ui. - */ -public interface Ui { - -} diff --git a/InCallUI/src/com/android/incallui/VideoCallFragment.java b/InCallUI/src/com/android/incallui/VideoCallFragment.java deleted file mode 100644 index 6a46a423d..000000000 --- a/InCallUI/src/com/android/incallui/VideoCallFragment.java +++ /dev/null @@ -1,901 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.graphics.Matrix; -import android.graphics.Point; -import android.graphics.SurfaceTexture; -import android.os.Bundle; -import android.view.Display; -import android.view.LayoutInflater; -import android.view.Surface; -import android.view.TextureView; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewStub; -import android.view.ViewTreeObserver; -import android.widget.FrameLayout; -import android.widget.ImageView; - -import com.android.dialer.R; -import com.android.phone.common.animation.AnimUtils; -import com.google.common.base.Objects; - -/** - * Fragment containing video calling surfaces. - */ -public class VideoCallFragment extends BaseFragment implements VideoCallPresenter.VideoCallUi { - private static final String TAG = VideoCallFragment.class.getSimpleName(); - private static final boolean DEBUG = false; - - /** - * Used to indicate that the surface dimensions are not set. - */ - private static final int DIMENSIONS_NOT_SET = -1; - - /** - * Surface ID for the display surface. - */ - public static final int SURFACE_DISPLAY = 1; - - /** - * Surface ID for the preview surface. - */ - public static final int SURFACE_PREVIEW = 2; - - /** - * Used to indicate that the UI rotation is unknown. - */ - public static final int ORIENTATION_UNKNOWN = -1; - - // Static storage used to retain the video surfaces across Activity restart. - // TextureViews are not parcelable, so it is not possible to store them in the saved state. - private static boolean sVideoSurfacesInUse = false; - private static VideoCallSurface sPreviewSurface = null; - private static VideoCallSurface sDisplaySurface = null; - private static Point sDisplaySize = null; - - /** - * {@link ViewStub} holding the video call surfaces. This is the parent for the - * {@link VideoCallFragment}. Used to ensure that the video surfaces are only inflated when - * required. - */ - private ViewStub mVideoViewsStub; - - /** - * Inflated view containing the video call surfaces represented by the {@link ViewStub}. - */ - private View mVideoViews; - - /** - * The {@link FrameLayout} containing the preview surface. - */ - private View mPreviewVideoContainer; - - /** - * Icon shown to indicate that the outgoing camera has been turned off. - */ - private View mCameraOff; - - /** - * {@link ImageView} containing the user's profile photo. - */ - private ImageView mPreviewPhoto; - - /** - * {@code True} when the layout of the activity has been completed. - */ - private boolean mIsLayoutComplete = false; - - /** - * {@code True} if in landscape mode. - */ - private boolean mIsLandscape; - - private int mAnimationDuration; - - /** - * Inner-class representing a {@link TextureView} and its associated {@link SurfaceTexture} and - * {@link Surface}. Used to manage the lifecycle of these objects across device orientation - * changes. - */ - private static class VideoCallSurface implements TextureView.SurfaceTextureListener, - View.OnClickListener, View.OnAttachStateChangeListener { - private int mSurfaceId; - private VideoCallPresenter mPresenter; - private TextureView mTextureView; - private SurfaceTexture mSavedSurfaceTexture; - private Surface mSavedSurface; - private boolean mIsDoneWithSurface; - private int mWidth = DIMENSIONS_NOT_SET; - private int mHeight = DIMENSIONS_NOT_SET; - - /** - * Creates an instance of a {@link VideoCallSurface}. - * - * @param surfaceId The surface ID of the surface. - * @param textureView The {@link TextureView} for the surface. - */ - public VideoCallSurface(VideoCallPresenter presenter, int surfaceId, - TextureView textureView) { - this(presenter, surfaceId, textureView, DIMENSIONS_NOT_SET, DIMENSIONS_NOT_SET); - } - - /** - * Creates an instance of a {@link VideoCallSurface}. - * - * @param surfaceId The surface ID of the surface. - * @param textureView The {@link TextureView} for the surface. - * @param width The width of the surface. - * @param height The height of the surface. - */ - public VideoCallSurface(VideoCallPresenter presenter,int surfaceId, TextureView textureView, - int width, int height) { - Log.d(this, "VideoCallSurface: surfaceId=" + surfaceId + - " width=" + width + " height=" + height); - mPresenter = presenter; - mWidth = width; - mHeight = height; - mSurfaceId = surfaceId; - - recreateView(textureView); - } - - /** - * Recreates a {@link VideoCallSurface} after a device orientation change. Re-applies the - * saved {@link SurfaceTexture} to the - * - * @param view The {@link TextureView}. - */ - public void recreateView(TextureView view) { - if (DEBUG) { - Log.i(TAG, "recreateView: " + view); - } - - if (mTextureView == view) { - return; - } - - mTextureView = view; - mTextureView.setSurfaceTextureListener(this); - mTextureView.setOnClickListener(this); - - final boolean areSameSurfaces = - Objects.equal(mSavedSurfaceTexture, mTextureView.getSurfaceTexture()); - Log.d(this, "recreateView: SavedSurfaceTexture=" + mSavedSurfaceTexture - + " areSameSurfaces=" + areSameSurfaces); - if (mSavedSurfaceTexture != null && !areSameSurfaces) { - mTextureView.setSurfaceTexture(mSavedSurfaceTexture); - if (createSurface(mWidth, mHeight)) { - onSurfaceCreated(); - } - } - mIsDoneWithSurface = false; - } - - public void resetPresenter(VideoCallPresenter presenter) { - Log.d(this, "resetPresenter: CurrentPresenter=" + mPresenter + " NewPresenter=" - + presenter); - mPresenter = presenter; - } - - /** - * Handles {@link SurfaceTexture} callback to indicate that a {@link SurfaceTexture} has - * been successfully created. - * - * @param surfaceTexture The {@link SurfaceTexture} which has been created. - * @param width The width of the {@link SurfaceTexture}. - * @param height The height of the {@link SurfaceTexture}. - */ - @Override - public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, - int height) { - boolean surfaceCreated; - if (DEBUG) { - Log.i(TAG, "onSurfaceTextureAvailable: " + surfaceTexture); - } - // Where there is no saved {@link SurfaceTexture} available, use the newly created one. - // If a saved {@link SurfaceTexture} is available, we are re-creating after an - // orientation change. - Log.d(this, " onSurfaceTextureAvailable mSurfaceId=" + mSurfaceId + " surfaceTexture=" - + surfaceTexture + " width=" + width - + " height=" + height + " mSavedSurfaceTexture=" + mSavedSurfaceTexture); - Log.d(this, " onSurfaceTextureAvailable VideoCallPresenter=" + mPresenter); - if (mSavedSurfaceTexture == null) { - mSavedSurfaceTexture = surfaceTexture; - surfaceCreated = createSurface(width, height); - } else { - // A saved SurfaceTexture was found. - Log.d(this, " onSurfaceTextureAvailable: Replacing with cached surface..."); - mTextureView.setSurfaceTexture(mSavedSurfaceTexture); - surfaceCreated = true; - } - - // Inform presenter that the surface is available. - if (surfaceCreated) { - onSurfaceCreated(); - } - } - - private void onSurfaceCreated() { - if (mPresenter != null) { - mPresenter.onSurfaceCreated(mSurfaceId); - } else { - Log.e(this, "onSurfaceTextureAvailable: Presenter is null"); - } - } - - /** - * Handles a change in the {@link SurfaceTexture}'s size. - * - * @param surfaceTexture The {@link SurfaceTexture}. - * @param width The new width. - * @param height The new height. - */ - @Override - public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width, - int height) { - // Not handled - } - - /** - * Handles {@link SurfaceTexture} destruct callback, indicating that it has been destroyed. - * - * @param surfaceTexture The {@link SurfaceTexture}. - * @return {@code True} if the {@link TextureView} can release the {@link SurfaceTexture}. - */ - @Override - public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) { - /** - * Destroying the surface texture; inform the presenter so it can null the surfaces. - */ - Log.d(this, " onSurfaceTextureDestroyed mSurfaceId=" + mSurfaceId + " surfaceTexture=" - + surfaceTexture + " SavedSurfaceTexture=" + mSavedSurfaceTexture - + " SavedSurface=" + mSavedSurface); - Log.d(this, " onSurfaceTextureDestroyed VideoCallPresenter=" + mPresenter); - - // Notify presenter if it is not null. - onSurfaceDestroyed(); - - if (mIsDoneWithSurface) { - onSurfaceReleased(); - if (mSavedSurface != null) { - mSavedSurface.release(); - mSavedSurface = null; - } - } - return mIsDoneWithSurface; - } - - private void onSurfaceDestroyed() { - if (mPresenter != null) { - mPresenter.onSurfaceDestroyed(mSurfaceId); - } else { - Log.e(this, "onSurfaceTextureDestroyed: Presenter is null."); - } - } - - /** - * Handles {@link SurfaceTexture} update callback. - * @param surface - */ - @Override - public void onSurfaceTextureUpdated(SurfaceTexture surface) { - // Not Handled - } - - @Override - public void onViewAttachedToWindow(View v) { - if (DEBUG) { - Log.i(TAG, "OnViewAttachedToWindow"); - } - if (mSavedSurfaceTexture != null) { - mTextureView.setSurfaceTexture(mSavedSurfaceTexture); - } - } - - @Override - public void onViewDetachedFromWindow(View v) {} - - /** - * Retrieves the current {@link TextureView}. - * - * @return The {@link TextureView}. - */ - public TextureView getTextureView() { - return mTextureView; - } - - /** - * Called by the user presenter to indicate that the surface is no longer required due to a - * change in video state. Releases and clears out the saved surface and surface textures. - */ - public void setDoneWithSurface() { - Log.d(this, "setDoneWithSurface: SavedSurface=" + mSavedSurface - + " SavedSurfaceTexture=" + mSavedSurfaceTexture); - mIsDoneWithSurface = true; - if (mTextureView != null && mTextureView.isAvailable()) { - return; - } - - if (mSavedSurface != null) { - onSurfaceReleased(); - mSavedSurface.release(); - mSavedSurface = null; - } - if (mSavedSurfaceTexture != null) { - mSavedSurfaceTexture.release(); - mSavedSurfaceTexture = null; - } - } - - private void onSurfaceReleased() { - if (mPresenter != null) { - mPresenter.onSurfaceReleased(mSurfaceId); - } else { - Log.d(this, "setDoneWithSurface: Presenter is null."); - } - } - - /** - * Retrieves the saved surface instance. - * - * @return The surface. - */ - public Surface getSurface() { - return mSavedSurface; - } - - /** - * Sets the dimensions of the surface. - * - * @param width The width of the surface, in pixels. - * @param height The height of the surface, in pixels. - */ - public void setSurfaceDimensions(int width, int height) { - Log.d(this, "setSurfaceDimensions, width=" + width + " height=" + height); - mWidth = width; - mHeight = height; - - if (width != DIMENSIONS_NOT_SET && height != DIMENSIONS_NOT_SET - && mSavedSurfaceTexture != null) { - Log.d(this, "setSurfaceDimensions, mSavedSurfaceTexture is NOT equal to null."); - mSavedSurfaceTexture.setDefaultBufferSize(width, height); - } - } - - /** - * Creates the {@link Surface}, adjusting the {@link SurfaceTexture} buffer size. - * @param width The width of the surface to create. - * @param height The height of the surface to create. - */ - private boolean createSurface(int width, int height) { - Log.d(this, "createSurface mSavedSurfaceTexture=" + mSavedSurfaceTexture - + " mSurfaceId =" + mSurfaceId + " mWidth " + width + " mHeight=" + height); - if (width != DIMENSIONS_NOT_SET && height != DIMENSIONS_NOT_SET - && mSavedSurfaceTexture != null) { - mSavedSurfaceTexture.setDefaultBufferSize(width, height); - mSavedSurface = new Surface(mSavedSurfaceTexture); - return true; - } - return false; - } - - /** - * Handles a user clicking the surface, which is the trigger to toggle the full screen - * Video UI. - * - * @param view The view receiving the click. - */ - @Override - public void onClick(View view) { - if (mPresenter != null) { - mPresenter.onSurfaceClick(mSurfaceId); - } else { - Log.e(this, "onClick: Presenter is null."); - } - } - - /** - * Returns the dimensions of the surface. - * - * @return The dimensions of the surface. - */ - public Point getSurfaceDimensions() { - return new Point(mWidth, mHeight); - } - }; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - mAnimationDuration = getResources().getInteger(R.integer.video_animation_duration); - } - - /** - * Handles creation of the activity and initialization of the presenter. - * - * @param savedInstanceState The saved instance state. - */ - @Override - public void onActivityCreated(Bundle savedInstanceState) { - mIsLandscape = getResources().getBoolean(R.bool.is_layout_landscape); - Log.d(this, "onActivityCreated: IsLandscape=" + mIsLandscape); - getPresenter().init(getActivity()); - - super.onActivityCreated(savedInstanceState); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - super.onCreateView(inflater, container, savedInstanceState); - - final View view = inflater.inflate(R.layout.video_call_fragment, container, false); - - return view; - } - - /** - * Centers the display view vertically for portrait orientations. The view is centered within - * the available space not occupied by the call card. This is a no-op for landscape mode. - * - * @param displayVideo The video view to center. - */ - private void centerDisplayView(View displayVideo) { - if (!mIsLandscape) { - ViewGroup.LayoutParams p = displayVideo.getLayoutParams(); - int height = p.height; - - float spaceBesideCallCard = InCallPresenter.getInstance().getSpaceBesideCallCard(); - // If space beside call card is zeo, layout hasn't happened yet so there is no point - // in attempting to center the view. - if (Math.abs(spaceBesideCallCard - 0.0f) < 0.0001) { - return; - } - float videoViewTranslation = height / 2 - spaceBesideCallCard / 2; - displayVideo.setTranslationY(videoViewTranslation); - } - } - - /** - * After creation of the fragment view, retrieves the required views. - * - * @param view The fragment view. - * @param savedInstanceState The saved instance state. - */ - @Override - public void onViewCreated(View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - Log.d(this, "onViewCreated: VideoSurfacesInUse=" + sVideoSurfacesInUse); - - mVideoViewsStub = (ViewStub) view.findViewById(R.id.videoCallViewsStub); - } - - @Override - public void onStop() { - super.onStop(); - Log.d(this, "onStop:"); - } - - @Override - public void onPause() { - super.onPause(); - Log.d(this, "onPause:"); - getPresenter().cancelAutoFullScreen(); - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - Log.d(this, "onDestroyView:"); - } - - /** - * Creates the presenter for the {@link VideoCallFragment}. - * @return The presenter instance. - */ - @Override - public VideoCallPresenter createPresenter() { - Log.d(this, "createPresenter"); - VideoCallPresenter presenter = new VideoCallPresenter(); - onPresenterChanged(presenter); - return presenter; - } - - /** - * @return The user interface for the presenter, which is this fragment. - */ - @Override - public VideoCallPresenter.VideoCallUi getUi() { - return this; - } - - /** - * Inflate video surfaces. - * - * @param show {@code True} if the video surfaces should be shown. - */ - private void inflateVideoUi(boolean show) { - int visibility = show ? View.VISIBLE : View.GONE; - getView().setVisibility(visibility); - - if (show) { - inflateVideoCallViews(); - } - - if (mVideoViews != null) { - mVideoViews.setVisibility(visibility); - } - } - - /** - * Hides and shows the incoming video view and changes the outgoing video view's state based on - * whether outgoing view is enabled or not. - */ - @Override - public void showVideoViews(boolean previewPaused, boolean showIncoming) { - inflateVideoUi(true); - - View incomingVideoView = mVideoViews.findViewById(R.id.incomingVideo); - if (incomingVideoView != null) { - incomingVideoView.setVisibility(showIncoming ? View.VISIBLE : View.INVISIBLE); - } - if (mCameraOff != null) { - mCameraOff.setVisibility(!previewPaused ? View.VISIBLE : View.INVISIBLE); - } - if (mPreviewPhoto != null) { - mPreviewPhoto.setVisibility(!previewPaused ? View.VISIBLE : View.INVISIBLE); - } - } - - /** - * Hide all video views. - */ - @Override - public void hideVideoUi() { - inflateVideoUi(false); - } - - /** - * Cleans up the video telephony surfaces. Used when the presenter indicates a change to an - * audio-only state. Since the surfaces are static, it is important to ensure they are cleaned - * up promptly. - */ - @Override - public void cleanupSurfaces() { - Log.d(this, "cleanupSurfaces"); - if (sDisplaySurface != null) { - sDisplaySurface.setDoneWithSurface(); - sDisplaySurface = null; - } - if (sPreviewSurface != null) { - sPreviewSurface.setDoneWithSurface(); - sPreviewSurface = null; - } - sVideoSurfacesInUse = false; - } - - @Override - public ImageView getPreviewPhotoView() { - return mPreviewPhoto; - } - - /** - * Adjusts the location of the video preview view by the specified offset. - * - * @param shiftUp {@code true} if the preview should shift up, {@code false} if it should shift - * down. - * @param offset The offset. - */ - @Override - public void adjustPreviewLocation(boolean shiftUp, int offset) { - if (sPreviewSurface == null || mPreviewVideoContainer == null) { - return; - } - - // Set the position of the secondary call info card to its starting location. - mPreviewVideoContainer.setTranslationY(shiftUp ? 0 : -offset); - - // Animate the secondary card info slide up/down as it appears and disappears. - mPreviewVideoContainer.animate() - .setInterpolator(AnimUtils.EASE_OUT_EASE_IN) - .setDuration(mAnimationDuration) - .translationY(shiftUp ? -offset : 0) - .start(); - } - - private void onPresenterChanged(VideoCallPresenter presenter) { - Log.d(this, "onPresenterChanged: Presenter=" + presenter); - if (sDisplaySurface != null) { - sDisplaySurface.resetPresenter(presenter);; - } - if (sPreviewSurface != null) { - sPreviewSurface.resetPresenter(presenter); - } - } - - /** - * @return {@code True} if the display video surface has been created. - */ - @Override - public boolean isDisplayVideoSurfaceCreated() { - boolean ret = sDisplaySurface != null && sDisplaySurface.getSurface() != null; - Log.d(this, " isDisplayVideoSurfaceCreated returns " + ret); - return ret; - } - - /** - * @return {@code True} if the preview video surface has been created. - */ - @Override - public boolean isPreviewVideoSurfaceCreated() { - boolean ret = sPreviewSurface != null && sPreviewSurface.getSurface() != null; - Log.d(this, " isPreviewVideoSurfaceCreated returns " + ret); - return ret; - } - - /** - * {@link android.view.Surface} on which incoming video for a video call is displayed. - * {@code Null} until the video views {@link android.view.ViewStub} is inflated. - */ - @Override - public Surface getDisplayVideoSurface() { - return sDisplaySurface == null ? null : sDisplaySurface.getSurface(); - } - - /** - * {@link android.view.Surface} on which a preview of the outgoing video for a video call is - * displayed. {@code Null} until the video views {@link android.view.ViewStub} is inflated. - */ - @Override - public Surface getPreviewVideoSurface() { - return sPreviewSurface == null ? null : sPreviewSurface.getSurface(); - } - - /** - * Changes the dimensions of the preview surface. Called when the dimensions change due to a - * device orientation change. - * - * @param width The new width. - * @param height The new height. - */ - @Override - public void setPreviewSize(int width, int height) { - Log.d(this, "setPreviewSize: width=" + width + " height=" + height); - if (sPreviewSurface != null) { - TextureView preview = sPreviewSurface.getTextureView(); - - if (preview == null ) { - return; - } - - // Set the dimensions of both the video surface and the FrameLayout containing it. - ViewGroup.LayoutParams params = preview.getLayoutParams(); - params.width = width; - params.height = height; - preview.setLayoutParams(params); - - if (mPreviewVideoContainer != null) { - ViewGroup.LayoutParams containerParams = mPreviewVideoContainer.getLayoutParams(); - containerParams.width = width; - containerParams.height = height; - mPreviewVideoContainer.setLayoutParams(containerParams); - } - - // The width and height are interchanged outside of this method based on the current - // orientation, so we can transform using "width", which will be either the width or - // the height. - Matrix transform = new Matrix(); - transform.setScale(-1, 1, width/2, 0); - preview.setTransform(transform); - } - } - - /** - * Sets the rotation of the preview surface. Called when the dimensions change due to a - * device orientation change. - * - * Please note that the screen orientation passed in is subtracted from 360 to get the actual - * preview rotation values. - * - * @param rotation The screen orientation. One of - - * {@link InCallOrientationEventListener#SCREEN_ORIENTATION_0}, - * {@link InCallOrientationEventListener#SCREEN_ORIENTATION_90}, - * {@link InCallOrientationEventListener#SCREEN_ORIENTATION_180}, - * {@link InCallOrientationEventListener#SCREEN_ORIENTATION_270}). - */ - @Override - public void setPreviewRotation(int orientation) { - Log.d(this, "setPreviewRotation: orientation=" + orientation); - if (sPreviewSurface != null) { - TextureView preview = sPreviewSurface.getTextureView(); - - if (preview == null ) { - return; - } - - preview.setRotation(orientation); - } - } - - @Override - public void setPreviewSurfaceSize(int width, int height) { - final boolean isPreviewSurfaceAvailable = sPreviewSurface != null; - Log.d(this, "setPreviewSurfaceSize: width=" + width + " height=" + height + - " isPreviewSurfaceAvailable=" + isPreviewSurfaceAvailable); - if (isPreviewSurfaceAvailable) { - sPreviewSurface.setSurfaceDimensions(width, height); - } - } - - /** - * returns UI's current orientation. - */ - @Override - public int getCurrentRotation() { - try { - return getActivity().getWindowManager().getDefaultDisplay().getRotation(); - } catch (Exception e) { - Log.e(this, "getCurrentRotation: Retrieving current rotation failed. Ex=" + e); - } - return ORIENTATION_UNKNOWN; - } - - /** - * Changes the dimensions of the display video surface. Called when the dimensions change due to - * a peer resolution update - * - * @param width The new width. - * @param height The new height. - */ - @Override - public void setDisplayVideoSize(int width, int height) { - Log.v(this, "setDisplayVideoSize: width=" + width + " height=" + height); - if (sDisplaySurface != null) { - TextureView displayVideo = sDisplaySurface.getTextureView(); - if (displayVideo == null) { - Log.e(this, "Display Video texture view is null. Bail out"); - return; - } - sDisplaySize = new Point(width, height); - setSurfaceSizeAndTranslation(displayVideo, sDisplaySize); - } else { - Log.e(this, "Display Video Surface is null. Bail out"); - } - } - - /** - * Determines the size of the device screen. - * - * @return {@link Point} specifying the width and height of the screen. - */ - @Override - public Point getScreenSize() { - // Get current screen size. - Display display = getActivity().getWindowManager().getDefaultDisplay(); - Point size = new Point(); - display.getSize(size); - - return size; - } - - /** - * Determines the size of the preview surface. - * - * @return {@link Point} specifying the width and height of the preview surface. - */ - @Override - public Point getPreviewSize() { - if (sPreviewSurface == null) { - return null; - } - return sPreviewSurface.getSurfaceDimensions(); - } - - /** - * Inflates the {@link ViewStub} containing the incoming and outgoing surfaces, if necessary, - * and creates {@link VideoCallSurface} instances to track the surfaces. - */ - private void inflateVideoCallViews() { - Log.d(this, "inflateVideoCallViews"); - if (mVideoViews == null ) { - mVideoViews = mVideoViewsStub.inflate(); - } - - if (mVideoViews != null) { - mPreviewVideoContainer = mVideoViews.findViewById(R.id.previewVideoContainer); - mCameraOff = mVideoViews.findViewById(R.id.previewCameraOff); - mPreviewPhoto = (ImageView) mVideoViews.findViewById(R.id.previewProfilePhoto); - - TextureView displaySurface = (TextureView) mVideoViews.findViewById(R.id.incomingVideo); - - Log.d(this, "inflateVideoCallViews: sVideoSurfacesInUse=" + sVideoSurfacesInUse); - //If peer adjusted screen size is not available, set screen size to default display size - Point screenSize = sDisplaySize == null ? getScreenSize() : sDisplaySize; - setSurfaceSizeAndTranslation(displaySurface, screenSize); - - if (!sVideoSurfacesInUse) { - // Where the video surfaces are not already in use (first time creating them), - // setup new VideoCallSurface instances to track them. - Log.d(this, " inflateVideoCallViews screenSize" + screenSize); - - sDisplaySurface = new VideoCallSurface(getPresenter(), SURFACE_DISPLAY, - (TextureView) mVideoViews.findViewById(R.id.incomingVideo), screenSize.x, - screenSize.y); - sPreviewSurface = new VideoCallSurface(getPresenter(), SURFACE_PREVIEW, - (TextureView) mVideoViews.findViewById(R.id.previewVideo)); - sVideoSurfacesInUse = true; - } else { - // In this case, the video surfaces are already in use (we are recreating the - // Fragment after a destroy/create cycle resulting from a rotation. - sDisplaySurface.recreateView((TextureView) mVideoViews.findViewById( - R.id.incomingVideo)); - sPreviewSurface.recreateView((TextureView) mVideoViews.findViewById( - R.id.previewVideo)); - } - - // Attempt to center the incoming video view, if it is in the layout. - final ViewTreeObserver observer = mVideoViews.getViewTreeObserver(); - observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - // Check if the layout includes the incoming video surface -- this will only be the - // case for a video call. - View displayVideo = mVideoViews.findViewById(R.id.incomingVideo); - if (displayVideo != null) { - centerDisplayView(displayVideo); - } - mIsLayoutComplete = true; - - // Remove the listener so we don't continually re-layout. - ViewTreeObserver observer = mVideoViews.getViewTreeObserver(); - if (observer.isAlive()) { - observer.removeOnGlobalLayoutListener(this); - } - } - }); - } - } - - /** - * Resizes a surface so that it has the same size as the full screen and so that it is - * centered vertically below the call card. - * - * @param textureView The {@link TextureView} to resize and position. - * @param size The size of the screen. - */ - private void setSurfaceSizeAndTranslation(TextureView textureView, Point size) { - // Set the surface to have that size. - ViewGroup.LayoutParams params = textureView.getLayoutParams(); - params.width = size.x; - params.height = size.y; - textureView.setLayoutParams(params); - Log.d(this, "setSurfaceSizeAndTranslation: Size=" + size + "IsLayoutComplete=" + - mIsLayoutComplete + "IsLandscape=" + mIsLandscape); - - // It is only possible to center the display view if layout of the views has completed. - // It is only after layout is complete that the dimensions of the Call Card has been - // established, which is a prerequisite to centering the view. - // Incoming video calls will center the view - if (mIsLayoutComplete) { - centerDisplayView(textureView); - } - } -} diff --git a/InCallUI/src/com/android/incallui/VideoCallPresenter.java b/InCallUI/src/com/android/incallui/VideoCallPresenter.java deleted file mode 100644 index 06e3e4440..000000000 --- a/InCallUI/src/com/android/incallui/VideoCallPresenter.java +++ /dev/null @@ -1,1306 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.content.Context; -import android.database.Cursor; -import android.graphics.Point; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.Handler; -import android.os.Looper; -import android.provider.ContactsContract; -import android.telecom.Connection; -import android.telecom.InCallService.VideoCall; -import android.telecom.VideoProfile; -import android.telecom.VideoProfile.CameraCapabilities; -import android.view.Surface; -import android.widget.ImageView; - -import com.android.contacts.common.ContactPhotoManager; -import com.android.contacts.common.compat.CompatUtils; -import com.android.dialer.R; -import com.android.incallui.InCallPresenter.InCallDetailsListener; -import com.android.incallui.InCallPresenter.InCallOrientationListener; -import com.android.incallui.InCallPresenter.InCallStateListener; -import com.android.incallui.InCallPresenter.IncomingCallListener; -import com.android.incallui.InCallVideoCallCallbackNotifier.SurfaceChangeListener; -import com.android.incallui.InCallVideoCallCallbackNotifier.VideoEventListener; - -import java.util.Objects; - -/** - * Logic related to the {@link VideoCallFragment} and for managing changes to the video calling - * surfaces based on other user interface events and incoming events from the - * {@class VideoCallListener}. - *

- * When a call's video state changes to bi-directional video, the - * {@link com.android.incallui.VideoCallPresenter} performs the following negotiation with the - * telephony layer: - *

    - *
  • {@code VideoCallPresenter} creates and informs telephony of the display surface.
  • - *
  • {@code VideoCallPresenter} creates the preview surface.
  • - *
  • {@code VideoCallPresenter} informs telephony of the currently selected camera.
  • - *
  • Telephony layer sends {@link CameraCapabilities}, including the - * dimensions of the video for the current camera.
  • - *
  • {@code VideoCallPresenter} adjusts size of the preview surface to match the aspect - * ratio of the camera.
  • - *
  • {@code VideoCallPresenter} informs telephony of the new preview surface.
  • - *
- *

- * When downgrading to an audio-only video state, the {@code VideoCallPresenter} nulls both - * surfaces. - */ -public class VideoCallPresenter extends Presenter implements - IncomingCallListener, InCallOrientationListener, InCallStateListener, - InCallDetailsListener, SurfaceChangeListener, VideoEventListener, - InCallPresenter.InCallEventListener { - public static final String TAG = "VideoCallPresenter"; - - public static final boolean DEBUG = false; - - /** - * Runnable which is posted to schedule automatically entering fullscreen mode. Will not auto - * enter fullscreen mode if the dialpad is visible (doing so would make it impossible to exit - * the dialpad). - */ - private Runnable mAutoFullscreenRunnable = new Runnable() { - @Override - public void run() { - if (mAutoFullScreenPending && !InCallPresenter.getInstance().isDialpadVisible() - && mIsVideoMode) { - - Log.v(this, "Automatically entering fullscreen mode."); - InCallPresenter.getInstance().setFullScreen(true); - mAutoFullScreenPending = false; - } else { - Log.v(this, "Skipping scheduled fullscreen mode."); - } - } - }; - - /** - * Defines the state of the preview surface negotiation with the telephony layer. - */ - private class PreviewSurfaceState { - /** - * The camera has not yet been set on the {@link VideoCall}; negotiation has not yet - * started. - */ - private static final int NONE = 0; - - /** - * The camera has been set on the {@link VideoCall}, but camera capabilities have not yet - * been received. - */ - private static final int CAMERA_SET = 1; - - /** - * The camera capabilties have been received from telephony, but the surface has not yet - * been set on the {@link VideoCall}. - */ - private static final int CAPABILITIES_RECEIVED = 2; - - /** - * The surface has been set on the {@link VideoCall}. - */ - private static final int SURFACE_SET = 3; - } - - /** - * The minimum width or height of the preview surface. Used when re-sizing the preview surface - * to match the aspect ratio of the currently selected camera. - */ - private float mMinimumVideoDimension; - - /** - * The current context. - */ - private Context mContext; - - /** - * The call the video surfaces are currently related to - */ - private Call mPrimaryCall; - - /** - * The {@link VideoCall} used to inform the video telephony layer of changes to the video - * surfaces. - */ - private VideoCall mVideoCall; - - /** - * Determines if the current UI state represents a video call. - */ - private int mCurrentVideoState; - - /** - * Call's current state - */ - private int mCurrentCallState = Call.State.INVALID; - - /** - * Determines the device orientation (portrait/lanscape). - */ - private int mDeviceOrientation = InCallOrientationEventListener.SCREEN_ORIENTATION_0; - - /** - * Tracks the state of the preview surface negotiation with the telephony layer. - */ - private int mPreviewSurfaceState = PreviewSurfaceState.NONE; - - private static boolean mIsVideoMode = false; - - /** - * Contact photo manager to retrieve cached contact photo information. - */ - private ContactPhotoManager mContactPhotoManager = null; - - /** - * The URI for the user's profile photo, or {@code null} if not specified. - */ - private ContactInfoCache.ContactCacheEntry mProfileInfo = null; - - /** - * UI thread handler used for delayed task execution. - */ - private Handler mHandler; - - /** - * Determines whether video calls should automatically enter full screen mode after - * {@link #mAutoFullscreenTimeoutMillis} milliseconds. - */ - private boolean mIsAutoFullscreenEnabled = false; - - /** - * Determines the number of milliseconds after which a video call will automatically enter - * fullscreen mode. Requires {@link #mIsAutoFullscreenEnabled} to be {@code true}. - */ - private int mAutoFullscreenTimeoutMillis = 0; - - /** - * Determines if the countdown is currently running to automatically enter full screen video - * mode. - */ - private boolean mAutoFullScreenPending = false; - - /** - * Initializes the presenter. - * - * @param context The current context. - */ - public void init(Context context) { - mContext = context; - mMinimumVideoDimension = mContext.getResources().getDimension( - R.dimen.video_preview_small_dimension); - mHandler = new Handler(Looper.getMainLooper()); - mIsAutoFullscreenEnabled = mContext.getResources() - .getBoolean(R.bool.video_call_auto_fullscreen); - mAutoFullscreenTimeoutMillis = mContext.getResources().getInteger( - R.integer.video_call_auto_fullscreen_timeout); - } - - /** - * Called when the user interface is ready to be used. - * - * @param ui The Ui implementation that is now ready to be used. - */ - @Override - public void onUiReady(VideoCallUi ui) { - super.onUiReady(ui); - Log.d(this, "onUiReady:"); - - // Do not register any listeners if video calling is not compatible to safeguard against - // any accidental calls of video calling code. - if (!CompatUtils.isVideoCompatible()) { - return; - } - - // Register for call state changes last - InCallPresenter.getInstance().addListener(this); - InCallPresenter.getInstance().addDetailsListener(this); - InCallPresenter.getInstance().addIncomingCallListener(this); - InCallPresenter.getInstance().addOrientationListener(this); - // To get updates of video call details changes - InCallPresenter.getInstance().addDetailsListener(this); - InCallPresenter.getInstance().addInCallEventListener(this); - - // Register for surface and video events from {@link InCallVideoCallListener}s. - InCallVideoCallCallbackNotifier.getInstance().addSurfaceChangeListener(this); - InCallVideoCallCallbackNotifier.getInstance().addVideoEventListener(this); - mCurrentVideoState = VideoProfile.STATE_AUDIO_ONLY; - mCurrentCallState = Call.State.INVALID; - - final InCallPresenter.InCallState inCallState = - InCallPresenter.getInstance().getInCallState(); - onStateChange(inCallState, inCallState, CallList.getInstance()); - } - - /** - * Called when the user interface is no longer ready to be used. - * - * @param ui The Ui implementation that is no longer ready to be used. - */ - @Override - public void onUiUnready(VideoCallUi ui) { - super.onUiUnready(ui); - Log.d(this, "onUiUnready:"); - - if (!CompatUtils.isVideoCompatible()) { - return; - } - - cancelAutoFullScreen(); - - InCallPresenter.getInstance().removeListener(this); - InCallPresenter.getInstance().removeDetailsListener(this); - InCallPresenter.getInstance().removeIncomingCallListener(this); - InCallPresenter.getInstance().removeOrientationListener(this); - InCallPresenter.getInstance().removeInCallEventListener(this); - - InCallVideoCallCallbackNotifier.getInstance().removeSurfaceChangeListener(this); - InCallVideoCallCallbackNotifier.getInstance().removeVideoEventListener(this); - } - - /** - * Handles the creation of a surface in the {@link VideoCallFragment}. - * - * @param surface The surface which was created. - */ - public void onSurfaceCreated(int surface) { - Log.d(this, "onSurfaceCreated surface=" + surface + " mVideoCall=" + mVideoCall); - Log.d(this, "onSurfaceCreated PreviewSurfaceState=" + mPreviewSurfaceState); - Log.d(this, "onSurfaceCreated presenter=" + this); - - final VideoCallUi ui = getUi(); - if (ui == null || mVideoCall == null) { - Log.w(this, "onSurfaceCreated: Error bad state VideoCallUi=" + ui + " mVideoCall=" - + mVideoCall); - return; - } - - // If the preview surface has just been created and we have already received camera - // capabilities, but not yet set the surface, we will set the surface now. - if (surface == VideoCallFragment.SURFACE_PREVIEW ) { - if (mPreviewSurfaceState == PreviewSurfaceState.CAPABILITIES_RECEIVED) { - mPreviewSurfaceState = PreviewSurfaceState.SURFACE_SET; - mVideoCall.setPreviewSurface(ui.getPreviewVideoSurface()); - } else if (mPreviewSurfaceState == PreviewSurfaceState.NONE && isCameraRequired()){ - enableCamera(mVideoCall, true); - } - } else if (surface == VideoCallFragment.SURFACE_DISPLAY) { - mVideoCall.setDisplaySurface(ui.getDisplayVideoSurface()); - } - } - - /** - * Handles structural changes (format or size) to a surface. - * - * @param surface The surface which changed. - * @param format The new PixelFormat of the surface. - * @param width The new width of the surface. - * @param height The new height of the surface. - */ - public void onSurfaceChanged(int surface, int format, int width, int height) { - //Do stuff - } - - /** - * Handles the destruction of a surface in the {@link VideoCallFragment}. - * Note: The surface is being released, that is, it is no longer valid. - * - * @param surface The surface which was destroyed. - */ - public void onSurfaceReleased(int surface) { - Log.d(this, "onSurfaceReleased: mSurfaceId=" + surface); - if ( mVideoCall == null) { - Log.w(this, "onSurfaceReleased: VideoCall is null. mSurfaceId=" + - surface); - return; - } - - if (surface == VideoCallFragment.SURFACE_DISPLAY) { - mVideoCall.setDisplaySurface(null); - } else if (surface == VideoCallFragment.SURFACE_PREVIEW) { - mVideoCall.setPreviewSurface(null); - enableCamera(mVideoCall, false); - } - } - - /** - * Called by {@link VideoCallFragment} when the surface is detached from UI (TextureView). - * Note: The surface will be cached by {@link VideoCallFragment}, so we don't immediately - * null out incoming video surface. - * @see VideoCallPresenter#onSurfaceReleased(int) - * - * @param surface The surface which was detached. - */ - public void onSurfaceDestroyed(int surface) { - Log.d(this, "onSurfaceDestroyed: mSurfaceId=" + surface); - if (mVideoCall == null) { - return; - } - - final boolean isChangingConfigurations = - InCallPresenter.getInstance().isChangingConfigurations(); - Log.d(this, "onSurfaceDestroyed: isChangingConfigurations=" + isChangingConfigurations); - - if (surface == VideoCallFragment.SURFACE_PREVIEW) { - if (!isChangingConfigurations) { - enableCamera(mVideoCall, false); - } else { - Log.w(this, "onSurfaceDestroyed: Activity is being destroyed due " - + "to configuration changes. Not closing the camera."); - } - } - } - - /** - * Handles clicks on the video surfaces by toggling full screen state. - * Informs the {@link InCallPresenter} of the change so that it can inform the - * {@link CallCardPresenter} of the change. - * - * @param surfaceId The video surface receiving the click. - */ - public void onSurfaceClick(int surfaceId) { - boolean isFullscreen = InCallPresenter.getInstance().toggleFullscreenMode(); - Log.v(this, "toggleFullScreen = " + isFullscreen); - } - - /** - * Handles incoming calls. - * - * @param oldState The old in call state. - * @param newState The new in call state. - * @param call The call. - */ - @Override - public void onIncomingCall(InCallPresenter.InCallState oldState, - InCallPresenter.InCallState newState, Call call) { - // same logic should happen as with onStateChange() - onStateChange(oldState, newState, CallList.getInstance()); - } - - /** - * Handles state changes (including incoming calls) - * - * @param newState The in call state. - * @param callList The call list. - */ - @Override - public void onStateChange(InCallPresenter.InCallState oldState, - InCallPresenter.InCallState newState, CallList callList) { - Log.d(this, "onStateChange oldState" + oldState + " newState=" + newState + - " isVideoMode=" + isVideoMode()); - - if (newState == InCallPresenter.InCallState.NO_CALLS) { - if (isVideoMode()) { - exitVideoMode(); - } - - cleanupSurfaces(); - } - - // Determine the primary active call). - Call primary = null; - - // Determine the call which is the focus of the user's attention. In the case of an - // incoming call waiting call, the primary call is still the active video call, however - // the determination of whether we should be in fullscreen mode is based on the type of the - // incoming call, not the active video call. - Call currentCall = null; - - if (newState == InCallPresenter.InCallState.INCOMING) { - // We don't want to replace active video call (primary call) - // with a waiting call, since user may choose to ignore/decline the waiting call and - // this should have no impact on current active video call, that is, we should not - // change the camera or UI unless the waiting VT call becomes active. - primary = callList.getActiveCall(); - currentCall = callList.getIncomingCall(); - if (!VideoUtils.isActiveVideoCall(primary)) { - primary = callList.getIncomingCall(); - } - } else if (newState == InCallPresenter.InCallState.OUTGOING) { - currentCall = primary = callList.getOutgoingCall(); - } else if (newState == InCallPresenter.InCallState.PENDING_OUTGOING) { - currentCall = primary = callList.getPendingOutgoingCall(); - } else if (newState == InCallPresenter.InCallState.INCALL) { - currentCall = primary = callList.getActiveCall(); - } - - final boolean primaryChanged = !Objects.equals(mPrimaryCall, primary); - Log.d(this, "onStateChange primaryChanged=" + primaryChanged); - Log.d(this, "onStateChange primary= " + primary); - Log.d(this, "onStateChange mPrimaryCall = " + mPrimaryCall); - if (primaryChanged) { - onPrimaryCallChanged(primary); - } else if (mPrimaryCall != null) { - updateVideoCall(primary); - } - updateCallCache(primary); - - // If the call context changed, potentially exit fullscreen or schedule auto enter of - // fullscreen mode. - // If the current call context is no longer a video call, exit fullscreen mode. - maybeExitFullscreen(currentCall); - // Schedule auto-enter of fullscreen mode if the current call context is a video call - maybeAutoEnterFullscreen(currentCall); - } - - /** - * Handles a change to the fullscreen mode of the app. - * - * @param isFullscreenMode {@code true} if the app is now fullscreen, {@code false} otherwise. - */ - @Override - public void onFullscreenModeChanged(boolean isFullscreenMode) { - cancelAutoFullScreen(); - } - - /** - * Handles changes to the visibility of the secondary caller info bar. - * - * @param isVisible {@code true} if the secondary caller info is showing, {@code false} - * otherwise. - * @param height the height of the secondary caller info bar. - */ - @Override - public void onSecondaryCallerInfoVisibilityChanged(boolean isVisible, int height) { - Log.d(this, - "onSecondaryCallerInfoVisibilityChanged : isVisible = " + isVisible + " height = " - + height); - getUi().adjustPreviewLocation(isVisible /* shiftUp */, height); - } - - private void checkForVideoStateChange(Call call) { - final boolean isVideoCall = VideoUtils.isVideoCall(call); - final boolean hasVideoStateChanged = mCurrentVideoState != call.getVideoState(); - - Log.d(this, "checkForVideoStateChange: isVideoCall= " + isVideoCall - + " hasVideoStateChanged=" + hasVideoStateChanged + " isVideoMode=" - + isVideoMode() + " previousVideoState: " + - VideoProfile.videoStateToString(mCurrentVideoState) + " newVideoState: " - + VideoProfile.videoStateToString(call.getVideoState())); - - if (!hasVideoStateChanged) { - return; - } - - updateCameraSelection(call); - - if (isVideoCall) { - adjustVideoMode(call); - } else if (isVideoMode()) { - exitVideoMode(); - } - } - - private void checkForCallStateChange(Call call) { - final boolean isVideoCall = VideoUtils.isVideoCall(call); - final boolean hasCallStateChanged = mCurrentCallState != call.getState(); - - Log.d(this, "checkForCallStateChange: isVideoCall= " + isVideoCall - + " hasCallStateChanged=" + - hasCallStateChanged + " isVideoMode=" + isVideoMode()); - - if (!hasCallStateChanged) { - return; - } - - if (isVideoCall) { - final InCallCameraManager cameraManager = InCallPresenter.getInstance(). - getInCallCameraManager(); - - String prevCameraId = cameraManager.getActiveCameraId(); - updateCameraSelection(call); - String newCameraId = cameraManager.getActiveCameraId(); - - if (!Objects.equals(prevCameraId, newCameraId) && VideoUtils.isActiveVideoCall(call)) { - enableCamera(call.getVideoCall(), true); - } - } - - // Make sure we hide or show the video UI if needed. - showVideoUi(call.getVideoState(), call.getState()); - } - - private void cleanupSurfaces() { - final VideoCallUi ui = getUi(); - if (ui == null) { - Log.w(this, "cleanupSurfaces"); - return; - } - ui.cleanupSurfaces(); - } - - private void onPrimaryCallChanged(Call newPrimaryCall) { - final boolean isVideoCall = VideoUtils.isVideoCall(newPrimaryCall); - final boolean isVideoMode = isVideoMode(); - - Log.d(this, "onPrimaryCallChanged: isVideoCall=" + isVideoCall + " isVideoMode=" - + isVideoMode); - - if (!isVideoCall && isVideoMode) { - // Terminate video mode if new primary call is not a video call - // and we are currently in video mode. - Log.d(this, "onPrimaryCallChanged: Exiting video mode..."); - exitVideoMode(); - } else if (isVideoCall) { - Log.d(this, "onPrimaryCallChanged: Entering video mode..."); - - updateCameraSelection(newPrimaryCall); - adjustVideoMode(newPrimaryCall); - } - checkForOrientationAllowedChange(newPrimaryCall); - } - - private boolean isVideoMode() { - return mIsVideoMode; - } - - private void updateCallCache(Call call) { - if (call == null) { - mCurrentVideoState = VideoProfile.STATE_AUDIO_ONLY; - mCurrentCallState = Call.State.INVALID; - mVideoCall = null; - mPrimaryCall = null; - } else { - mCurrentVideoState = call.getVideoState(); - mVideoCall = call.getVideoCall(); - mCurrentCallState = call.getState(); - mPrimaryCall = call; - } - } - - /** - * Handles changes to the details of the call. The {@link VideoCallPresenter} is interested in - * changes to the video state. - * - * @param call The call for which the details changed. - * @param details The new call details. - */ - @Override - public void onDetailsChanged(Call call, android.telecom.Call.Details details) { - Log.d(this, " onDetailsChanged call=" + call + " details=" + details + " mPrimaryCall=" - + mPrimaryCall); - if (call == null) { - return; - } - // If the details change is not for the currently active call no update is required. - if (!call.equals(mPrimaryCall)) { - Log.d(this, " onDetailsChanged: Details not for current active call so returning. "); - return; - } - - updateVideoCall(call); - - updateCallCache(call); - } - - private void updateVideoCall(Call call) { - checkForVideoCallChange(call); - checkForVideoStateChange(call); - checkForCallStateChange(call); - checkForOrientationAllowedChange(call); - } - - private void checkForOrientationAllowedChange(Call call) { - InCallPresenter.getInstance().setInCallAllowsOrientationChange( - VideoUtils.isVideoCall(call)); - } - - /** - * Checks for a change to the video call and changes it if required. - */ - private void checkForVideoCallChange(Call call) { - final VideoCall videoCall = call.getTelecomCall().getVideoCall(); - Log.d(this, "checkForVideoCallChange: videoCall=" + videoCall + " mVideoCall=" - + mVideoCall); - if (!Objects.equals(videoCall, mVideoCall)) { - changeVideoCall(call); - } - } - - /** - * Handles a change to the video call. Sets the surfaces on the previous call to null and sets - * the surfaces on the new video call accordingly. - * - * @param call The new video call. - */ - private void changeVideoCall(Call call) { - final VideoCall videoCall = call.getTelecomCall().getVideoCall(); - Log.d(this, "changeVideoCall to videoCall=" + videoCall + " mVideoCall=" + mVideoCall); - // Null out the surfaces on the previous video call. - if (mVideoCall != null) { - // Log.d(this, "Null out the surfaces on the previous video call."); - // mVideoCall.setDisplaySurface(null); - // mVideoCall.setPreviewSurface(null); - } - - final boolean hasChanged = mVideoCall == null && videoCall != null; - - mVideoCall = videoCall; - if (mVideoCall == null || call == null) { - Log.d(this, "Video call or primary call is null. Return"); - return; - } - - if (VideoUtils.isVideoCall(call) && hasChanged) { - adjustVideoMode(call); - } - } - - private static boolean isCameraRequired(int videoState) { - return VideoProfile.isBidirectional(videoState) - || VideoProfile.isTransmissionEnabled(videoState); - } - - private boolean isCameraRequired() { - return mPrimaryCall != null && isCameraRequired(mPrimaryCall.getVideoState()); - } - - /** - * Adjusts the current video mode by setting up the preview and display surfaces as necessary. - * Expected to be called whenever the video state associated with a call changes (e.g. a user - * turns their camera on or off) to ensure the correct surfaces are shown/hidden. - * TODO(vt): Need to adjust size and orientation of preview surface here. - */ - private void adjustVideoMode(Call call) { - VideoCall videoCall = call.getVideoCall(); - int newVideoState = call.getVideoState(); - - Log.d(this, "adjustVideoMode videoCall= " + videoCall + " videoState: " + newVideoState); - VideoCallUi ui = getUi(); - if (ui == null) { - Log.e(this, "Error VideoCallUi is null so returning"); - return; - } - - showVideoUi(newVideoState, call.getState()); - - // Communicate the current camera to telephony and make a request for the camera - // capabilities. - if (videoCall != null) { - if (ui.isDisplayVideoSurfaceCreated()) { - Log.d(this, "Calling setDisplaySurface with " + ui.getDisplayVideoSurface()); - videoCall.setDisplaySurface(ui.getDisplayVideoSurface()); - } - - videoCall.setDeviceOrientation(mDeviceOrientation); - enableCamera(videoCall, isCameraRequired(newVideoState)); - } - int previousVideoState = mCurrentVideoState; - mCurrentVideoState = newVideoState; - mIsVideoMode = true; - - // adjustVideoMode may be called if we are already in a 1-way video state. In this case - // we do not want to trigger auto-fullscreen mode. - if (!VideoUtils.isVideoCall(previousVideoState) && VideoUtils.isVideoCall(newVideoState)) { - maybeAutoEnterFullscreen(call); - } - } - - private void enableCamera(VideoCall videoCall, boolean isCameraRequired) { - Log.d(this, "enableCamera: VideoCall=" + videoCall + " enabling=" + isCameraRequired); - if (videoCall == null) { - Log.w(this, "enableCamera: VideoCall is null."); - return; - } - - if (isCameraRequired) { - InCallCameraManager cameraManager = InCallPresenter.getInstance(). - getInCallCameraManager(); - videoCall.setCamera(cameraManager.getActiveCameraId()); - mPreviewSurfaceState = PreviewSurfaceState.CAMERA_SET; - - videoCall.requestCameraCapabilities(); - } else { - mPreviewSurfaceState = PreviewSurfaceState.NONE; - videoCall.setCamera(null); - } - } - - /** - * Exits video mode by hiding the video surfaces and making other adjustments (eg. audio). - */ - private void exitVideoMode() { - Log.d(this, "exitVideoMode"); - - showVideoUi(VideoProfile.STATE_AUDIO_ONLY, Call.State.ACTIVE); - enableCamera(mVideoCall, false); - InCallPresenter.getInstance().setFullScreen(false); - - mIsVideoMode = false; - } - - /** - * Based on the current video state and call state, show or hide the incoming and - * outgoing video surfaces. The outgoing video surface is shown any time video is transmitting. - * The incoming video surface is shown whenever the video is un-paused and active. - * - * @param videoState The video state. - * @param callState The call state. - */ - private void showVideoUi(int videoState, int callState) { - VideoCallUi ui = getUi(); - if (ui == null) { - Log.e(this, "showVideoUi, VideoCallUi is null returning"); - return; - } - boolean showIncomingVideo = showIncomingVideo(videoState, callState); - boolean showOutgoingVideo = showOutgoingVideo(videoState); - Log.v(this, "showVideoUi : showIncoming = " + showIncomingVideo + " showOutgoing = " - + showOutgoingVideo); - if (showIncomingVideo || showOutgoingVideo) { - ui.showVideoViews(showOutgoingVideo, showIncomingVideo); - - if (VideoProfile.isReceptionEnabled(videoState)) { - loadProfilePhotoAsync(); - } - } else { - ui.hideVideoUi(); - } - - InCallPresenter.getInstance().enableScreenTimeout( - VideoProfile.isAudioOnly(videoState)); - } - - /** - * Determines if the incoming video surface should be shown based on the current videoState and - * callState. The video surface is shown when incoming video is not paused, the call is active, - * and video reception is enabled. - * - * @param videoState The current video state. - * @param callState The current call state. - * @return {@code true} if the incoming video surface should be shown, {@code false} otherwise. - */ - public static boolean showIncomingVideo(int videoState, int callState) { - if (!CompatUtils.isVideoCompatible()) { - return false; - } - - boolean isPaused = VideoProfile.isPaused(videoState); - boolean isCallActive = callState == Call.State.ACTIVE; - - return !isPaused && isCallActive && VideoProfile.isReceptionEnabled(videoState); - } - - /** - * Determines if the outgoing video surface should be shown based on the current videoState. - * The video surface is shown if video transmission is enabled. - * - * @param videoState The current video state. - * @return {@code true} if the the outgoing video surface should be shown, {@code false} - * otherwise. - */ - public static boolean showOutgoingVideo(int videoState) { - if (!CompatUtils.isVideoCompatible()) { - return false; - } - - return VideoProfile.isTransmissionEnabled(videoState); - } - - /** - * Handles peer video pause state changes. - * - * @param call The call which paused or un-pausedvideo transmission. - * @param paused {@code True} when the video transmission is paused, {@code false} when video - * transmission resumes. - */ - @Override - public void onPeerPauseStateChanged(Call call, boolean paused) { - if (!call.equals(mPrimaryCall)) { - return; - } - - // TODO(vt): Show/hide the peer contact photo. - } - - /** - * Handles peer video dimension changes. - * - * @param call The call which experienced a peer video dimension change. - * @param width The new peer video width . - * @param height The new peer video height. - */ - @Override - public void onUpdatePeerDimensions(Call call, int width, int height) { - Log.d(this, "onUpdatePeerDimensions: width= " + width + " height= " + height); - VideoCallUi ui = getUi(); - if (ui == null) { - Log.e(this, "VideoCallUi is null. Bail out"); - return; - } - if (!call.equals(mPrimaryCall)) { - Log.e(this, "Current call is not equal to primary call. Bail out"); - return; - } - - // Change size of display surface to match the peer aspect ratio - if (width > 0 && height > 0) { - setDisplayVideoSize(width, height); - } - } - - /** - * Handles any video quality changes in the call. - * - * @param call The call which experienced a video quality change. - * @param videoQuality The new video call quality. - */ - @Override - public void onVideoQualityChanged(Call call, int videoQuality) { - // No-op - } - - /** - * Handles a change to the dimensions of the local camera. Receiving the camera capabilities - * triggers the creation of the video - * - * @param call The call which experienced the camera dimension change. - * @param width The new camera video width. - * @param height The new camera video height. - */ - @Override - public void onCameraDimensionsChange(Call call, int width, int height) { - Log.d(this, "onCameraDimensionsChange call=" + call + " width=" + width + " height=" - + height); - VideoCallUi ui = getUi(); - if (ui == null) { - Log.e(this, "onCameraDimensionsChange ui is null"); - return; - } - - if (!call.equals(mPrimaryCall)) { - Log.e(this, "Call is not primary call"); - return; - } - - mPreviewSurfaceState = PreviewSurfaceState.CAPABILITIES_RECEIVED; - changePreviewDimensions(width, height); - - // Check if the preview surface is ready yet; if it is, set it on the {@code VideoCall}. - // If it not yet ready, it will be set when when creation completes. - if (ui.isPreviewVideoSurfaceCreated()) { - mPreviewSurfaceState = PreviewSurfaceState.SURFACE_SET; - mVideoCall.setPreviewSurface(ui.getPreviewVideoSurface()); - } - } - - /** - * Changes the dimensions of the preview surface. - * - * @param width The new width. - * @param height The new height. - */ - private void changePreviewDimensions(int width, int height) { - VideoCallUi ui = getUi(); - if (ui == null) { - return; - } - - // Resize the surface used to display the preview video - ui.setPreviewSurfaceSize(width, height); - - // Configure the preview surface to the correct aspect ratio. - float aspectRatio = 1.0f; - if (width > 0 && height > 0) { - aspectRatio = (float) width / (float) height; - } - - // Resize the textureview housing the preview video and rotate it appropriately based on - // the device orientation - setPreviewSize(mDeviceOrientation, aspectRatio); - } - - /** - * Called when call session event is raised. - * - * @param event The call session event. - */ - @Override - public void onCallSessionEvent(int event) { - StringBuilder sb = new StringBuilder(); - sb.append("onCallSessionEvent = "); - - switch (event) { - case Connection.VideoProvider.SESSION_EVENT_RX_PAUSE: - sb.append("rx_pause"); - break; - case Connection.VideoProvider.SESSION_EVENT_RX_RESUME: - sb.append("rx_resume"); - break; - case Connection.VideoProvider.SESSION_EVENT_CAMERA_FAILURE: - sb.append("camera_failure"); - break; - case Connection.VideoProvider.SESSION_EVENT_CAMERA_READY: - sb.append("camera_ready"); - break; - default: - sb.append("unknown event = "); - sb.append(event); - break; - } - Log.d(this, sb.toString()); - } - - /** - * Handles a change to the call data usage - * - * @param dataUsage call data usage value - */ - @Override - public void onCallDataUsageChange(long dataUsage) { - Log.d(this, "onCallDataUsageChange dataUsage=" + dataUsage); - } - - /** - * Handles changes to the device orientation. - * @param orientation The screen orientation of the device (one of: - * {@link InCallOrientationEventListener#SCREEN_ORIENTATION_0}, - * {@link InCallOrientationEventListener#SCREEN_ORIENTATION_90}, - * {@link InCallOrientationEventListener#SCREEN_ORIENTATION_180}, - * {@link InCallOrientationEventListener#SCREEN_ORIENTATION_270}). - */ - @Override - public void onDeviceOrientationChanged(int orientation) { - mDeviceOrientation = orientation; - - VideoCallUi ui = getUi(); - if (ui == null) { - Log.e(this, "onDeviceOrientationChanged: VideoCallUi is null"); - return; - } - - Point previewDimensions = ui.getPreviewSize(); - if (previewDimensions == null) { - return; - } - Log.d(this, "onDeviceOrientationChanged: orientation=" + orientation + " size: " - + previewDimensions); - changePreviewDimensions(previewDimensions.x, previewDimensions.y); - - ui.setPreviewRotation(mDeviceOrientation); - } - - /** - * Sets the preview surface size based on the current device orientation. - * See: {@link InCallOrientationEventListener#SCREEN_ORIENTATION_0}, - * {@link InCallOrientationEventListener#SCREEN_ORIENTATION_90}, - * {@link InCallOrientationEventListener#SCREEN_ORIENTATION_180}, - * {@link InCallOrientationEventListener#SCREEN_ORIENTATION_270}). - * - * @param orientation The device orientation - * @param aspectRatio The aspect ratio of the camera (width / height). - */ - private void setPreviewSize(int orientation, float aspectRatio) { - VideoCallUi ui = getUi(); - if (ui == null) { - return; - } - - int height; - int width; - - if (orientation == InCallOrientationEventListener.SCREEN_ORIENTATION_90 || - orientation == InCallOrientationEventListener.SCREEN_ORIENTATION_270) { - width = (int) (mMinimumVideoDimension * aspectRatio); - height = (int) mMinimumVideoDimension; - } else { - // Portrait or reverse portrait orientation. - width = (int) mMinimumVideoDimension; - height = (int) (mMinimumVideoDimension * aspectRatio); - } - ui.setPreviewSize(width, height); - } - - /** - * Sets the display video surface size based on peer width and height - * - * @param width peer width - * @param height peer height - */ - private void setDisplayVideoSize(int width, int height) { - Log.v(this, "setDisplayVideoSize: Received peer width=" + width + " height=" + height); - VideoCallUi ui = getUi(); - if (ui == null) { - return; - } - - // Get current display size - Point size = ui.getScreenSize(); - Log.v(this, "setDisplayVideoSize: windowmgr width=" + size.x - + " windowmgr height=" + size.y); - if (size.y * width > size.x * height) { - // current display height is too much. Correct it - size.y = (int) (size.x * height / width); - } else if (size.y * width < size.x * height) { - // current display width is too much. Correct it - size.x = (int) (size.y * width / height); - } - ui.setDisplayVideoSize(size.x, size.y); - } - - /** - * Exits fullscreen mode if the current call context has changed to a non-video call. - * - * @param call The call. - */ - protected void maybeExitFullscreen(Call call) { - if (call == null) { - return; - } - - if (!VideoUtils.isVideoCall(call) || call.getState() == Call.State.INCOMING) { - InCallPresenter.getInstance().setFullScreen(false); - } - } - - /** - * Schedules auto-entering of fullscreen mode. - * Will not enter full screen mode if any of the following conditions are met: - * 1. No call - * 2. Call is not active - * 3. Call is not video call - * 4. Already in fullscreen mode - * 5. The current video state is not bi-directional (if the remote party stops transmitting, - * the user's contact photo would dominate in fullscreen mode). - * - * @param call The current call. - */ - protected void maybeAutoEnterFullscreen(Call call) { - if (!mIsAutoFullscreenEnabled) { - return; - } - - if (call == null || ( - call != null && (call.getState() != Call.State.ACTIVE || - !VideoUtils.isVideoCall(call)) || - InCallPresenter.getInstance().isFullscreen()) || - !VideoUtils.isBidirectionalVideoCall(call)) { - // Ensure any previously scheduled attempt to enter fullscreen is cancelled. - cancelAutoFullScreen(); - return; - } - - if (mAutoFullScreenPending) { - Log.v(this, "maybeAutoEnterFullscreen : already pending."); - return; - } - Log.v(this, "maybeAutoEnterFullscreen : scheduled"); - mAutoFullScreenPending = true; - mHandler.postDelayed(mAutoFullscreenRunnable, mAutoFullscreenTimeoutMillis); - } - - /** - * Cancels pending auto fullscreen mode. - */ - public void cancelAutoFullScreen() { - if (!mAutoFullScreenPending) { - Log.v(this, "cancelAutoFullScreen : none pending."); - return; - } - Log.v(this, "cancelAutoFullScreen : cancelling pending"); - mAutoFullScreenPending = false; - } - - private static void updateCameraSelection(Call call) { - Log.d(TAG, "updateCameraSelection: call=" + call); - Log.d(TAG, "updateCameraSelection: call=" + toSimpleString(call)); - - final Call activeCall = CallList.getInstance().getActiveCall(); - int cameraDir = Call.VideoSettings.CAMERA_DIRECTION_UNKNOWN; - - // this function should never be called with null call object, however if it happens we - // should handle it gracefully. - if (call == null) { - cameraDir = Call.VideoSettings.CAMERA_DIRECTION_UNKNOWN; - com.android.incallui.Log.e(TAG, "updateCameraSelection: Call object is null." - + " Setting camera direction to default value (CAMERA_DIRECTION_UNKNOWN)"); - } - - // Clear camera direction if this is not a video call. - else if (VideoUtils.isAudioCall(call)) { - cameraDir = Call.VideoSettings.CAMERA_DIRECTION_UNKNOWN; - call.getVideoSettings().setCameraDir(cameraDir); - } - - // If this is a waiting video call, default to active call's camera, - // since we don't want to change the current camera for waiting call - // without user's permission. - else if (VideoUtils.isVideoCall(activeCall) && VideoUtils.isIncomingVideoCall(call)) { - cameraDir = activeCall.getVideoSettings().getCameraDir(); - } - - // Infer the camera direction from the video state and store it, - // if this is an outgoing video call. - else if (VideoUtils.isOutgoingVideoCall(call) && !isCameraDirectionSet(call) ) { - cameraDir = toCameraDirection(call.getVideoState()); - call.getVideoSettings().setCameraDir(cameraDir); - } - - // Use the stored camera dir if this is an outgoing video call for which camera direction - // is set. - else if (VideoUtils.isOutgoingVideoCall(call)) { - cameraDir = call.getVideoSettings().getCameraDir(); - } - - // Infer the camera direction from the video state and store it, - // if this is an active video call and camera direction is not set. - else if (VideoUtils.isActiveVideoCall(call) && !isCameraDirectionSet(call)) { - cameraDir = toCameraDirection(call.getVideoState()); - call.getVideoSettings().setCameraDir(cameraDir); - } - - // Use the stored camera dir if this is an active video call for which camera direction - // is set. - else if (VideoUtils.isActiveVideoCall(call)) { - cameraDir = call.getVideoSettings().getCameraDir(); - } - - // For all other cases infer the camera direction but don't store it in the call object. - else { - cameraDir = toCameraDirection(call.getVideoState()); - } - - com.android.incallui.Log.d(TAG, "updateCameraSelection: Setting camera direction to " + - cameraDir + " Call=" + call); - final InCallCameraManager cameraManager = InCallPresenter.getInstance(). - getInCallCameraManager(); - cameraManager.setUseFrontFacingCamera(cameraDir == - Call.VideoSettings.CAMERA_DIRECTION_FRONT_FACING); - } - - private static int toCameraDirection(int videoState) { - return VideoProfile.isTransmissionEnabled(videoState) && - !VideoProfile.isBidirectional(videoState) - ? Call.VideoSettings.CAMERA_DIRECTION_BACK_FACING - : Call.VideoSettings.CAMERA_DIRECTION_FRONT_FACING; - } - - private static boolean isCameraDirectionSet(Call call) { - return VideoUtils.isVideoCall(call) && call.getVideoSettings().getCameraDir() - != Call.VideoSettings.CAMERA_DIRECTION_UNKNOWN; - } - - private static String toSimpleString(Call call) { - return call == null ? null : call.toSimpleString(); - } - - /** - * Starts an asynchronous load of the user's profile photo. - */ - public void loadProfilePhotoAsync() { - final VideoCallUi ui = getUi(); - if (ui == null) { - return; - } - - final AsyncTask task = new AsyncTask() { - /** - * Performs asynchronous load of the user profile information. - * - * @param params The parameters of the task. - * - * @return {@code null}. - */ - @Override - protected Void doInBackground(Void... params) { - if (mProfileInfo == null) { - // Try and read the photo URI from the local profile. - mProfileInfo = new ContactInfoCache.ContactCacheEntry(); - final Cursor cursor = mContext.getContentResolver().query( - ContactsContract.Profile.CONTENT_URI, new String[]{ - ContactsContract.CommonDataKinds.Phone._ID, - ContactsContract.CommonDataKinds.Phone.PHOTO_URI, - ContactsContract.CommonDataKinds.Phone.LOOKUP_KEY, - ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME, - ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME_ALTERNATIVE - }, null, null, null); - if (cursor != null) { - try { - if (cursor.moveToFirst()) { - mProfileInfo.lookupKey = cursor.getString(cursor.getColumnIndex( - ContactsContract.CommonDataKinds.Phone.LOOKUP_KEY)); - String photoUri = cursor.getString(cursor.getColumnIndex( - ContactsContract.CommonDataKinds.Phone.PHOTO_URI)); - mProfileInfo.displayPhotoUri = photoUri == null ? null - : Uri.parse(photoUri); - mProfileInfo.namePrimary = cursor.getString(cursor.getColumnIndex( - ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)); - mProfileInfo.nameAlternative = cursor.getString( - cursor.getColumnIndex(ContactsContract.CommonDataKinds - .Phone.DISPLAY_NAME_ALTERNATIVE)); - } - } finally { - cursor.close(); - } - } - } - return null; - } - - @Override - protected void onPostExecute(Void result) { - // If user profile information was found, issue an async request to load the user's - // profile photo. - if (mProfileInfo != null) { - if (mContactPhotoManager == null) { - mContactPhotoManager = ContactPhotoManager.getInstance(mContext); - } - ContactPhotoManager.DefaultImageRequest imageRequest = (mProfileInfo != null) - ? null : - new ContactPhotoManager.DefaultImageRequest(mProfileInfo.namePrimary, - mProfileInfo.lookupKey, false /* isCircularPhoto */); - - ImageView photoView = ui.getPreviewPhotoView(); - if (photoView == null) { - return; - } - mContactPhotoManager.loadDirectoryPhoto(photoView, - mProfileInfo.displayPhotoUri, - false /* darkTheme */, false /* isCircular */, imageRequest); - } - } - }; - - task.execute(); - } - - /** - * Defines the VideoCallUI interactions. - */ - public interface VideoCallUi extends Ui { - void showVideoViews(boolean showPreview, boolean showIncoming); - void hideVideoUi(); - boolean isDisplayVideoSurfaceCreated(); - boolean isPreviewVideoSurfaceCreated(); - Surface getDisplayVideoSurface(); - Surface getPreviewVideoSurface(); - int getCurrentRotation(); - void setPreviewSize(int width, int height); - void setPreviewSurfaceSize(int width, int height); - void setDisplayVideoSize(int width, int height); - Point getScreenSize(); - Point getPreviewSize(); - void cleanupSurfaces(); - ImageView getPreviewPhotoView(); - void adjustPreviewLocation(boolean shiftUp, int offset); - void setPreviewRotation(int orientation); - } -} diff --git a/InCallUI/src/com/android/incallui/VideoPauseController.java b/InCallUI/src/com/android/incallui/VideoPauseController.java deleted file mode 100644 index fb873500e..000000000 --- a/InCallUI/src/com/android/incallui/VideoPauseController.java +++ /dev/null @@ -1,420 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import com.android.incallui.Call.State; -import com.android.incallui.InCallPresenter.InCallState; -import com.android.incallui.InCallPresenter.InCallStateListener; -import com.android.incallui.InCallPresenter.IncomingCallListener; -import com.android.incallui.InCallVideoCallCallbackNotifier.SessionModificationListener; -import com.google.common.base.Preconditions; - -import android.telecom.VideoProfile; - -/** - * This class is responsible for generating video pause/resume requests when the InCall UI is sent - * to the background and subsequently brought back to the foreground. - */ -class VideoPauseController implements InCallStateListener, IncomingCallListener { - private static final String TAG = "VideoPauseController"; - - /** - * Keeps track of the current active/foreground call. - */ - private class CallContext { - public CallContext(Call call) { - Preconditions.checkNotNull(call); - update(call); - } - - public void update(Call call) { - mCall = Preconditions.checkNotNull(call); - mState = call.getState(); - mVideoState = call.getVideoState(); - } - - public int getState() { - return mState; - } - - public int getVideoState() { - return mVideoState; - } - - public String toString() { - return String.format("CallContext {CallId=%s, State=%s, VideoState=%d}", - mCall.getId(), mState, mVideoState); - } - - public Call getCall() { - return mCall; - } - - private int mState = State.INVALID; - private int mVideoState; - private Call mCall; - } - - private InCallPresenter mInCallPresenter; - private static VideoPauseController sVideoPauseController; - - /** - * The current call context, if applicable. - */ - private CallContext mPrimaryCallContext = null; - - /** - * Tracks whether the application is in the background. {@code True} if the application is in - * the background, {@code false} otherwise. - */ - private boolean mIsInBackground = false; - - /** - * Singleton accessor for the {@link VideoPauseController}. - * @return Singleton instance of the {@link VideoPauseController}. - */ - /*package*/ - static synchronized VideoPauseController getInstance() { - if (sVideoPauseController == null) { - sVideoPauseController = new VideoPauseController(); - } - return sVideoPauseController; - } - - /** - * Configures the {@link VideoPauseController} to listen to call events. Configured via the - * {@link com.android.incallui.InCallPresenter}. - * - * @param inCallPresenter The {@link com.android.incallui.InCallPresenter}. - */ - public void setUp(InCallPresenter inCallPresenter) { - log("setUp"); - mInCallPresenter = Preconditions.checkNotNull(inCallPresenter); - mInCallPresenter.addListener(this); - mInCallPresenter.addIncomingCallListener(this); - } - - /** - * Cleans up the {@link VideoPauseController} by removing all listeners and clearing its - * internal state. Called from {@link com.android.incallui.InCallPresenter}. - */ - public void tearDown() { - log("tearDown..."); - mInCallPresenter.removeListener(this); - mInCallPresenter.removeIncomingCallListener(this); - clear(); - } - - /** - * Clears the internal state for the {@link VideoPauseController}. - */ - private void clear() { - mInCallPresenter = null; - mPrimaryCallContext = null; - mIsInBackground = false; - } - - /** - * Handles changes in the {@link InCallState}. Triggers pause and resumption of video for the - * current foreground call. - * - * @param oldState The previous {@link InCallState}. - * @param newState The current {@link InCallState}. - * @param callList List of current call. - */ - @Override - public void onStateChange(InCallState oldState, InCallState newState, CallList callList) { - log("onStateChange, OldState=" + oldState + " NewState=" + newState); - - Call call = null; - if (newState == InCallState.INCOMING) { - call = callList.getIncomingCall(); - } else if (newState == InCallState.WAITING_FOR_ACCOUNT) { - call = callList.getWaitingForAccountCall(); - } else if (newState == InCallState.PENDING_OUTGOING) { - call = callList.getPendingOutgoingCall(); - } else if (newState == InCallState.OUTGOING) { - call = callList.getOutgoingCall(); - } else { - call = callList.getActiveCall(); - } - - boolean hasPrimaryCallChanged = !areSame(call, mPrimaryCallContext); - boolean canVideoPause = VideoUtils.canVideoPause(call); - log("onStateChange, hasPrimaryCallChanged=" + hasPrimaryCallChanged); - log("onStateChange, canVideoPause=" + canVideoPause); - log("onStateChange, IsInBackground=" + mIsInBackground); - - if (hasPrimaryCallChanged) { - onPrimaryCallChanged(call); - return; - } - - if (isDialing(mPrimaryCallContext) && canVideoPause && mIsInBackground) { - // Bring UI to foreground if outgoing request becomes active while UI is in - // background. - bringToForeground(); - } else if (!isVideoCall(mPrimaryCallContext) && canVideoPause && mIsInBackground) { - // Bring UI to foreground if VoLTE call becomes active while UI is in - // background. - bringToForeground(); - } - - updatePrimaryCallContext(call); - } - - /** - * Handles a change to the primary call. - *

- * Reject incoming or hangup dialing call: Where the previous call was an incoming call or a - * call in dialing state, resume the new primary call. - * Call swap: Where the new primary call is incoming, pause video on the previous primary call. - * - * @param call The new primary call. - */ - private void onPrimaryCallChanged(Call call) { - log("onPrimaryCallChanged: New call = " + call); - log("onPrimaryCallChanged: Old call = " + mPrimaryCallContext); - log("onPrimaryCallChanged, IsInBackground=" + mIsInBackground); - - Preconditions.checkState(!areSame(call, mPrimaryCallContext)); - final boolean canVideoPause = VideoUtils.canVideoPause(call); - - if ((isIncomingCall(mPrimaryCallContext) || isDialing(mPrimaryCallContext) || - (call != null && VideoProfile.isPaused(call.getVideoState()))) - && canVideoPause && !mIsInBackground) { - // Send resume request for the active call, if user rejects incoming call, ends dialing - // call, or the call was previously in a paused state and UI is in the foreground. - sendRequest(call, true); - } else if (isIncomingCall(call) && canVideoPause(mPrimaryCallContext)) { - // Send pause request if there is an active video call, and we just received a new - // incoming call. - sendRequest(mPrimaryCallContext.getCall(), false); - } - - updatePrimaryCallContext(call); - } - - /** - * Handles new incoming calls by triggering a change in the primary call. - * - * @param oldState the old {@link InCallState}. - * @param newState the new {@link InCallState}. - * @param call the incoming call. - */ - @Override - public void onIncomingCall(InCallState oldState, InCallState newState, Call call) { - log("onIncomingCall, OldState=" + oldState + " NewState=" + newState + " Call=" + call); - - if (areSame(call, mPrimaryCallContext)) { - return; - } - - onPrimaryCallChanged(call); - } - - /** - * Caches a reference to the primary call and stores its previous state. - * - * @param call The new primary call. - */ - private void updatePrimaryCallContext(Call call) { - if (call == null) { - mPrimaryCallContext = null; - } else if (mPrimaryCallContext != null) { - mPrimaryCallContext.update(call); - } else { - mPrimaryCallContext = new CallContext(call); - } - } - - /** - * Called when UI goes in/out of the foreground. - * @param showing true if UI is in the foreground, false otherwise. - */ - public void onUiShowing(boolean showing) { - // Only send pause/unpause requests if we are in the INCALL state. - if (mInCallPresenter == null) { - return; - } - final boolean isInCall = mInCallPresenter.getInCallState() == InCallState.INCALL; - if (showing) { - onResume(isInCall); - } else { - onPause(isInCall); - } - } - - /** - * Called when UI is brought to the foreground. Sends a session modification request to resume - * the outgoing video. - * @param isInCall true if phone state is INCALL, false otherwise - */ - private void onResume(boolean isInCall) { - log("onResume"); - - mIsInBackground = false; - if (canVideoPause(mPrimaryCallContext) && isInCall) { - sendRequest(mPrimaryCallContext.getCall(), true); - } else { - log("onResume. Ignoring..."); - } - } - - /** - * Called when UI is sent to the background. Sends a session modification request to pause the - * outgoing video. - * @param isInCall true if phone state is INCALL, false otherwise - */ - private void onPause(boolean isInCall) { - log("onPause"); - - mIsInBackground = true; - if (canVideoPause(mPrimaryCallContext) && isInCall) { - sendRequest(mPrimaryCallContext.getCall(), false); - } else { - log("onPause, Ignoring..."); - } - } - - private void bringToForeground() { - if (mInCallPresenter != null) { - log("Bringing UI to foreground"); - mInCallPresenter.bringToForeground(false); - } else { - loge("InCallPresenter is null. Cannot bring UI to foreground"); - } - } - - /** - * Sends Pause/Resume request. - * - * @param call Call to be paused/resumed. - * @param resume If true resume request will be sent, otherwise pause request. - */ - private void sendRequest(Call call, boolean resume) { - // Check if this call supports pause/un-pause. - if (!call.can(android.telecom.Call.Details.CAPABILITY_CAN_PAUSE_VIDEO)) { - return; - } - - if (resume) { - log("sending resume request, call=" + call); - call.getVideoCall() - .sendSessionModifyRequest(VideoUtils.makeVideoUnPauseProfile(call)); - } else { - log("sending pause request, call=" + call); - call.getVideoCall().sendSessionModifyRequest(VideoUtils.makeVideoPauseProfile(call)); - } - } - - /** - * Determines if a given call is the same one stored in a {@link CallContext}. - * - * @param call The call. - * @param callContext The call context. - * @return {@code true} if the {@link Call} is the same as the one referenced in the - * {@link CallContext}. - */ - private static boolean areSame(Call call, CallContext callContext) { - if (call == null && callContext == null) { - return true; - } else if (call == null || callContext == null) { - return false; - } - return call.equals(callContext.getCall()); - } - - /** - * Determines if a video call can be paused. Only a video call which is active can be paused. - * - * @param callContext The call context to check. - * @return {@code true} if the call is an active video call. - */ - private static boolean canVideoPause(CallContext callContext) { - return isVideoCall(callContext) && callContext.getState() == Call.State.ACTIVE; - } - - /** - * Determines if a call referenced by a {@link CallContext} is a video call. - * - * @param callContext The call context. - * @return {@code true} if the call is a video call, {@code false} otherwise. - */ - private static boolean isVideoCall(CallContext callContext) { - return callContext != null && VideoUtils.isVideoCall(callContext.getVideoState()); - } - - /** - * Determines if call is in incoming/waiting state. - * - * @param call The call context. - * @return {@code true} if the call is in incoming or waiting state, {@code false} otherwise. - */ - private static boolean isIncomingCall(CallContext call) { - return call != null && isIncomingCall(call.getCall()); - } - - /** - * Determines if a call is in incoming/waiting state. - * - * @param call The call. - * @return {@code true} if the call is in incoming or waiting state, {@code false} otherwise. - */ - private static boolean isIncomingCall(Call call) { - return call != null && (call.getState() == Call.State.CALL_WAITING - || call.getState() == Call.State.INCOMING); - } - - /** - * Determines if a call is dialing. - * - * @param call The call context. - * @return {@code true} if the call is dialing, {@code false} otherwise. - */ - private static boolean isDialing(CallContext call) { - return call != null && Call.State.isDialing(call.getState()); - } - - /** - * Determines if a call is holding. - * - * @param call The call context. - * @return {@code true} if the call is holding, {@code false} otherwise. - */ - private static boolean isHolding(CallContext call) { - return call != null && call.getState() == Call.State.ONHOLD; - } - - /** - * Logs a debug message. - * - * @param msg The message. - */ - private void log(String msg) { - Log.d(this, TAG + msg); - } - - /** - * Logs an error message. - * - * @param msg The message. - */ - private void loge(String msg) { - Log.e(this, TAG + msg); - } -} diff --git a/InCallUI/src/com/android/incallui/VideoUtils.java b/InCallUI/src/com/android/incallui/VideoUtils.java deleted file mode 100644 index a2eb8bcf2..000000000 --- a/InCallUI/src/com/android/incallui/VideoUtils.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.telecom.VideoProfile; - -import com.android.contacts.common.compat.CompatUtils; - -import com.google.common.base.Preconditions; - -public class VideoUtils { - - public static boolean isVideoCall(Call call) { - return call != null && isVideoCall(call.getVideoState()); - } - - public static boolean isVideoCall(int videoState) { - if (!CompatUtils.isVideoCompatible()) { - return false; - } - - return VideoProfile.isTransmissionEnabled(videoState) - || VideoProfile.isReceptionEnabled(videoState); - } - - public static boolean isBidirectionalVideoCall(Call call) { - if (!CompatUtils.isVideoCompatible()) { - return false; - } - - return VideoProfile.isBidirectional(call.getVideoState()); - } - - public static boolean isTransmissionEnabled(Call call) { - if (!CompatUtils.isVideoCompatible()) { - return false; - } - - return VideoProfile.isTransmissionEnabled(call.getVideoState()); - } - - public static boolean isIncomingVideoCall(Call call) { - if (!VideoUtils.isVideoCall(call)) { - return false; - } - final int state = call.getState(); - return (state == Call.State.INCOMING) || (state == Call.State.CALL_WAITING); - } - - public static boolean isActiveVideoCall(Call call) { - return VideoUtils.isVideoCall(call) && call.getState() == Call.State.ACTIVE; - } - - public static boolean isOutgoingVideoCall(Call call) { - if (!VideoUtils.isVideoCall(call)) { - return false; - } - final int state = call.getState(); - return Call.State.isDialing(state) || state == Call.State.CONNECTING - || state == Call.State.SELECT_PHONE_ACCOUNT; - } - - public static boolean isAudioCall(Call call) { - if (!CompatUtils.isVideoCompatible()) { - return true; - } - - return call != null && VideoProfile.isAudioOnly(call.getVideoState()); - } - - // TODO (ims-vt) Check if special handling is needed for CONF calls. - public static boolean canVideoPause(Call call) { - return isVideoCall(call) && call.getState() == Call.State.ACTIVE; - } - - public static VideoProfile makeVideoPauseProfile(Call call) { - Preconditions.checkNotNull(call); - Preconditions.checkState(!VideoProfile.isAudioOnly(call.getVideoState())); - return new VideoProfile(getPausedVideoState(call.getVideoState())); - } - - public static VideoProfile makeVideoUnPauseProfile(Call call) { - Preconditions.checkNotNull(call); - return new VideoProfile(getUnPausedVideoState(call.getVideoState())); - } - - public static int getUnPausedVideoState(int videoState) { - return videoState & (~VideoProfile.STATE_PAUSED); - } - - public static int getPausedVideoState(int videoState) { - return videoState | VideoProfile.STATE_PAUSED; - } - -} diff --git a/InCallUI/src/com/android/incallui/async/PausableExecutor.java b/InCallUI/src/com/android/incallui/async/PausableExecutor.java deleted file mode 100644 index 1b8201a79..000000000 --- a/InCallUI/src/com/android/incallui/async/PausableExecutor.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui.async; - -import com.android.contacts.common.testing.NeededForTesting; - -import java.util.concurrent.Executor; - -/** - * Executor that can be used to easily synchronize testing and production code. Production code - * should call {@link #milestone()} at points in the code where the state of the system is worthy of - * testing. In a test scenario, this method will pause execution until the test acknowledges the - * milestone through the use of {@link #ackMilestoneForTesting()}. - */ -public interface PausableExecutor extends Executor { - - /** - * Method called from asynchronous production code to inform this executor that it has - * reached a point that puts the system into a state worth testing. TestableExecutors intended - * for use in a testing environment should cause the calling thread to block. In the production - * environment this should be a no-op. - */ - void milestone(); - - /** - * Method called from the test code to inform this executor that the state of the production - * system at the current milestone has been sufficiently tested. Every milestone must be - * acknowledged. - */ - @NeededForTesting - void ackMilestoneForTesting(); - - /** - * Method called from the test code to inform this executor that the tests are finished with all - * milestones. Future calls to {@link #milestone()} or {@link #awaitMilestoneForTesting()} - * should return immediately. - */ - @NeededForTesting - void ackAllMilestonesForTesting(); - - /** - * Method called from the test code to block until a milestone has been reached in the - * production code. - */ - @NeededForTesting - void awaitMilestoneForTesting() throws InterruptedException; -} diff --git a/InCallUI/src/com/android/incallui/async/PausableExecutorImpl.java b/InCallUI/src/com/android/incallui/async/PausableExecutorImpl.java deleted file mode 100644 index 15900e57b..000000000 --- a/InCallUI/src/com/android/incallui/async/PausableExecutorImpl.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui.async; - -import java.util.concurrent.Executors; - -/** - * {@link PausableExecutor} intended for use in production environments. - */ -public class PausableExecutorImpl implements PausableExecutor { - - @Override - public void milestone() {} - - @Override - public void ackMilestoneForTesting() {} - - @Override - public void ackAllMilestonesForTesting() {} - - @Override - public void awaitMilestoneForTesting() {} - - @Override - public void execute(Runnable command) { - Executors.newSingleThreadExecutor().execute(command); - } -} diff --git a/InCallUI/src/com/android/incallui/ringtone/DialerRingtoneManager.java b/InCallUI/src/com/android/incallui/ringtone/DialerRingtoneManager.java deleted file mode 100644 index 39844e5a2..000000000 --- a/InCallUI/src/com/android/incallui/ringtone/DialerRingtoneManager.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui.ringtone; - -import com.google.common.base.Preconditions; - -import android.content.ContentResolver; -import android.net.Uri; -import android.provider.Settings; -import android.support.annotation.Nullable; - -import com.android.contacts.common.compat.CompatUtils; -import com.android.contacts.common.testing.NeededForTesting; -import com.android.incallui.Call; -import com.android.incallui.Call.State; -import com.android.incallui.CallList; - -/** - * Class that determines when ringtones should be played and can play the call waiting tone when - * necessary. - */ -public class DialerRingtoneManager { - - /* - * Flag used to determine if the Dialer is responsible for playing ringtones for incoming calls. - * Once we're ready to enable Dialer Ringing, these flags should be removed. - */ - private static final boolean IS_DIALER_RINGING_ENABLED = false; - private Boolean mIsDialerRingingEnabledForTesting; - - private final InCallTonePlayer mInCallTonePlayer; - private final CallList mCallList; - - /** - * Creates the DialerRingtoneManager with the given {@link InCallTonePlayer}. - * - * @param inCallTonePlayer the tone player used to play in-call tones. - * @param callList the CallList used to check for {@link State#CALL_WAITING} - * @throws NullPointerException if inCallTonePlayer or callList are null - */ - public DialerRingtoneManager(InCallTonePlayer inCallTonePlayer, CallList callList) { - mInCallTonePlayer = Preconditions.checkNotNull(inCallTonePlayer); - mCallList = Preconditions.checkNotNull(callList); - } - - /** - * Determines if a ringtone should be played for the given call state (see {@link State}) and - * {@link Uri}. - * - * @param callState the call state for the call being checked. - * @param ringtoneUri the ringtone to potentially play. - * @return {@code true} if the ringtone should be played, {@code false} otherwise. - */ - public boolean shouldPlayRingtone(int callState, @Nullable Uri ringtoneUri) { - return isDialerRingingEnabled() - && translateCallStateForCallWaiting(callState) == State.INCOMING - && ringtoneUri != null; - } - - /** - * Determines if an incoming call should vibrate as well as ring. - * - * @param resolver {@link ContentResolver} used to look up the - * {@link Settings.System#VIBRATE_WHEN_RINGING} setting. - * @return {@code true} if the call should vibrate, {@code false} otherwise. - */ - public boolean shouldVibrate(ContentResolver resolver) { - return Settings.System.getInt(resolver, Settings.System.VIBRATE_WHEN_RINGING, 0) != 0; - } - - /** - * The incoming callState is never set as {@link State#CALL_WAITING} because - * {@link Call#translateState(int)} doesn't account for that case, check for it here - */ - private int translateCallStateForCallWaiting(int callState) { - if (callState != State.INCOMING) { - return callState; - } - return mCallList.getActiveCall() == null ? State.INCOMING : State.CALL_WAITING; - } - - private boolean isDialerRingingEnabled() { - if (mIsDialerRingingEnabledForTesting != null) { - return mIsDialerRingingEnabledForTesting; - } - return CompatUtils.isNCompatible() && IS_DIALER_RINGING_ENABLED; - } - - /** - * Determines if a call waiting tone should be played for the the given call state - * (see {@link State}). - * - * @param callState the call state for the call being checked. - * @return {@code true} if the call waiting tone should be played, {@code false} otherwise. - */ - public boolean shouldPlayCallWaitingTone(int callState) { - return isDialerRingingEnabled() - && translateCallStateForCallWaiting(callState) == State.CALL_WAITING - && !mInCallTonePlayer.isPlayingTone(); - } - - /** - * Plays the call waiting tone. - */ - public void playCallWaitingTone() { - if (!isDialerRingingEnabled()) { - return; - } - mInCallTonePlayer.play(InCallTonePlayer.TONE_CALL_WAITING); - } - - /** - * Stops playing the call waiting tone. - */ - public void stopCallWaitingTone() { - if (!isDialerRingingEnabled()) { - return; - } - mInCallTonePlayer.stop(); - } - - @NeededForTesting - void setDialerRingingEnabledForTesting(boolean status) { - mIsDialerRingingEnabledForTesting = status; - } -} diff --git a/InCallUI/src/com/android/incallui/ringtone/InCallTonePlayer.java b/InCallUI/src/com/android/incallui/ringtone/InCallTonePlayer.java deleted file mode 100644 index 3a8b03d91..000000000 --- a/InCallUI/src/com/android/incallui/ringtone/InCallTonePlayer.java +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui.ringtone; - -import com.google.common.base.MoreObjects; -import com.google.common.base.Preconditions; - -import android.media.AudioManager; -import android.media.ToneGenerator; -import android.support.annotation.Nullable; - -import com.android.incallui.Log; -import com.android.incallui.async.PausableExecutor; - -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -/** - * Class responsible for playing in-call related tones in a background thread. This class only - * allows one tone to be played at a time. - */ -public class InCallTonePlayer { - - public static final int TONE_CALL_WAITING = 4; - - public static final int VOLUME_RELATIVE_HIGH_PRIORITY = 80; - - private final ToneGeneratorFactory mToneGeneratorFactory; - private final PausableExecutor mExecutor; - private @Nullable CountDownLatch mNumPlayingTones; - - /** - * Creates a new InCallTonePlayer. - * - * @param toneGeneratorFactory the {@link ToneGeneratorFactory} used to create - * {@link ToneGenerator}s. - * @param executor the {@link PausableExecutor} used to play tones in a background thread. - * @throws NullPointerException if audioModeProvider, toneGeneratorFactory, or executor are - * {@code null}. - */ - public InCallTonePlayer(ToneGeneratorFactory toneGeneratorFactory, PausableExecutor executor) { - mToneGeneratorFactory = Preconditions.checkNotNull(toneGeneratorFactory); - mExecutor = Preconditions.checkNotNull(executor); - } - - /** - * @return {@code true} if a tone is currently playing, {@code false} otherwise. - */ - public boolean isPlayingTone() { - return mNumPlayingTones != null && mNumPlayingTones.getCount() > 0; - } - - /** - * Plays the given tone in a background thread. - * - * @param tone the tone to play. - * @throws IllegalStateException if a tone is already playing. - * @throws IllegalArgumentException if the tone is invalid. - */ - public void play(int tone) { - if (isPlayingTone()) { - throw new IllegalStateException("Tone already playing"); - } - final ToneGeneratorInfo info = getToneGeneratorInfo(tone); - mNumPlayingTones = new CountDownLatch(1); - mExecutor.execute(new Runnable() { - @Override - public void run() { - playOnBackgroundThread(info); - } - }); - } - - private ToneGeneratorInfo getToneGeneratorInfo(int tone) { - switch (tone) { - case TONE_CALL_WAITING: - /* - * Call waiting tones play until they're stopped either by the user accepting or - * declining the call so the tone length is set at what's effectively forever. The - * tone is played at a high priority volume and through STREAM_VOICE_CALL since it's - * call related and using that stream will route it through bluetooth devices - * appropriately. - */ - return new ToneGeneratorInfo(ToneGenerator.TONE_SUP_CALL_WAITING, - VOLUME_RELATIVE_HIGH_PRIORITY, - Integer.MAX_VALUE, - AudioManager.STREAM_VOICE_CALL); - default: - throw new IllegalArgumentException("Bad tone: " + tone); - } - } - - private void playOnBackgroundThread(ToneGeneratorInfo info) { - ToneGenerator toneGenerator = null; - try { - Log.v(this, "Starting tone " + info); - toneGenerator = mToneGeneratorFactory.newInCallToneGenerator(info.stream, info.volume); - toneGenerator.startTone(info.tone); - /* - * During tests, this will block until the tests call mExecutor.ackMilestone. This call - * allows for synchronization to the point where the tone has started playing. - */ - mExecutor.milestone(); - if (mNumPlayingTones != null) { - mNumPlayingTones.await(info.toneLengthMillis, TimeUnit.MILLISECONDS); - // Allows for synchronization to the point where the tone has completed playing. - mExecutor.milestone(); - } - } catch (InterruptedException e) { - Log.w(this, "Interrupted while playing in-call tone."); - } finally { - if (toneGenerator != null) { - toneGenerator.release(); - } - if (mNumPlayingTones != null) { - mNumPlayingTones.countDown(); - } - // Allows for synchronization to the point where this background thread has cleaned up. - mExecutor.milestone(); - } - } - - /** - * Stops playback of the current tone. - */ - public void stop() { - if (mNumPlayingTones != null) { - mNumPlayingTones.countDown(); - } - } - - private static class ToneGeneratorInfo { - public final int tone; - public final int volume; - public final int toneLengthMillis; - public final int stream; - - public ToneGeneratorInfo(int toneGeneratorType, int volume, int toneLengthMillis, - int stream) { - this.tone = toneGeneratorType; - this.volume = volume; - this.toneLengthMillis = toneLengthMillis; - this.stream = stream; - } - - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("tone", tone) - .add("volume", volume) - .add("toneLengthMillis", toneLengthMillis).toString(); - } - } -} diff --git a/InCallUI/src/com/android/incallui/ringtone/ToneGeneratorFactory.java b/InCallUI/src/com/android/incallui/ringtone/ToneGeneratorFactory.java deleted file mode 100644 index ac47c8a7d..000000000 --- a/InCallUI/src/com/android/incallui/ringtone/ToneGeneratorFactory.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui.ringtone; - -import android.media.ToneGenerator; - -/** - * Factory used to create {@link ToneGenerator}s. - */ -public class ToneGeneratorFactory { - - /** - * Creates a new {@link ToneGenerator} to use while in a call. - * - * @param stream the stream through which to play tones. - * @param volume the volume at which to play tones. - * @return a new ToneGenerator. - */ - public ToneGenerator newInCallToneGenerator(int stream, int volume) { - return new ToneGenerator(stream, volume); - } -} diff --git a/InCallUI/src/com/android/incallui/service/PhoneNumberService.java b/InCallUI/src/com/android/incallui/service/PhoneNumberService.java deleted file mode 100644 index 70da4ef3a..000000000 --- a/InCallUI/src/com/android/incallui/service/PhoneNumberService.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui.service; - -import android.graphics.Bitmap; - -/** - * Provides phone number lookup services. - */ -public interface PhoneNumberService { - - /** - * Get a phone number number asynchronously. - * - * @param phoneNumber The phone number to lookup. - * @param listener The listener to notify when the phone number lookup is complete. - * @param imageListener The listener to notify when the image lookup is complete. - */ - public void getPhoneNumberInfo(String phoneNumber, NumberLookupListener listener, - ImageLookupListener imageListener, boolean isIncoming); - - public interface NumberLookupListener { - - /** - * Callback when a phone number has been looked up. - * - * @param info The looked up information. Or (@literal null} if there are no results. - */ - public void onPhoneNumberInfoComplete(PhoneNumberInfo info); - } - - public interface ImageLookupListener { - - /** - * Callback when a image has been fetched. - * - * @param bitmap The fetched image. - */ - public void onImageFetchComplete(Bitmap bitmap); - } - - public interface PhoneNumberInfo { - public String getDisplayName(); - public String getNumber(); - public int getPhoneType(); - public String getPhoneLabel(); - public String getNormalizedNumber(); - public String getImageUrl(); - public String getLookupKey(); - public boolean isBusiness(); - public int getLookupSource(); - } -} diff --git a/InCallUI/src/com/android/incallui/spam/SpamCallListListener.java b/InCallUI/src/com/android/incallui/spam/SpamCallListListener.java deleted file mode 100644 index b97f4d099..000000000 --- a/InCallUI/src/com/android/incallui/spam/SpamCallListListener.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui.spam; - -import com.google.common.annotations.VisibleForTesting; - -import android.content.Context; -import android.telecom.DisconnectCause; -import android.text.TextUtils; - -import com.android.dialer.calllog.CallLogAsyncTaskUtil; -import com.android.incallui.Call; -import com.android.incallui.CallList; -import com.android.incallui.Log; - -public class SpamCallListListener implements CallList.Listener { - private static final String TAG = "SpamCallListListener"; - - private final Context mContext; - - public SpamCallListListener(Context context) { - mContext = context; - } - - @Override - public void onIncomingCall(final Call call) { - String number = call.getNumber(); - if (TextUtils.isEmpty(number)) { - return; - } - CallLogAsyncTaskUtil.getNumberInCallHistory(mContext, number, - new CallLogAsyncTaskUtil.OnGetNumberInCallHistoryListener() { - @Override - public void onComplete(boolean inCallHistory) { - call.setCallHistoryStatus(inCallHistory ? - Call.CALL_HISTORY_STATUS_PRESENT - : Call.CALL_HISTORY_STATUS_NOT_PRESENT); - } - }); - } - - @Override - public void onUpgradeToVideo(Call call) {} - - @Override - public void onCallListChange(CallList callList) {} - - @Override - public void onDisconnect(Call call) { - if (shouldShowAfterCallNotification(call)) { - showNotification(call.getNumber()); - } - } - - /** - * Posts the intent for displaying the after call spam notification to the user. - */ - @VisibleForTesting - /* package */ void showNotification(String number) { - //TODO(mhashmi): build and show notifications here - } - - /** - * Determines if the after call notification should be shown for the specified call. - */ - private boolean shouldShowAfterCallNotification(Call call) { - String number = call.getNumber(); - if (TextUtils.isEmpty(number)) { - return false; - } - - Call.LogState logState = call.getLogState(); - if (!logState.isIncoming) { - return false; - } - - if (logState.duration <= 0) { - return false; - } - - if (logState.contactLookupResult != Call.LogState.LOOKUP_NOT_FOUND - && logState.contactLookupResult != Call.LogState.LOOKUP_UNKNOWN) { - return false; - } - - int callHistoryStatus = call.getCallHistoryStatus(); - if (callHistoryStatus == Call.CALL_HISTORY_STATUS_PRESENT) { - return false; - } else if (callHistoryStatus == Call.CALL_HISTORY_STATUS_UNKNOWN) { - Log.i(TAG, "Call history status is unknown, returning false"); - return false; - } - - // Check if call disconnected because of either user hanging up - int disconnectCause = call.getDisconnectCause().getCode(); - if (disconnectCause != DisconnectCause.LOCAL && disconnectCause != DisconnectCause.REMOTE) { - return false; - } - - Log.i(TAG, "shouldShowAfterCallNotification, returning true"); - return true; - } -} \ No newline at end of file diff --git a/InCallUI/src/com/android/incallui/util/AccessibilityUtil.java b/InCallUI/src/com/android/incallui/util/AccessibilityUtil.java deleted file mode 100644 index 1fdd2bac6..000000000 --- a/InCallUI/src/com/android/incallui/util/AccessibilityUtil.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui.util; - -import android.content.Context; -import android.view.accessibility.AccessibilityManager; - -public class AccessibilityUtil { - public static boolean isTalkBackEnabled(Context context) { - AccessibilityManager accessibilityManager = (AccessibilityManager) context - .getSystemService(Context.ACCESSIBILITY_SERVICE); - return accessibilityManager != null - && accessibilityManager.isEnabled() - && accessibilityManager.isTouchExplorationEnabled(); - } -} diff --git a/InCallUI/src/com/android/incallui/util/TelecomCallUtil.java b/InCallUI/src/com/android/incallui/util/TelecomCallUtil.java deleted file mode 100644 index 53ecc29e9..000000000 --- a/InCallUI/src/com/android/incallui/util/TelecomCallUtil.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui.util; - -import android.net.Uri; -import android.telecom.Call; -import android.telephony.PhoneNumberUtils; - -/** - * Class to provide a standard interface for obtaining information from the underlying - * android.telecom.Call. Much of this should be obtained through the incall.Call, but - * on occasion we need to interact with the telecom.Call directly (eg. call blocking, - * before the incall.Call has been created). - */ -public class TelecomCallUtil { - - // Whether the call handle is an emergency number. - public static boolean isEmergencyCall(Call call) { - Uri handle = call.getDetails().getHandle(); - return PhoneNumberUtils.isEmergencyNumber( - handle == null ? "" : handle.getSchemeSpecificPart()); - } - - public static String getNumber(Call call) { - if (call == null) { - return null; - } - if (call.getDetails().getGatewayInfo() != null) { - return call.getDetails().getGatewayInfo() - .getOriginalAddress().getSchemeSpecificPart(); - } - Uri handle = getHandle(call); - return handle == null ? null : handle.getSchemeSpecificPart(); - } - - public static Uri getHandle(Call call) { - return call == null ? null : call.getDetails().getHandle(); - } -} diff --git a/InCallUI/src/com/android/incallui/widget/multiwaveview/Ease.java b/InCallUI/src/com/android/incallui/widget/multiwaveview/Ease.java deleted file mode 100644 index 5ef689771..000000000 --- a/InCallUI/src/com/android/incallui/widget/multiwaveview/Ease.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui.widget.multiwaveview; - -import android.animation.TimeInterpolator; - -class Ease { - private static final float DOMAIN = 1.0f; - private static final float DURATION = 1.0f; - private static final float START = 0.0f; - - static class Linear { - public static final TimeInterpolator easeNone = new TimeInterpolator() { - public float getInterpolation(float input) { - return input; - } - }; - } - - static class Cubic { - public static final TimeInterpolator easeIn = new TimeInterpolator() { - public float getInterpolation(float input) { - return DOMAIN*(input/=DURATION)*input*input + START; - } - }; - public static final TimeInterpolator easeOut = new TimeInterpolator() { - public float getInterpolation(float input) { - return DOMAIN*((input=input/DURATION-1)*input*input + 1) + START; - } - }; - public static final TimeInterpolator easeInOut = new TimeInterpolator() { - public float getInterpolation(float input) { - return ((input/=DURATION/2) < 1.0f) ? - (DOMAIN/2*input*input*input + START) - : (DOMAIN/2*((input-=2)*input*input + 2) + START); - } - }; - } - - static class Quad { - public static final TimeInterpolator easeIn = new TimeInterpolator() { - public float getInterpolation (float input) { - return DOMAIN*(input/=DURATION)*input + START; - } - }; - public static final TimeInterpolator easeOut = new TimeInterpolator() { - public float getInterpolation(float input) { - return -DOMAIN *(input/=DURATION)*(input-2) + START; - } - }; - public static final TimeInterpolator easeInOut = new TimeInterpolator() { - public float getInterpolation(float input) { - return ((input/=DURATION/2) < 1) ? - (DOMAIN/2*input*input + START) - : (-DOMAIN/2 * ((--input)*(input-2) - 1) + START); - } - }; - } - - static class Quart { - public static final TimeInterpolator easeIn = new TimeInterpolator() { - public float getInterpolation(float input) { - return DOMAIN*(input/=DURATION)*input*input*input + START; - } - }; - public static final TimeInterpolator easeOut = new TimeInterpolator() { - public float getInterpolation(float input) { - return -DOMAIN * ((input=input/DURATION-1)*input*input*input - 1) + START; - } - }; - public static final TimeInterpolator easeInOut = new TimeInterpolator() { - public float getInterpolation(float input) { - return ((input/=DURATION/2) < 1) ? - (DOMAIN/2*input*input*input*input + START) - : (-DOMAIN/2 * ((input-=2)*input*input*input - 2) + START); - } - }; - } - - static class Quint { - public static final TimeInterpolator easeIn = new TimeInterpolator() { - public float getInterpolation(float input) { - return DOMAIN*(input/=DURATION)*input*input*input*input + START; - } - }; - public static final TimeInterpolator easeOut = new TimeInterpolator() { - public float getInterpolation(float input) { - return DOMAIN*((input=input/DURATION-1)*input*input*input*input + 1) + START; - } - }; - public static final TimeInterpolator easeInOut = new TimeInterpolator() { - public float getInterpolation(float input) { - return ((input/=DURATION/2) < 1) ? - (DOMAIN/2*input*input*input*input*input + START) - : (DOMAIN/2*((input-=2)*input*input*input*input + 2) + START); - } - }; - } - - static class Sine { - public static final TimeInterpolator easeIn = new TimeInterpolator() { - public float getInterpolation(float input) { - return -DOMAIN * (float) Math.cos(input/DURATION * (Math.PI/2)) + DOMAIN + START; - } - }; - public static final TimeInterpolator easeOut = new TimeInterpolator() { - public float getInterpolation(float input) { - return DOMAIN * (float) Math.sin(input/DURATION * (Math.PI/2)) + START; - } - }; - public static final TimeInterpolator easeInOut = new TimeInterpolator() { - public float getInterpolation(float input) { - return -DOMAIN/2 * ((float)Math.cos(Math.PI*input/DURATION) - 1.0f) + START; - } - }; - } - -} diff --git a/InCallUI/src/com/android/incallui/widget/multiwaveview/GlowPadView.java b/InCallUI/src/com/android/incallui/widget/multiwaveview/GlowPadView.java deleted file mode 100644 index efeb4b7e3..000000000 --- a/InCallUI/src/com/android/incallui/widget/multiwaveview/GlowPadView.java +++ /dev/null @@ -1,1473 +0,0 @@ -/* - * 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.incallui.widget.multiwaveview; - -import android.animation.Animator; -import android.animation.Animator.AnimatorListener; -import android.animation.AnimatorListenerAdapter; -import android.animation.TimeInterpolator; -import android.animation.ValueAnimator; -import android.animation.ValueAnimator.AnimatorUpdateListener; -import android.content.ComponentName; -import android.content.Context; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.os.Bundle; -import android.os.Vibrator; -import android.support.v4.view.ViewCompat; -import android.support.v4.view.accessibility.AccessibilityEventCompat; -import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; -import android.support.v4.widget.ExploreByTouchHelper; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.util.Log; -import android.util.TypedValue; -import android.view.Gravity; -import android.view.MotionEvent; -import android.view.View; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityManager; -import android.view.accessibility.AccessibilityNodeInfo; -import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; -import android.view.accessibility.AccessibilityNodeProvider; - -import com.android.dialer.R; - -import java.util.ArrayList; -import java.util.List; - -/** - * This is a copy of com.android.internal.widget.multiwaveview.GlowPadView with minor changes - * to remove dependencies on private api's. - * - * Incoporated the scaling functionality. - * - * A re-usable widget containing a center, outer ring and wave animation. - */ -public class GlowPadView extends View { - private static final String TAG = "GlowPadView"; - private static final boolean DEBUG = false; - - // Wave state machine - private static final int STATE_IDLE = 0; - private static final int STATE_START = 1; - private static final int STATE_FIRST_TOUCH = 2; - private static final int STATE_TRACKING = 3; - private static final int STATE_SNAP = 4; - private static final int STATE_FINISH = 5; - - // Animation properties. - private static final float SNAP_MARGIN_DEFAULT = 20.0f; // distance to ring before we snap to it - - public interface OnTriggerListener { - int NO_HANDLE = 0; - int CENTER_HANDLE = 1; - public void onGrabbed(View v, int handle); - public void onReleased(View v, int handle); - public void onTrigger(View v, int target); - public void onGrabbedStateChange(View v, int handle); - public void onFinishFinalAnimation(); - } - - // Tuneable parameters for animation - private static final int WAVE_ANIMATION_DURATION = 1350; - private static final int RETURN_TO_HOME_DELAY = 1200; - private static final int RETURN_TO_HOME_DURATION = 200; - private static final int HIDE_ANIMATION_DELAY = 200; - private static final int HIDE_ANIMATION_DURATION = 200; - private static final int SHOW_ANIMATION_DURATION = 200; - private static final int SHOW_ANIMATION_DELAY = 50; - private static final int INITIAL_SHOW_HANDLE_DURATION = 200; - private static final int REVEAL_GLOW_DELAY = 0; - private static final int REVEAL_GLOW_DURATION = 0; - - private static final float TAP_RADIUS_SCALE_ACCESSIBILITY_ENABLED = 1.3f; - private static final float TARGET_SCALE_EXPANDED = 1.0f; - private static final float TARGET_SCALE_COLLAPSED = 0.8f; - private static final float RING_SCALE_EXPANDED = 1.0f; - private static final float RING_SCALE_COLLAPSED = 0.5f; - - private ArrayList mTargetDrawables = new ArrayList(); - private AnimationBundle mWaveAnimations = new AnimationBundle(); - private AnimationBundle mTargetAnimations = new AnimationBundle(); - private AnimationBundle mGlowAnimations = new AnimationBundle(); - private ArrayList mTargetDescriptions; - private ArrayList mDirectionDescriptions; - private OnTriggerListener mOnTriggerListener; - private TargetDrawable mHandleDrawable; - private TargetDrawable mOuterRing; - private Vibrator mVibrator; - - private int mFeedbackCount = 3; - private int mVibrationDuration = 0; - private int mGrabbedState; - private int mActiveTarget = -1; - private float mGlowRadius; - private float mWaveCenterX; - private float mWaveCenterY; - private int mMaxTargetHeight; - private int mMaxTargetWidth; - private float mRingScaleFactor = 1f; - private boolean mAllowScaling; - - private float mOuterRadius = 0.0f; - private float mSnapMargin = 0.0f; - private boolean mDragging; - private int mNewTargetResources; - - private AccessibilityNodeProvider mAccessibilityNodeProvider; - private GlowpadExploreByTouchHelper mExploreByTouchHelper; - - private class AnimationBundle extends ArrayList { - private static final long serialVersionUID = 0xA84D78726F127468L; - private boolean mSuspended; - - public void start() { - if (mSuspended) return; // ignore attempts to start animations - final int count = size(); - for (int i = 0; i < count; i++) { - Tweener anim = get(i); - anim.animator.start(); - } - } - - public void cancel() { - final int count = size(); - for (int i = 0; i < count; i++) { - Tweener anim = get(i); - anim.animator.cancel(); - } - clear(); - } - - public void stop() { - final int count = size(); - for (int i = 0; i < count; i++) { - Tweener anim = get(i); - anim.animator.end(); - } - clear(); - } - - public void setSuspended(boolean suspend) { - mSuspended = suspend; - } - }; - - private AnimatorListener mResetListener = new AnimatorListenerAdapter() { - public void onAnimationEnd(Animator animator) { - switchToState(STATE_IDLE, mWaveCenterX, mWaveCenterY); - dispatchOnFinishFinalAnimation(); - } - }; - - private AnimatorListener mResetListenerWithPing = new AnimatorListenerAdapter() { - public void onAnimationEnd(Animator animator) { - ping(); - switchToState(STATE_IDLE, mWaveCenterX, mWaveCenterY); - dispatchOnFinishFinalAnimation(); - } - }; - - private AnimatorUpdateListener mUpdateListener = new AnimatorUpdateListener() { - public void onAnimationUpdate(ValueAnimator animation) { - invalidate(); - } - }; - - private boolean mAnimatingTargets; - private AnimatorListener mTargetUpdateListener = new AnimatorListenerAdapter() { - public void onAnimationEnd(Animator animator) { - if (mNewTargetResources != 0) { - internalSetTargetResources(mNewTargetResources); - mNewTargetResources = 0; - hideTargets(false, false); - } - mAnimatingTargets = false; - } - }; - private int mTargetResourceId; - private int mTargetDescriptionsResourceId; - private int mDirectionDescriptionsResourceId; - private boolean mAlwaysTrackFinger; - private int mHorizontalInset; - private int mVerticalInset; - private int mGravity = Gravity.TOP; - private boolean mInitialLayout = true; - private Tweener mBackgroundAnimator; - private PointCloud mPointCloud; - private float mInnerRadius; - private int mPointerId; - - public GlowPadView(Context context) { - this(context, null); - } - - public GlowPadView(Context context, AttributeSet attrs) { - super(context, attrs); - Resources res = context.getResources(); - - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.GlowPadView); - mInnerRadius = a.getDimension(R.styleable.GlowPadView_innerRadius, mInnerRadius); - mOuterRadius = a.getDimension(R.styleable.GlowPadView_outerRadius, mOuterRadius); - mSnapMargin = a.getDimension(R.styleable.GlowPadView_snapMargin, mSnapMargin); - mVibrationDuration = a.getInt(R.styleable.GlowPadView_vibrationDuration, - mVibrationDuration); - mFeedbackCount = a.getInt(R.styleable.GlowPadView_feedbackCount, - mFeedbackCount); - mAllowScaling = a.getBoolean(R.styleable.GlowPadView_allowScaling, false); - TypedValue handle = a.peekValue(R.styleable.GlowPadView_handleDrawable); - setHandleDrawable(handle != null ? handle.resourceId : R.drawable.ic_incall_audio_handle); - mOuterRing = new TargetDrawable(res, - getResourceId(a, R.styleable.GlowPadView_outerRingDrawable), 1); - - mAlwaysTrackFinger = a.getBoolean(R.styleable.GlowPadView_alwaysTrackFinger, false); - - int pointId = getResourceId(a, R.styleable.GlowPadView_pointDrawable); - Drawable pointDrawable = pointId != 0 ? res.getDrawable(pointId) : null; - mGlowRadius = a.getDimension(R.styleable.GlowPadView_glowRadius, 0.0f); - - TypedValue outValue = new TypedValue(); - - // Read array of target drawables - if (a.getValue(R.styleable.GlowPadView_targetDrawables, outValue)) { - internalSetTargetResources(outValue.resourceId); - } - if (mTargetDrawables == null || mTargetDrawables.size() == 0) { - throw new IllegalStateException("Must specify at least one target drawable"); - } - - // Read array of target descriptions - if (a.getValue(R.styleable.GlowPadView_targetDescriptions, outValue)) { - final int resourceId = outValue.resourceId; - if (resourceId == 0) { - throw new IllegalStateException("Must specify target descriptions"); - } - setTargetDescriptionsResourceId(resourceId); - } - - // Read array of direction descriptions - if (a.getValue(R.styleable.GlowPadView_directionDescriptions, outValue)) { - final int resourceId = outValue.resourceId; - if (resourceId == 0) { - throw new IllegalStateException("Must specify direction descriptions"); - } - setDirectionDescriptionsResourceId(resourceId); - } - - // Use gravity attribute from LinearLayout - //a = context.obtainStyledAttributes(attrs, R.styleable.LinearLayout); - mGravity = a.getInt(R.styleable.GlowPadView_android_gravity, Gravity.TOP); - a.recycle(); - - - setVibrateEnabled(mVibrationDuration > 0); - - assignDefaultsIfNeeded(); - - mPointCloud = new PointCloud(pointDrawable); - mPointCloud.makePointCloud(mInnerRadius, mOuterRadius); - mPointCloud.glowManager.setRadius(mGlowRadius); - - mExploreByTouchHelper = new GlowpadExploreByTouchHelper(this); - ViewCompat.setAccessibilityDelegate(this, mExploreByTouchHelper); - } - - private int getResourceId(TypedArray a, int id) { - TypedValue tv = a.peekValue(id); - return tv == null ? 0 : tv.resourceId; - } - - private void dump() { - Log.v(TAG, "Outer Radius = " + mOuterRadius); - Log.v(TAG, "SnapMargin = " + mSnapMargin); - Log.v(TAG, "FeedbackCount = " + mFeedbackCount); - Log.v(TAG, "VibrationDuration = " + mVibrationDuration); - Log.v(TAG, "GlowRadius = " + mGlowRadius); - Log.v(TAG, "WaveCenterX = " + mWaveCenterX); - Log.v(TAG, "WaveCenterY = " + mWaveCenterY); - } - - public void suspendAnimations() { - mWaveAnimations.setSuspended(true); - mTargetAnimations.setSuspended(true); - mGlowAnimations.setSuspended(true); - } - - public void resumeAnimations() { - mWaveAnimations.setSuspended(false); - mTargetAnimations.setSuspended(false); - mGlowAnimations.setSuspended(false); - mWaveAnimations.start(); - mTargetAnimations.start(); - mGlowAnimations.start(); - } - - @Override - protected int getSuggestedMinimumWidth() { - // View should be large enough to contain the background + handle and - // target drawable on either edge. - return (int) (Math.max(mOuterRing.getWidth(), 2 * mOuterRadius) + mMaxTargetWidth); - } - - @Override - protected int getSuggestedMinimumHeight() { - // View should be large enough to contain the unlock ring + target and - // target drawable on either edge - return (int) (Math.max(mOuterRing.getHeight(), 2 * mOuterRadius) + mMaxTargetHeight); - } - - /** - * This gets the suggested width accounting for the ring's scale factor. - */ - protected int getScaledSuggestedMinimumWidth() { - return (int) (mRingScaleFactor * Math.max(mOuterRing.getWidth(), 2 * mOuterRadius) - + mMaxTargetWidth); - } - - /** - * This gets the suggested height accounting for the ring's scale factor. - */ - protected int getScaledSuggestedMinimumHeight() { - return (int) (mRingScaleFactor * Math.max(mOuterRing.getHeight(), 2 * mOuterRadius) - + mMaxTargetHeight); - } - - private int resolveMeasured(int measureSpec, int desired) - { - int result = 0; - int specSize = MeasureSpec.getSize(measureSpec); - switch (MeasureSpec.getMode(measureSpec)) { - case MeasureSpec.UNSPECIFIED: - result = desired; - break; - case MeasureSpec.AT_MOST: - result = Math.min(specSize, desired); - break; - case MeasureSpec.EXACTLY: - default: - result = specSize; - } - return result; - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - final int minimumWidth = getSuggestedMinimumWidth(); - final int minimumHeight = getSuggestedMinimumHeight(); - int computedWidth = resolveMeasured(widthMeasureSpec, minimumWidth); - int computedHeight = resolveMeasured(heightMeasureSpec, minimumHeight); - - mRingScaleFactor = computeScaleFactor(minimumWidth, minimumHeight, - computedWidth, computedHeight); - - int scaledWidth = getScaledSuggestedMinimumWidth(); - int scaledHeight = getScaledSuggestedMinimumHeight(); - - computeInsets(computedWidth - scaledWidth, computedHeight - scaledHeight); - setMeasuredDimension(computedWidth, computedHeight); - } - - private void switchToState(int state, float x, float y) { - switch (state) { - case STATE_IDLE: - deactivateTargets(); - hideGlow(0, 0, 0.0f, null); - startBackgroundAnimation(0, 0.0f); - mHandleDrawable.setState(TargetDrawable.STATE_INACTIVE); - mHandleDrawable.setAlpha(1.0f); - break; - - case STATE_START: - startBackgroundAnimation(0, 0.0f); - break; - - case STATE_FIRST_TOUCH: - mHandleDrawable.setAlpha(0.0f); - deactivateTargets(); - showTargets(true); - startBackgroundAnimation(INITIAL_SHOW_HANDLE_DURATION, 1.0f); - setGrabbedState(OnTriggerListener.CENTER_HANDLE); - - final AccessibilityManager accessibilityManager = - (AccessibilityManager) getContext().getSystemService( - Context.ACCESSIBILITY_SERVICE); - if (accessibilityManager.isEnabled()) { - announceTargets(); - } - break; - - case STATE_TRACKING: - mHandleDrawable.setAlpha(0.0f); - break; - - case STATE_SNAP: - // TODO: Add transition states (see list_selector_background_transition.xml) - mHandleDrawable.setAlpha(0.0f); - showGlow(REVEAL_GLOW_DURATION , REVEAL_GLOW_DELAY, 0.0f, null); - break; - - case STATE_FINISH: - doFinish(); - break; - } - } - - private void showGlow(int duration, int delay, float finalAlpha, - AnimatorListener finishListener) { - mGlowAnimations.cancel(); - mGlowAnimations.add(Tweener.to(mPointCloud.glowManager, duration, - "ease", Ease.Cubic.easeIn, - "delay", delay, - "alpha", finalAlpha, - "onUpdate", mUpdateListener, - "onComplete", finishListener)); - mGlowAnimations.start(); - } - - private void hideGlow(int duration, int delay, float finalAlpha, - AnimatorListener finishListener) { - mGlowAnimations.cancel(); - mGlowAnimations.add(Tweener.to(mPointCloud.glowManager, duration, - "ease", Ease.Quart.easeOut, - "delay", delay, - "alpha", finalAlpha, - "x", 0.0f, - "y", 0.0f, - "onUpdate", mUpdateListener, - "onComplete", finishListener)); - mGlowAnimations.start(); - } - - private void deactivateTargets() { - final int count = mTargetDrawables.size(); - for (int i = 0; i < count; i++) { - TargetDrawable target = mTargetDrawables.get(i); - target.setState(TargetDrawable.STATE_INACTIVE); - } - mActiveTarget = -1; - } - - /** - * Dispatches a trigger event to listener. Ignored if a listener is not set. - * @param whichTarget the target that was triggered. - */ - private void dispatchTriggerEvent(int whichTarget) { - vibrate(); - if (mOnTriggerListener != null) { - mOnTriggerListener.onTrigger(this, whichTarget); - } - } - - private void dispatchOnFinishFinalAnimation() { - if (mOnTriggerListener != null) { - mOnTriggerListener.onFinishFinalAnimation(); - } - } - - private void doFinish() { - final int activeTarget = mActiveTarget; - final boolean targetHit = activeTarget != -1; - - if (targetHit) { - if (DEBUG) Log.v(TAG, "Finish with target hit = " + targetHit); - - highlightSelected(activeTarget); - - // Inform listener of any active targets. Typically only one will be active. - hideGlow(RETURN_TO_HOME_DURATION, RETURN_TO_HOME_DELAY, 0.0f, mResetListener); - dispatchTriggerEvent(activeTarget); - if (!mAlwaysTrackFinger) { - // Force ring and targets to finish animation to final expanded state - mTargetAnimations.stop(); - } - } else { - // Animate handle back to the center based on current state. - hideGlow(HIDE_ANIMATION_DURATION, 0, 0.0f, mResetListenerWithPing); - hideTargets(true, false); - } - - setGrabbedState(OnTriggerListener.NO_HANDLE); - } - - private void highlightSelected(int activeTarget) { - // Highlight the given target and fade others - mTargetDrawables.get(activeTarget).setState(TargetDrawable.STATE_ACTIVE); - hideUnselected(activeTarget); - } - - private void hideUnselected(int active) { - for (int i = 0; i < mTargetDrawables.size(); i++) { - if (i != active) { - mTargetDrawables.get(i).setAlpha(0.0f); - } - } - } - - private void hideTargets(boolean animate, boolean expanded) { - mTargetAnimations.cancel(); - // Note: these animations should complete at the same time so that we can swap out - // the target assets asynchronously from the setTargetResources() call. - mAnimatingTargets = animate; - final int duration = animate ? HIDE_ANIMATION_DURATION : 0; - final int delay = animate ? HIDE_ANIMATION_DELAY : 0; - - final float targetScale = expanded ? - TARGET_SCALE_EXPANDED : TARGET_SCALE_COLLAPSED; - final int length = mTargetDrawables.size(); - final TimeInterpolator interpolator = Ease.Cubic.easeOut; - for (int i = 0; i < length; i++) { - TargetDrawable target = mTargetDrawables.get(i); - target.setState(TargetDrawable.STATE_INACTIVE); - mTargetAnimations.add(Tweener.to(target, duration, - "ease", interpolator, - "alpha", 0.0f, - "scaleX", targetScale, - "scaleY", targetScale, - "delay", delay, - "onUpdate", mUpdateListener)); - } - - float ringScaleTarget = expanded ? - RING_SCALE_EXPANDED : RING_SCALE_COLLAPSED; - ringScaleTarget *= mRingScaleFactor; - mTargetAnimations.add(Tweener.to(mOuterRing, duration, - "ease", interpolator, - "alpha", 0.0f, - "scaleX", ringScaleTarget, - "scaleY", ringScaleTarget, - "delay", delay, - "onUpdate", mUpdateListener, - "onComplete", mTargetUpdateListener)); - - mTargetAnimations.start(); - } - - private void showTargets(boolean animate) { - mTargetAnimations.stop(); - mAnimatingTargets = animate; - final int delay = animate ? SHOW_ANIMATION_DELAY : 0; - final int duration = animate ? SHOW_ANIMATION_DURATION : 0; - final int length = mTargetDrawables.size(); - for (int i = 0; i < length; i++) { - TargetDrawable target = mTargetDrawables.get(i); - target.setState(TargetDrawable.STATE_INACTIVE); - mTargetAnimations.add(Tweener.to(target, duration, - "ease", Ease.Cubic.easeOut, - "alpha", 1.0f, - "scaleX", 1.0f, - "scaleY", 1.0f, - "delay", delay, - "onUpdate", mUpdateListener)); - } - float ringScale = mRingScaleFactor * RING_SCALE_EXPANDED; - mTargetAnimations.add(Tweener.to(mOuterRing, duration, - "ease", Ease.Cubic.easeOut, - "alpha", 1.0f, - "scaleX", ringScale, - "scaleY", ringScale, - "delay", delay, - "onUpdate", mUpdateListener, - "onComplete", mTargetUpdateListener)); - - mTargetAnimations.start(); - } - - private void vibrate() { - if (mVibrator != null) { - mVibrator.vibrate(mVibrationDuration); - } - } - - private ArrayList loadDrawableArray(int resourceId) { - Resources res = getContext().getResources(); - TypedArray array = res.obtainTypedArray(resourceId); - final int count = array.length(); - ArrayList drawables = new ArrayList(count); - for (int i = 0; i < count; i++) { - TypedValue value = array.peekValue(i); - TargetDrawable target = new TargetDrawable(res, value != null ? value.resourceId : 0, 3); - drawables.add(target); - } - array.recycle(); - return drawables; - } - - private void internalSetTargetResources(int resourceId) { - final ArrayList targets = loadDrawableArray(resourceId); - mTargetDrawables = targets; - mTargetResourceId = resourceId; - - int maxWidth = mHandleDrawable.getWidth(); - int maxHeight = mHandleDrawable.getHeight(); - final int count = targets.size(); - for (int i = 0; i < count; i++) { - TargetDrawable target = targets.get(i); - maxWidth = Math.max(maxWidth, target.getWidth()); - maxHeight = Math.max(maxHeight, target.getHeight()); - } - if (mMaxTargetWidth != maxWidth || mMaxTargetHeight != maxHeight) { - mMaxTargetWidth = maxWidth; - mMaxTargetHeight = maxHeight; - requestLayout(); // required to resize layout and call updateTargetPositions() - } else { - updateTargetPositions(mWaveCenterX, mWaveCenterY); - updatePointCloudPosition(mWaveCenterX, mWaveCenterY); - } - } - /** - * Loads an array of drawables from the given resourceId. - * - * @param resourceId - */ - public void setTargetResources(int resourceId) { - if (mAnimatingTargets) { - // postpone this change until we return to the initial state - mNewTargetResources = resourceId; - } else { - internalSetTargetResources(resourceId); - } - } - - public int getTargetResourceId() { - return mTargetResourceId; - } - - /** - * Sets the handle drawable to the drawable specified by the resource ID. - * @param resourceId - */ - public void setHandleDrawable(int resourceId) { - if (mHandleDrawable != null) { - mHandleDrawable.setDrawable(getResources(), resourceId); - } else { - mHandleDrawable = new TargetDrawable(getResources(), resourceId, 1); - } - mHandleDrawable.setState(TargetDrawable.STATE_INACTIVE); - } - - /** - * Sets the resource id specifying the target descriptions for accessibility. - * - * @param resourceId The resource id. - */ - public void setTargetDescriptionsResourceId(int resourceId) { - mTargetDescriptionsResourceId = resourceId; - if (mTargetDescriptions != null) { - mTargetDescriptions.clear(); - } - } - - /** - * Gets the resource id specifying the target descriptions for accessibility. - * - * @return The resource id. - */ - public int getTargetDescriptionsResourceId() { - return mTargetDescriptionsResourceId; - } - - /** - * Sets the resource id specifying the target direction descriptions for accessibility. - * - * @param resourceId The resource id. - */ - public void setDirectionDescriptionsResourceId(int resourceId) { - mDirectionDescriptionsResourceId = resourceId; - if (mDirectionDescriptions != null) { - mDirectionDescriptions.clear(); - } - } - - /** - * Gets the resource id specifying the target direction descriptions. - * - * @return The resource id. - */ - public int getDirectionDescriptionsResourceId() { - return mDirectionDescriptionsResourceId; - } - - /** - * Enable or disable vibrate on touch. - * - * @param enabled - */ - public void setVibrateEnabled(boolean enabled) { - if (enabled && mVibrator == null) { - mVibrator = (Vibrator) getContext().getSystemService(Context.VIBRATOR_SERVICE); - } else { - mVibrator = null; - } - } - - /** - * Starts wave animation. - * - */ - public void ping() { - if (mFeedbackCount > 0) { - boolean doWaveAnimation = true; - final AnimationBundle waveAnimations = mWaveAnimations; - - // Don't do a wave if there's already one in progress - if (waveAnimations.size() > 0 && waveAnimations.get(0).animator.isRunning()) { - long t = waveAnimations.get(0).animator.getCurrentPlayTime(); - if (t < WAVE_ANIMATION_DURATION/2) { - doWaveAnimation = false; - } - } - - if (doWaveAnimation) { - startWaveAnimation(); - } - } - } - - private void stopAndHideWaveAnimation() { - mWaveAnimations.cancel(); - mPointCloud.waveManager.setAlpha(0.0f); - } - - private void startWaveAnimation() { - mWaveAnimations.cancel(); - mPointCloud.waveManager.setAlpha(1.0f); - mPointCloud.waveManager.setRadius(mHandleDrawable.getWidth()/2.0f); - mWaveAnimations.add(Tweener.to(mPointCloud.waveManager, WAVE_ANIMATION_DURATION, - "ease", Ease.Quad.easeOut, - "delay", 0, - "radius", 2.0f * mOuterRadius, - "onUpdate", mUpdateListener, - "onComplete", - new AnimatorListenerAdapter() { - public void onAnimationEnd(Animator animator) { - mPointCloud.waveManager.setRadius(0.0f); - mPointCloud.waveManager.setAlpha(0.0f); - } - })); - mWaveAnimations.start(); - } - - /** - * Resets the widget to default state and cancels all animation. If animate is 'true', will - * animate objects into place. Otherwise, objects will snap back to place. - * - * @param animate - */ - public void reset(boolean animate) { - mGlowAnimations.stop(); - mTargetAnimations.stop(); - startBackgroundAnimation(0, 0.0f); - stopAndHideWaveAnimation(); - hideTargets(animate, false); - hideGlow(0, 0, 0.0f, null); - Tweener.reset(); - } - - private void startBackgroundAnimation(int duration, float alpha) { - final Drawable background = getBackground(); - if (mAlwaysTrackFinger && background != null) { - if (mBackgroundAnimator != null) { - mBackgroundAnimator.animator.cancel(); - } - mBackgroundAnimator = Tweener.to(background, duration, - "ease", Ease.Cubic.easeIn, - "alpha", (int)(255.0f * alpha), - "delay", SHOW_ANIMATION_DELAY); - mBackgroundAnimator.animator.start(); - } - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - final int action = event.getActionMasked(); - boolean handled = false; - switch (action) { - case MotionEvent.ACTION_POINTER_DOWN: - case MotionEvent.ACTION_DOWN: - if (DEBUG) Log.v(TAG, "*** DOWN ***"); - handleDown(event); - handleMove(event); - handled = true; - break; - - case MotionEvent.ACTION_MOVE: - if (DEBUG) Log.v(TAG, "*** MOVE ***"); - handleMove(event); - handled = true; - break; - - case MotionEvent.ACTION_POINTER_UP: - case MotionEvent.ACTION_UP: - if (DEBUG) Log.v(TAG, "*** UP ***"); - handleMove(event); - handleUp(event); - handled = true; - break; - - case MotionEvent.ACTION_CANCEL: - if (DEBUG) Log.v(TAG, "*** CANCEL ***"); - handleMove(event); - handleCancel(event); - handled = true; - break; - } - invalidate(); - return handled ? true : super.onTouchEvent(event); - } - - private void updateGlowPosition(float x, float y) { - float dx = x - mOuterRing.getX(); - float dy = y - mOuterRing.getY(); - dx *= 1f / mRingScaleFactor; - dy *= 1f / mRingScaleFactor; - mPointCloud.glowManager.setX(mOuterRing.getX() + dx); - mPointCloud.glowManager.setY(mOuterRing.getY() + dy); - } - - private void handleDown(MotionEvent event) { - int actionIndex = event.getActionIndex(); - float eventX = event.getX(actionIndex); - float eventY = event.getY(actionIndex); - switchToState(STATE_START, eventX, eventY); - if (!trySwitchToFirstTouchState(eventX, eventY)) { - mDragging = false; - } else { - mPointerId = event.getPointerId(actionIndex); - updateGlowPosition(eventX, eventY); - } - } - - private void handleUp(MotionEvent event) { - if (DEBUG && mDragging) Log.v(TAG, "** Handle RELEASE"); - int actionIndex = event.getActionIndex(); - if (event.getPointerId(actionIndex) == mPointerId) { - switchToState(STATE_FINISH, event.getX(actionIndex), event.getY(actionIndex)); - } - } - - private void handleCancel(MotionEvent event) { - if (DEBUG && mDragging) Log.v(TAG, "** Handle CANCEL"); - - // We should drop the active target here but it interferes with - // moving off the screen in the direction of the navigation bar. At some point we may - // want to revisit how we handle this. For now we'll allow a canceled event to - // activate the current target. - - // mActiveTarget = -1; // Drop the active target if canceled. - - int actionIndex = event.findPointerIndex(mPointerId); - actionIndex = actionIndex == -1 ? 0 : actionIndex; - switchToState(STATE_FINISH, event.getX(actionIndex), event.getY(actionIndex)); - } - - private void handleMove(MotionEvent event) { - int activeTarget = -1; - final int historySize = event.getHistorySize(); - ArrayList targets = mTargetDrawables; - int ntargets = targets.size(); - float x = 0.0f; - float y = 0.0f; - int actionIndex = event.findPointerIndex(mPointerId); - - if (actionIndex == -1) { - return; // no data for this pointer - } - - for (int k = 0; k < historySize + 1; k++) { - float eventX = k < historySize ? event.getHistoricalX(actionIndex, k) - : event.getX(actionIndex); - float eventY = k < historySize ? event.getHistoricalY(actionIndex, k) - :event.getY(actionIndex); - // tx and ty are relative to wave center - float tx = eventX - mWaveCenterX; - float ty = eventY - mWaveCenterY; - float touchRadius = (float) Math.hypot(tx, ty); - final float scale = touchRadius > mOuterRadius ? mOuterRadius / touchRadius : 1.0f; - float limitX = tx * scale; - float limitY = ty * scale; - double angleRad = Math.atan2(-ty, tx); - - if (!mDragging) { - trySwitchToFirstTouchState(eventX, eventY); - } - - if (mDragging) { - // For multiple targets, snap to the one that matches - final float snapRadius = mRingScaleFactor * mOuterRadius - mSnapMargin; - final float snapDistance2 = snapRadius * snapRadius; - // Find first target in range - for (int i = 0; i < ntargets; i++) { - TargetDrawable target = targets.get(i); - - double targetMinRad = (i - 0.5) * 2 * Math.PI / ntargets; - double targetMaxRad = (i + 0.5) * 2 * Math.PI / ntargets; - if (target.isEnabled()) { - boolean angleMatches = - (angleRad > targetMinRad && angleRad <= targetMaxRad) || - (angleRad + 2 * Math.PI > targetMinRad && - angleRad + 2 * Math.PI <= targetMaxRad); - if (angleMatches && (dist2(tx, ty) > snapDistance2)) { - activeTarget = i; - } - } - } - } - x = limitX; - y = limitY; - } - - if (!mDragging) { - return; - } - - if (activeTarget != -1) { - switchToState(STATE_SNAP, x,y); - updateGlowPosition(x, y); - } else { - switchToState(STATE_TRACKING, x, y); - updateGlowPosition(x, y); - } - - if (mActiveTarget != activeTarget) { - // Defocus the old target - if (mActiveTarget != -1) { - TargetDrawable target = targets.get(mActiveTarget); - target.setState(TargetDrawable.STATE_INACTIVE); - } - // Focus the new target - if (activeTarget != -1) { - TargetDrawable target = targets.get(activeTarget); - target.setState(TargetDrawable.STATE_FOCUSED); - final AccessibilityManager accessibilityManager = - (AccessibilityManager) getContext().getSystemService( - Context.ACCESSIBILITY_SERVICE); - if (accessibilityManager.isEnabled()) { - String targetContentDescription = getTargetDescription(activeTarget); - announceForAccessibility(targetContentDescription); - } - } - } - mActiveTarget = activeTarget; - } - - @Override - public boolean onHoverEvent(MotionEvent event) { - final AccessibilityManager accessibilityManager = - (AccessibilityManager) getContext().getSystemService( - Context.ACCESSIBILITY_SERVICE); - if (accessibilityManager.isTouchExplorationEnabled()) { - final int action = event.getAction(); - switch (action) { - case MotionEvent.ACTION_HOVER_ENTER: - event.setAction(MotionEvent.ACTION_DOWN); - break; - case MotionEvent.ACTION_HOVER_MOVE: - event.setAction(MotionEvent.ACTION_MOVE); - break; - case MotionEvent.ACTION_HOVER_EXIT: - event.setAction(MotionEvent.ACTION_UP); - break; - } - onTouchEvent(event); - event.setAction(action); - } - super.onHoverEvent(event); - return true; - } - - /** - * Sets the current grabbed state, and dispatches a grabbed state change - * event to our listener. - */ - private void setGrabbedState(int newState) { - if (newState != mGrabbedState) { - if (newState != OnTriggerListener.NO_HANDLE) { - vibrate(); - } - mGrabbedState = newState; - if (mOnTriggerListener != null) { - if (newState == OnTriggerListener.NO_HANDLE) { - mOnTriggerListener.onReleased(this, OnTriggerListener.CENTER_HANDLE); - } else { - mOnTriggerListener.onGrabbed(this, OnTriggerListener.CENTER_HANDLE); - } - mOnTriggerListener.onGrabbedStateChange(this, newState); - } - } - } - - private boolean trySwitchToFirstTouchState(float x, float y) { - final float tx = x - mWaveCenterX; - final float ty = y - mWaveCenterY; - if (mAlwaysTrackFinger || dist2(tx,ty) <= getScaledGlowRadiusSquared()) { - if (DEBUG) Log.v(TAG, "** Handle HIT"); - switchToState(STATE_FIRST_TOUCH, x, y); - updateGlowPosition(tx, ty); - mDragging = true; - return true; - } - return false; - } - - private void assignDefaultsIfNeeded() { - if (mOuterRadius == 0.0f) { - mOuterRadius = Math.max(mOuterRing.getWidth(), mOuterRing.getHeight())/2.0f; - } - if (mSnapMargin == 0.0f) { - mSnapMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, - SNAP_MARGIN_DEFAULT, getContext().getResources().getDisplayMetrics()); - } - if (mInnerRadius == 0.0f) { - mInnerRadius = mHandleDrawable.getWidth() / 10.0f; - } - } - - private void computeInsets(int dx, int dy) { - final int layoutDirection = getLayoutDirection(); - final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection); - - switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { - case Gravity.LEFT: - mHorizontalInset = 0; - break; - case Gravity.RIGHT: - mHorizontalInset = dx; - break; - case Gravity.CENTER_HORIZONTAL: - default: - mHorizontalInset = dx / 2; - break; - } - switch (absoluteGravity & Gravity.VERTICAL_GRAVITY_MASK) { - case Gravity.TOP: - mVerticalInset = 0; - break; - case Gravity.BOTTOM: - mVerticalInset = dy; - break; - case Gravity.CENTER_VERTICAL: - default: - mVerticalInset = dy / 2; - break; - } - } - - /** - * Given the desired width and height of the ring and the allocated width and height, compute - * how much we need to scale the ring. - */ - private float computeScaleFactor(int desiredWidth, int desiredHeight, - int actualWidth, int actualHeight) { - - // Return unity if scaling is not allowed. - if (!mAllowScaling) return 1f; - - final int layoutDirection = getLayoutDirection(); - final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection); - - float scaleX = 1f; - float scaleY = 1f; - - // We use the gravity as a cue for whether we want to scale on a particular axis. - // We only scale to fit horizontally if we're not pinned to the left or right. Likewise, - // we only scale to fit vertically if we're not pinned to the top or bottom. In these - // cases, we want the ring to hang off the side or top/bottom, respectively. - switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { - case Gravity.LEFT: - case Gravity.RIGHT: - break; - case Gravity.CENTER_HORIZONTAL: - default: - if (desiredWidth > actualWidth) { - scaleX = (1f * actualWidth - mMaxTargetWidth) / - (desiredWidth - mMaxTargetWidth); - } - break; - } - switch (absoluteGravity & Gravity.VERTICAL_GRAVITY_MASK) { - case Gravity.TOP: - case Gravity.BOTTOM: - break; - case Gravity.CENTER_VERTICAL: - default: - if (desiredHeight > actualHeight) { - scaleY = (1f * actualHeight - mMaxTargetHeight) / - (desiredHeight - mMaxTargetHeight); - } - break; - } - return Math.min(scaleX, scaleY); - } - - private float getRingWidth() { - return mRingScaleFactor * Math.max(mOuterRing.getWidth(), 2 * mOuterRadius); - } - - private float getRingHeight() { - return mRingScaleFactor * Math.max(mOuterRing.getHeight(), 2 * mOuterRadius); - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - final int width = right - left; - final int height = bottom - top; - - // Target placement width/height. This puts the targets on the greater of the ring - // width or the specified outer radius. - final float placementWidth = getRingWidth(); - final float placementHeight = getRingHeight(); - float newWaveCenterX = mHorizontalInset - + (mMaxTargetWidth + placementWidth) / 2; - float newWaveCenterY = mVerticalInset - + (mMaxTargetHeight + placementHeight) / 2; - - if (mInitialLayout) { - stopAndHideWaveAnimation(); - hideTargets(false, false); - mInitialLayout = false; - } - - mOuterRing.setPositionX(newWaveCenterX); - mOuterRing.setPositionY(newWaveCenterY); - - mPointCloud.setScale(mRingScaleFactor); - - mHandleDrawable.setPositionX(newWaveCenterX); - mHandleDrawable.setPositionY(newWaveCenterY); - - updateTargetPositions(newWaveCenterX, newWaveCenterY); - updatePointCloudPosition(newWaveCenterX, newWaveCenterY); - updateGlowPosition(newWaveCenterX, newWaveCenterY); - - mWaveCenterX = newWaveCenterX; - mWaveCenterY = newWaveCenterY; - - if (DEBUG) dump(); - } - - private void updateTargetPositions(float centerX, float centerY) { - // Reposition the target drawables if the view changed. - ArrayList targets = mTargetDrawables; - final int size = targets.size(); - final float alpha = (float) (-2.0f * Math.PI / size); - for (int i = 0; i < size; i++) { - final TargetDrawable targetIcon = targets.get(i); - final float angle = alpha * i; - targetIcon.setPositionX(centerX); - targetIcon.setPositionY(centerY); - targetIcon.setX(getRingWidth() / 2 * (float) Math.cos(angle)); - targetIcon.setY(getRingHeight() / 2 * (float) Math.sin(angle)); - } - } - - private void updatePointCloudPosition(float centerX, float centerY) { - mPointCloud.setCenter(centerX, centerY); - } - - @Override - protected void onDraw(Canvas canvas) { - mPointCloud.draw(canvas); - mOuterRing.draw(canvas); - final int ntargets = mTargetDrawables.size(); - for (int i = 0; i < ntargets; i++) { - TargetDrawable target = mTargetDrawables.get(i); - if (target != null) { - target.draw(canvas); - } - } - mHandleDrawable.draw(canvas); - } - - public void setOnTriggerListener(OnTriggerListener listener) { - mOnTriggerListener = listener; - } - - private float square(float d) { - return d * d; - } - - private float dist2(float dx, float dy) { - return dx*dx + dy*dy; - } - - private float getScaledGlowRadiusSquared() { - final float scaledTapRadius; - final AccessibilityManager accessibilityManager = - (AccessibilityManager) getContext().getSystemService( - Context.ACCESSIBILITY_SERVICE); - if (accessibilityManager.isEnabled()) { - scaledTapRadius = TAP_RADIUS_SCALE_ACCESSIBILITY_ENABLED * mGlowRadius; - } else { - scaledTapRadius = mGlowRadius; - } - return square(scaledTapRadius); - } - - private void announceTargets() { - StringBuilder utterance = new StringBuilder(); - final int targetCount = mTargetDrawables.size(); - for (int i = 0; i < targetCount; i++) { - String targetDescription = getTargetDescription(i); - String directionDescription = getDirectionDescription(i); - if (!TextUtils.isEmpty(targetDescription) - && !TextUtils.isEmpty(directionDescription)) { - String text = String.format(directionDescription, targetDescription); - utterance.append(text); - } - } - if (utterance.length() > 0) { - announceForAccessibility(utterance.toString()); - } - } - - private String getTargetDescription(int index) { - if (mTargetDescriptions == null || mTargetDescriptions.isEmpty()) { - mTargetDescriptions = loadDescriptions(mTargetDescriptionsResourceId); - if (mTargetDrawables.size() != mTargetDescriptions.size()) { - Log.w(TAG, "The number of target drawables must be" - + " equal to the number of target descriptions."); - return null; - } - } - return mTargetDescriptions.get(index); - } - - private String getDirectionDescription(int index) { - if (mDirectionDescriptions == null || mDirectionDescriptions.isEmpty()) { - mDirectionDescriptions = loadDescriptions(mDirectionDescriptionsResourceId); - if (mTargetDrawables.size() != mDirectionDescriptions.size()) { - Log.w(TAG, "The number of target drawables must be" - + " equal to the number of direction descriptions."); - return null; - } - } - return mDirectionDescriptions.get(index); - } - - private ArrayList loadDescriptions(int resourceId) { - TypedArray array = getContext().getResources().obtainTypedArray(resourceId); - final int count = array.length(); - ArrayList targetContentDescriptions = new ArrayList(count); - for (int i = 0; i < count; i++) { - String contentDescription = array.getString(i); - targetContentDescriptions.add(contentDescription); - } - array.recycle(); - return targetContentDescriptions; - } - - public int getResourceIdForTarget(int index) { - final TargetDrawable drawable = mTargetDrawables.get(index); - return drawable == null ? 0 : drawable.getResourceId(); - } - - public void setEnableTarget(int resourceId, boolean enabled) { - for (int i = 0; i < mTargetDrawables.size(); i++) { - final TargetDrawable target = mTargetDrawables.get(i); - if (target.getResourceId() == resourceId) { - target.setEnabled(enabled); - break; // should never be more than one match - } - } - } - - /** - * Gets the position of a target in the array that matches the given resource. - * @param resourceId - * @return the index or -1 if not found - */ - public int getTargetPosition(int resourceId) { - for (int i = 0; i < mTargetDrawables.size(); i++) { - final TargetDrawable target = mTargetDrawables.get(i); - if (target.getResourceId() == resourceId) { - return i; // should never be more than one match - } - } - return -1; - } - - private boolean replaceTargetDrawables(Resources res, int existingResourceId, - int newResourceId) { - if (existingResourceId == 0 || newResourceId == 0) { - return false; - } - - boolean result = false; - final ArrayList drawables = mTargetDrawables; - final int size = drawables.size(); - for (int i = 0; i < size; i++) { - final TargetDrawable target = drawables.get(i); - if (target != null && target.getResourceId() == existingResourceId) { - target.setDrawable(res, newResourceId); - result = true; - } - } - - if (result) { - requestLayout(); // in case any given drawable's size changes - } - - return result; - } - - /** - * Searches the given package for a resource to use to replace the Drawable on the - * target with the given resource id - * @param component of the .apk that contains the resource - * @param name of the metadata in the .apk - * @param existingResId the resource id of the target to search for - * @return true if found in the given package and replaced at least one target Drawables - */ - public boolean replaceTargetDrawablesIfPresent(ComponentName component, String name, - int existingResId) { - if (existingResId == 0) return false; - - boolean replaced = false; - if (component != null) { - try { - PackageManager packageManager = getContext().getPackageManager(); - // Look for the search icon specified in the activity meta-data - Bundle metaData = packageManager.getActivityInfo( - component, PackageManager.GET_META_DATA).metaData; - if (metaData != null) { - int iconResId = metaData.getInt(name); - if (iconResId != 0) { - Resources res = packageManager.getResourcesForActivity(component); - replaced = replaceTargetDrawables(res, existingResId, iconResId); - } - } - } catch (NameNotFoundException e) { - Log.w(TAG, "Failed to swap drawable; " - + component.flattenToShortString() + " not found", e); - } catch (Resources.NotFoundException nfe) { - Log.w(TAG, "Failed to swap drawable from " - + component.flattenToShortString(), nfe); - } - } - if (!replaced) { - // Restore the original drawable - replaceTargetDrawables(getContext().getResources(), existingResId, existingResId); - } - return replaced; - } - - public class GlowpadExploreByTouchHelper extends ExploreByTouchHelper { - - private Rect mBounds = new Rect(); - - public GlowpadExploreByTouchHelper(View forView) { - super(forView); - } - - @Override - protected int getVirtualViewAt(float x, float y) { - if (mGrabbedState == OnTriggerListener.CENTER_HANDLE) { - for (int i = 0; i < mTargetDrawables.size(); i++) { - final TargetDrawable target = mTargetDrawables.get(i); - if (target.isEnabled() && target.getBounds().contains((int) x, (int) y)) { - return i; - } - } - return INVALID_ID; - } else { - return HOST_ID; - } - } - - @Override - protected void getVisibleVirtualViews(List virtualViewIds) { - if (mGrabbedState == OnTriggerListener.CENTER_HANDLE) { - // Add virtual views backwards so that accessibility services like switch - // access traverse them in the correct order - for (int i = mTargetDrawables.size() - 1; i >= 0; i--) { - if (mTargetDrawables.get(i).isEnabled()) { - virtualViewIds.add(i); - } - } - } - } - - @Override - protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) { - if (virtualViewId >= 0 && virtualViewId < mTargetDescriptions.size()) { - event.setContentDescription(mTargetDescriptions.get(virtualViewId)); - } - } - - @Override - public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { - if (host == GlowPadView.this && event.getEventType() - == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) { - event.setContentChangeTypes(AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE); - } - super.onInitializeAccessibilityEvent(host, event); - } - - @Override - public void onPopulateNodeForHost(AccessibilityNodeInfoCompat node) { - if (mGrabbedState == OnTriggerListener.NO_HANDLE) { - node.setClickable(true); - node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK); - } - mBounds.set(0, 0, GlowPadView.this.getWidth(), GlowPadView.this.getHeight()); - node.setBoundsInParent(mBounds); - } - - @Override - public boolean performAccessibilityAction(View host, int action, Bundle args) { - if (mGrabbedState == OnTriggerListener.NO_HANDLE) { - // Simulate handle being grabbed to expose targets. - trySwitchToFirstTouchState(mWaveCenterX, mWaveCenterY); - invalidateRoot(); - return true; - } - return super.performAccessibilityAction(host, action, args); - } - - @Override - protected void onPopulateNodeForVirtualView(int virtualViewId, - AccessibilityNodeInfoCompat node) { - if (virtualViewId < mTargetDrawables.size()) { - final TargetDrawable target = mTargetDrawables.get(virtualViewId); - node.setBoundsInParent(target.getBounds()); - node.setClickable(true); - node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK); - node.setContentDescription(getTargetDescription(virtualViewId)); - } - } - - @Override - protected boolean onPerformActionForVirtualView(int virtualViewId, int action, - Bundle arguments) { - if (action == AccessibilityNodeInfo.ACTION_CLICK) { - if (virtualViewId >= 0 && virtualViewId < mTargetDrawables.size()) { - dispatchTriggerEvent(virtualViewId); - return true; - } - } - return false; - } - - } -} diff --git a/InCallUI/src/com/android/incallui/widget/multiwaveview/PointCloud.java b/InCallUI/src/com/android/incallui/widget/multiwaveview/PointCloud.java deleted file mode 100644 index 07a2cb964..000000000 --- a/InCallUI/src/com/android/incallui/widget/multiwaveview/PointCloud.java +++ /dev/null @@ -1,235 +0,0 @@ -/* - * 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.incallui.widget.multiwaveview; - -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.drawable.Drawable; -import android.util.Log; - -import java.util.ArrayList; - -public class PointCloud { - private static final float MIN_POINT_SIZE = 2.0f; - private static final float MAX_POINT_SIZE = 4.0f; - private static final int INNER_POINTS = 8; - private static final String TAG = "PointCloud"; - private ArrayList mPointCloud = new ArrayList(); - private Drawable mDrawable; - private float mCenterX; - private float mCenterY; - private Paint mPaint; - private float mScale = 1.0f; - private static final float PI = (float) Math.PI; - - // These allow us to have multiple concurrent animations. - WaveManager waveManager = new WaveManager(); - GlowManager glowManager = new GlowManager(); - private float mOuterRadius; - - public class WaveManager { - private float radius = 50; - private float width = 200.0f; // TODO: Make configurable - private float alpha = 0.0f; - public void setRadius(float r) { - radius = r; - } - - public float getRadius() { - return radius; - } - - public void setAlpha(float a) { - alpha = a; - } - - public float getAlpha() { - return alpha; - } - }; - - public class GlowManager { - private float x; - private float y; - private float radius = 0.0f; - private float alpha = 0.0f; - - public void setX(float x1) { - x = x1; - } - - public float getX() { - return x; - } - - public void setY(float y1) { - y = y1; - } - - public float getY() { - return y; - } - - public void setAlpha(float a) { - alpha = a; - } - - public float getAlpha() { - return alpha; - } - - public void setRadius(float r) { - radius = r; - } - - public float getRadius() { - return radius; - } - } - - class Point { - float x; - float y; - float radius; - - public Point(float x2, float y2, float r) { - x = (float) x2; - y = (float) y2; - radius = r; - } - } - - public PointCloud(Drawable drawable) { - mPaint = new Paint(); - mPaint.setFilterBitmap(true); - mPaint.setColor(Color.rgb(255, 255, 255)); // TODO: make configurable - mPaint.setAntiAlias(true); - mPaint.setDither(true); - - mDrawable = drawable; - if (mDrawable != null) { - drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); - } - } - - public void setCenter(float x, float y) { - mCenterX = x; - mCenterY = y; - } - - public void makePointCloud(float innerRadius, float outerRadius) { - if (innerRadius == 0) { - Log.w(TAG, "Must specify an inner radius"); - return; - } - mOuterRadius = outerRadius; - mPointCloud.clear(); - final float pointAreaRadius = (outerRadius - innerRadius); - final float ds = (2.0f * PI * innerRadius / INNER_POINTS); - final int bands = (int) Math.round(pointAreaRadius / ds); - final float dr = pointAreaRadius / bands; - float r = innerRadius; - for (int b = 0; b <= bands; b++, r += dr) { - float circumference = 2.0f * PI * r; - final int pointsInBand = (int) (circumference / ds); - float eta = PI/2.0f; - float dEta = 2.0f * PI / pointsInBand; - for (int i = 0; i < pointsInBand; i++) { - float x = r * (float) Math.cos(eta); - float y = r * (float) Math.sin(eta); - eta += dEta; - mPointCloud.add(new Point(x, y, r)); - } - } - } - - public void setScale(float scale) { - mScale = scale; - } - - public float getScale() { - return mScale; - } - - private static float hypot(float x, float y) { - return (float) Math.hypot(x, y); - } - - private static float max(float a, float b) { - return a > b ? a : b; - } - - public int getAlphaForPoint(Point point) { - // Contribution from positional glow - float glowDistance = hypot(glowManager.x - point.x, glowManager.y - point.y); - float glowAlpha = 0.0f; - - if (glowDistance < glowManager.radius) { - double cos = Math.cos(Math.PI * 0.25d * glowDistance / glowManager.radius); - glowAlpha = glowManager.alpha * max(0.0f, (float) Math.pow(cos, 10.0d)); - } - - // Compute contribution from Wave - float radius = hypot(point.x, point.y); - float distanceToWaveRing = (radius - waveManager.radius); - float waveAlpha = 0.0f; - if (distanceToWaveRing < waveManager.width * 0.5f && distanceToWaveRing < 0.0f) { - double cos = Math.cos(Math.PI * 0.25d * distanceToWaveRing / waveManager.width); - waveAlpha = waveManager.alpha * max(0.0f, (float) Math.pow(cos, 20.0d)); - } - - return (int) (max(glowAlpha, waveAlpha) * 255); - } - - private float interp(float min, float max, float f) { - return min + (max - min) * f; - } - - public void draw(Canvas canvas) { - ArrayList points = mPointCloud; - canvas.save(Canvas.MATRIX_SAVE_FLAG); - canvas.scale(mScale, mScale, mCenterX, mCenterY); - for (int i = 0; i < points.size(); i++) { - Point point = points.get(i); - final float pointSize = interp(MAX_POINT_SIZE, MIN_POINT_SIZE, - point.radius / mOuterRadius); - final float px = point.x + mCenterX; - final float py = point.y + mCenterY; - int alpha = getAlphaForPoint(point); - - if (alpha == 0) continue; - - if (mDrawable != null) { - canvas.save(Canvas.MATRIX_SAVE_FLAG); - final float cx = mDrawable.getIntrinsicWidth() * 0.5f; - final float cy = mDrawable.getIntrinsicHeight() * 0.5f; - final float s = pointSize / MAX_POINT_SIZE; - canvas.scale(s, s, px, py); - canvas.translate(px - cx, py - cy); - mDrawable.setAlpha(alpha); - mDrawable.draw(canvas); - canvas.restore(); - } else { - mPaint.setAlpha(alpha); - canvas.drawCircle(px, py, pointSize, mPaint); - } - } - canvas.restore(); - } - -} diff --git a/InCallUI/src/com/android/incallui/widget/multiwaveview/TargetDrawable.java b/InCallUI/src/com/android/incallui/widget/multiwaveview/TargetDrawable.java deleted file mode 100644 index adc5324eb..000000000 --- a/InCallUI/src/com/android/incallui/widget/multiwaveview/TargetDrawable.java +++ /dev/null @@ -1,250 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui.widget.multiwaveview; - -import android.content.res.Resources; -import android.graphics.Canvas; -import android.graphics.ColorFilter; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.StateListDrawable; -import android.util.Log; - -public class TargetDrawable { - private static final String TAG = "TargetDrawable"; - private static final boolean DEBUG = false; - - public static final int[] STATE_ACTIVE = - { android.R.attr.state_enabled, android.R.attr.state_active }; - public static final int[] STATE_INACTIVE = - { android.R.attr.state_enabled, -android.R.attr.state_active }; - public static final int[] STATE_FOCUSED = - { android.R.attr.state_enabled, -android.R.attr.state_active, - android.R.attr.state_focused }; - - private float mTranslationX = 0.0f; - private float mTranslationY = 0.0f; - private float mPositionX = 0.0f; - private float mPositionY = 0.0f; - private float mScaleX = 1.0f; - private float mScaleY = 1.0f; - private float mAlpha = 1.0f; - private Drawable mDrawable; - private boolean mEnabled = true; - private final int mResourceId; - private int mNumDrawables = 1; - private Rect mBounds; - - /** - * This is changed from the framework version to pass in the number of drawables in the - * container. The framework version relies on private api's to get the count from - * StateListDrawable. - * - * @param res - * @param resId - * @param count The number of drawables in the resource. - */ - public TargetDrawable(Resources res, int resId, int count) { - mResourceId = resId; - setDrawable(res, resId); - mNumDrawables = count; - } - - public void setDrawable(Resources res, int resId) { - // Note we explicitly don't set mResourceId to resId since we allow the drawable to be - // swapped at runtime and want to re-use the existing resource id for identification. - Drawable drawable = resId == 0 ? null : res.getDrawable(resId); - // Mutate the drawable so we can animate shared drawable properties. - mDrawable = drawable != null ? drawable.mutate() : null; - resizeDrawables(); - setState(STATE_INACTIVE); - } - - public TargetDrawable(TargetDrawable other) { - mResourceId = other.mResourceId; - // Mutate the drawable so we can animate shared drawable properties. - mDrawable = other.mDrawable != null ? other.mDrawable.mutate() : null; - resizeDrawables(); - setState(STATE_INACTIVE); - } - - public void setState(int [] state) { - if (mDrawable instanceof StateListDrawable) { - StateListDrawable d = (StateListDrawable) mDrawable; - d.setState(state); - } - } - - /** - * Returns true if the drawable is a StateListDrawable and is in the focused state. - * - * @return - */ - public boolean isActive() { - if (mDrawable instanceof StateListDrawable) { - StateListDrawable d = (StateListDrawable) mDrawable; - int[] states = d.getState(); - for (int i = 0; i < states.length; i++) { - if (states[i] == android.R.attr.state_focused) { - return true; - } - } - } - return false; - } - - /** - * Returns true if this target is enabled. Typically an enabled target contains a valid - * drawable in a valid state. Currently all targets with valid drawables are valid. - * - * @return - */ - public boolean isEnabled() { - return mDrawable != null && mEnabled; - } - - /** - * Makes drawables in a StateListDrawable all the same dimensions. - * If not a StateListDrawable, then justs sets the bounds to the intrinsic size of the - * drawable. - */ - private void resizeDrawables() { - if (mDrawable instanceof StateListDrawable) { - StateListDrawable d = (StateListDrawable) mDrawable; - int maxWidth = 0; - int maxHeight = 0; - - for (int i = 0; i < mNumDrawables; i++) { - d.selectDrawable(i); - Drawable childDrawable = d.getCurrent(); - maxWidth = Math.max(maxWidth, childDrawable.getIntrinsicWidth()); - maxHeight = Math.max(maxHeight, childDrawable.getIntrinsicHeight()); - } - - if (DEBUG) Log.v(TAG, "union of childDrawable rects " + d + " to: " - + maxWidth + "x" + maxHeight); - d.setBounds(0, 0, maxWidth, maxHeight); - - for (int i = 0; i < mNumDrawables; i++) { - d.selectDrawable(i); - Drawable childDrawable = d.getCurrent(); - if (DEBUG) Log.v(TAG, "sizing drawable " + childDrawable + " to: " - + maxWidth + "x" + maxHeight); - childDrawable.setBounds(0, 0, maxWidth, maxHeight); - } - } else if (mDrawable != null) { - mDrawable.setBounds(0, 0, - mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight()); - } - } - - public void setX(float x) { - mTranslationX = x; - } - - public void setY(float y) { - mTranslationY = y; - } - - public void setScaleX(float x) { - mScaleX = x; - } - - public void setScaleY(float y) { - mScaleY = y; - } - - public void setAlpha(float alpha) { - mAlpha = alpha; - } - - public float getX() { - return mTranslationX; - } - - public float getY() { - return mTranslationY; - } - - public float getScaleX() { - return mScaleX; - } - - public float getScaleY() { - return mScaleY; - } - - public float getAlpha() { - return mAlpha; - } - - public void setPositionX(float x) { - mPositionX = x; - } - - public void setPositionY(float y) { - mPositionY = y; - } - - public float getPositionX() { - return mPositionX; - } - - public float getPositionY() { - return mPositionY; - } - - public int getWidth() { - return mDrawable != null ? mDrawable.getIntrinsicWidth() : 0; - } - - public int getHeight() { - return mDrawable != null ? mDrawable.getIntrinsicHeight() : 0; - } - - public Rect getBounds() { - if (mBounds == null) { - mBounds = new Rect(); - } - mBounds.set((int) (mTranslationX + mPositionX - getWidth() * 0.5), - (int) (mTranslationY + mPositionY - getHeight() * 0.5), - (int) (mTranslationX + mPositionX + getWidth() * 0.5), - (int) (mTranslationY + mPositionY + getHeight() * 0.5)); - return mBounds; - } - - public void draw(Canvas canvas) { - if (mDrawable == null || !mEnabled) { - return; - } - canvas.save(Canvas.MATRIX_SAVE_FLAG); - canvas.scale(mScaleX, mScaleY, mPositionX, mPositionY); - canvas.translate(mTranslationX + mPositionX, mTranslationY + mPositionY); - canvas.translate(-0.5f * getWidth(), -0.5f * getHeight()); - mDrawable.setAlpha((int) Math.round(mAlpha * 255f)); - mDrawable.draw(canvas); - canvas.restore(); - } - - public void setEnabled(boolean enabled) { - mEnabled = enabled; - } - - public int getResourceId() { - return mResourceId; - } -} diff --git a/InCallUI/src/com/android/incallui/widget/multiwaveview/Tweener.java b/InCallUI/src/com/android/incallui/widget/multiwaveview/Tweener.java deleted file mode 100644 index 7222442fe..000000000 --- a/InCallUI/src/com/android/incallui/widget/multiwaveview/Tweener.java +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui.widget.multiwaveview; - -import android.animation.Animator; -import android.animation.Animator.AnimatorListener; -import android.animation.AnimatorListenerAdapter; -import android.animation.ObjectAnimator; -import android.animation.PropertyValuesHolder; -import android.animation.TimeInterpolator; -import android.animation.ValueAnimator.AnimatorUpdateListener; -import android.util.Log; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map.Entry; - -class Tweener { - private static final String TAG = "Tweener"; - private static final boolean DEBUG = false; - - ObjectAnimator animator; - private static HashMap sTweens = new HashMap(); - - public Tweener(ObjectAnimator anim) { - animator = anim; - } - - private static void remove(Animator animator) { - Iterator> iter = sTweens.entrySet().iterator(); - while (iter.hasNext()) { - Entry entry = iter.next(); - if (entry.getValue().animator == animator) { - if (DEBUG) Log.v(TAG, "Removing tweener " + sTweens.get(entry.getKey()) - + " sTweens.size() = " + sTweens.size()); - iter.remove(); - break; // an animator can only be attached to one object - } - } - } - - public static Tweener to(Object object, long duration, Object... vars) { - long delay = 0; - AnimatorUpdateListener updateListener = null; - AnimatorListener listener = null; - TimeInterpolator interpolator = null; - - // Iterate through arguments and discover properties to animate - ArrayList props = new ArrayList(vars.length/2); - for (int i = 0; i < vars.length; i+=2) { - if (!(vars[i] instanceof String)) { - throw new IllegalArgumentException("Key must be a string: " + vars[i]); - } - String key = (String) vars[i]; - Object value = vars[i+1]; - - if ("simultaneousTween".equals(key)) { - // TODO - } else if ("ease".equals(key)) { - interpolator = (TimeInterpolator) value; // TODO: multiple interpolators? - } else if ("onUpdate".equals(key) || "onUpdateListener".equals(key)) { - updateListener = (AnimatorUpdateListener) value; - } else if ("onComplete".equals(key) || "onCompleteListener".equals(key)) { - listener = (AnimatorListener) value; - } else if ("delay".equals(key)) { - delay = ((Number) value).longValue(); - } else if ("syncWith".equals(key)) { - // TODO - } else if (value instanceof float[]) { - props.add(PropertyValuesHolder.ofFloat(key, - ((float[])value)[0], ((float[])value)[1])); - } else if (value instanceof int[]) { - props.add(PropertyValuesHolder.ofInt(key, - ((int[])value)[0], ((int[])value)[1])); - } else if (value instanceof Number) { - float floatValue = ((Number)value).floatValue(); - props.add(PropertyValuesHolder.ofFloat(key, floatValue)); - } else { - throw new IllegalArgumentException( - "Bad argument for key \"" + key + "\" with value " + value.getClass()); - } - } - - // Re-use existing tween, if present - Tweener tween = sTweens.get(object); - ObjectAnimator anim = null; - if (tween == null) { - anim = ObjectAnimator.ofPropertyValuesHolder(object, - props.toArray(new PropertyValuesHolder[props.size()])); - tween = new Tweener(anim); - sTweens.put(object, tween); - if (DEBUG) Log.v(TAG, "Added new Tweener " + tween); - } else { - anim = sTweens.get(object).animator; - replace(props, object); // Cancel all animators for given object - } - - if (interpolator != null) { - anim.setInterpolator(interpolator); - } - - // Update animation with properties discovered in loop above - anim.setStartDelay(delay); - anim.setDuration(duration); - if (updateListener != null) { - anim.removeAllUpdateListeners(); // There should be only one - anim.addUpdateListener(updateListener); - } - if (listener != null) { - anim.removeAllListeners(); // There should be only one. - anim.addListener(listener); - } - anim.addListener(mCleanupListener); - - return tween; - } - - Tweener from(Object object, long duration, Object... vars) { - // TODO: for v of vars - // toVars[v] = object[v] - // object[v] = vars[v] - return Tweener.to(object, duration, vars); - } - - // Listener to watch for completed animations and remove them. - private static AnimatorListener mCleanupListener = new AnimatorListenerAdapter() { - - @Override - public void onAnimationEnd(Animator animation) { - remove(animation); - } - - @Override - public void onAnimationCancel(Animator animation) { - remove(animation); - } - }; - - public static void reset() { - if (DEBUG) { - Log.v(TAG, "Reset()"); - if (sTweens.size() > 0) { - Log.v(TAG, "Cleaning up " + sTweens.size() + " animations"); - } - } - sTweens.clear(); - } - - private static void replace(ArrayList props, Object... args) { - for (final Object killobject : args) { - Tweener tween = sTweens.get(killobject); - if (tween != null) { - tween.animator.cancel(); - if (props != null) { - tween.animator.setValues( - props.toArray(new PropertyValuesHolder[props.size()])); - } else { - sTweens.remove(tween); - } - } - } - } -} diff --git a/InCallUI/src/com/android/incalluibind/ObjectFactory.java b/InCallUI/src/com/android/incalluibind/ObjectFactory.java deleted file mode 100644 index 7e9423acf..000000000 --- a/InCallUI/src/com/android/incalluibind/ObjectFactory.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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.incalluibind; - -import android.content.Context; -import android.content.Intent; - -import com.android.incallui.CallCardPresenter.EmergencyCallListener; -import com.android.incallui.ContactUtils; -import com.android.incallui.DistanceHelper; -import com.android.incallui.service.PhoneNumberService; - -public class ObjectFactory { - - public static PhoneNumberService newPhoneNumberService(Context context) { - // no phone number service. - return null; - } - - public static EmergencyCallListener newEmergencyCallListener() { - return null; - } - - /** @return An {@link Intent} to be broadcast when the InCallUI is visible. */ - public static Intent getUiReadyBroadcastIntent(Context context) { - return null; - } - - /** - * @return An {@link Intent} to be broadcast when the call state button in the InCallUI is - * touched while in a call. - */ - public static Intent getCallStateButtonBroadcastIntent(Context context) { - return null; - } - - public static DistanceHelper newDistanceHelper(Context context, - DistanceHelper.Listener listener) { - return null; - } - - public static ContactUtils getContactUtilsInstance(Context context) { - return null; - } -} diff --git a/InCallUI/tests/src/com/android/incallui/CallCardPresenterTest.java b/InCallUI/tests/src/com/android/incallui/CallCardPresenterTest.java deleted file mode 100644 index 79545ce4b..000000000 --- a/InCallUI/tests/src/com/android/incallui/CallCardPresenterTest.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.test.AndroidTestCase; -import android.test.suitebuilder.annotation.MediumTest; - -import com.android.contacts.common.preference.ContactsPreferences; -import com.android.incallui.ContactInfoCache.ContactCacheEntry; - -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; - -@MediumTest -public class CallCardPresenterTest extends AndroidTestCase { - - private static final String NAME_PRIMARY = "Full Name"; - private static final String NAME_ALTERNATIVE = "Name, Full"; - private static final String LOCATION = "US"; - private static final String NUMBER = "8006459001"; - - @Mock private ContactsPreferences mContactsPreferences; - private ContactCacheEntry mUnlockedContactInfo; - private ContactCacheEntry mLockedContactInfo; - - @Override - public void setUp() throws Exception { - super.setUp(); - MockitoAnnotations.initMocks(this); - - Mockito.when(mContactsPreferences.getDisplayOrder()) - .thenReturn(ContactsPreferences.DISPLAY_ORDER_PRIMARY); - - // Unlocked all contact info is available - mUnlockedContactInfo = new ContactCacheEntry(); - mUnlockedContactInfo.namePrimary = NAME_PRIMARY; - mUnlockedContactInfo.nameAlternative = NAME_ALTERNATIVE; - mUnlockedContactInfo.location = LOCATION; - mUnlockedContactInfo.number = NUMBER; - - // Locked only number and location are available - mLockedContactInfo = new ContactCacheEntry(); - mLockedContactInfo .location = LOCATION; - mLockedContactInfo .number = NUMBER; - } - - @Override - public void tearDown() throws Exception { - super.tearDown(); - ContactsPreferencesFactory.setTestInstance(null); - } - - public void testGetNameForCall_Unlocked() { - ContactsPreferencesFactory.setTestInstance(mContactsPreferences); - CallCardPresenter presenter = new CallCardPresenter(); - presenter.init(getContext(), null); - - assertEquals(NAME_PRIMARY, presenter.getNameForCall(mUnlockedContactInfo)); - } - - public void testGetNameForCall_Locked() { - ContactsPreferencesFactory.setTestInstance(null); - CallCardPresenter presenter = new CallCardPresenter(); - presenter.init(getContext(), null); - - assertEquals(NUMBER, presenter.getNameForCall(mLockedContactInfo)); - } - - public void testGetNameForCall_EmptyPreferredName() { - ContactCacheEntry contactInfo = new ContactCacheEntry(); - contactInfo.number = NUMBER; - - ContactsPreferencesFactory.setTestInstance(null); - CallCardPresenter presenter = new CallCardPresenter(); - presenter.init(getContext(), null); - - assertEquals(NUMBER, presenter.getNameForCall(contactInfo)); - } - - public void testGetNumberForCall_Unlocked() { - ContactsPreferencesFactory.setTestInstance(mContactsPreferences); - CallCardPresenter presenter = new CallCardPresenter(); - presenter.init(getContext(), null); - - assertEquals(NUMBER, presenter.getNumberForCall(mUnlockedContactInfo)); - } - - public void testGetNumberForCall_Locked() { - ContactsPreferencesFactory.setTestInstance(null); - CallCardPresenter presenter = new CallCardPresenter(); - presenter.init(getContext(), null); - - assertEquals(LOCATION, presenter.getNumberForCall(mLockedContactInfo)); - } - - public void testGetNumberForCall_EmptyPreferredName() { - ContactCacheEntry contactInfo = new ContactCacheEntry(); - contactInfo.location = LOCATION; - - ContactsPreferencesFactory.setTestInstance(null); - CallCardPresenter presenter = new CallCardPresenter(); - presenter.init(getContext(), null); - - assertEquals(LOCATION, presenter.getNumberForCall(contactInfo)); - } -} diff --git a/InCallUI/tests/src/com/android/incallui/CallTest.java b/InCallUI/tests/src/com/android/incallui/CallTest.java deleted file mode 100644 index 118ec38da..000000000 --- a/InCallUI/tests/src/com/android/incallui/CallTest.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.os.Bundle; -import android.telecom.Connection; -import android.test.AndroidTestCase; -import android.test.suitebuilder.annotation.SmallTest; - -import java.util.ArrayList; -import java.util.Arrays; - -// @formatter:off -/** - * Run test with - * adb shell am instrument -e class com.android.incallui.CallTest -w com.google.android.dialer.tests/android.test.InstrumentationTestRunner - */ -// @formatter:on - -@SmallTest -public class CallTest extends AndroidTestCase { - - private TestCall mCall; - - private final static String CHILD_NUMBER = "123"; - private final static ArrayList LAST_FORWARDED_NUMBER_LIST = - new ArrayList(Arrays.asList("456", "789")); - private final static String LAST_FORWARDED_NUMBER = "789"; - private final static String CALL_SUBJECT = "foo"; - - @Override - public void setUp() throws Exception { - super.setUp(); - - mCall = new TestCall(); - } - - @Override - public void tearDown() throws Exception { - super.tearDown(); - } - - public void testUpdateFromCallExtras() { - mCall.updateFromCallExtras(getTestBundle()); - verifyTestBundleResult(); - } - - public void testUpdateFromCallExtras_corruptedBundle() { - mCall.setBundleCorrupted(true); - mCall.updateFromCallExtras(getTestBundle()); - - assertEquals(mCall.getChildNumber(), null); - assertEquals(mCall.getLastForwardedNumber(), null); - assertEquals(mCall.getCallSubject(), null); - } - - public void testUpdateFromCallExtras_corruptedBundleOverwrite() { - - mCall.updateFromCallExtras(getTestBundle()); - mCall.setBundleCorrupted(true); - Bundle bundle = new Bundle(); - bundle.putString(Connection.EXTRA_CHILD_ADDRESS, "321"); - bundle.putStringArrayList(Connection.EXTRA_LAST_FORWARDED_NUMBER, - new ArrayList(Arrays.asList("654", "987"))); - bundle.putString(Connection.EXTRA_CALL_SUBJECT, "bar"); - mCall.updateFromCallExtras(bundle); - //corrupted bundle should not overwrite existing values. - verifyTestBundleResult(); - } - - private Bundle getTestBundle() { - Bundle bundle = new Bundle(); - bundle.putString(Connection.EXTRA_CHILD_ADDRESS, CHILD_NUMBER); - bundle.putStringArrayList(Connection.EXTRA_LAST_FORWARDED_NUMBER, - LAST_FORWARDED_NUMBER_LIST); - bundle.putString(Connection.EXTRA_CALL_SUBJECT, CALL_SUBJECT); - return bundle; - } - - private void verifyTestBundleResult() { - assertEquals(CHILD_NUMBER, mCall.getChildNumber()); - assertEquals(LAST_FORWARDED_NUMBER, mCall.getLastForwardedNumber()); - assertEquals(CALL_SUBJECT, mCall.getCallSubject()); - } - - private class TestCall extends Call { - - private boolean mBundleCorrupted = false; - - public TestCall() { - super(Call.State.NEW); - } - - @Override - public void updateFromCallExtras(Bundle bundle) { - super.updateFromCallExtras(bundle); - } - - public void setBundleCorrupted(boolean value) { - this.mBundleCorrupted = value; - } - - @Override - protected boolean areCallExtrasCorrupted(Bundle callExtras) { - if (mBundleCorrupted) { - return true; - } - return super.areCallExtrasCorrupted(callExtras); - } - } -} diff --git a/InCallUI/tests/src/com/android/incallui/CallerInfoUtilsTest.java b/InCallUI/tests/src/com/android/incallui/CallerInfoUtilsTest.java deleted file mode 100644 index de5a0239e..000000000 --- a/InCallUI/tests/src/com/android/incallui/CallerInfoUtilsTest.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.test.AndroidTestCase; -import android.test.suitebuilder.annotation.SmallTest; - -@SmallTest -public class CallerInfoUtilsTest extends AndroidTestCase { - public void testToLogSafeNumber_email() { - assertEquals("xxx@xxx.xxx", CallerInfoUtils.toLogSafePhoneNumber("foo@foo.com")); - } - - public void testToLogSafeNumber_phoneNumber() { - assertEquals("xxx-xxx-xxxx", CallerInfoUtils.toLogSafePhoneNumber("123-456-6789")); - } -} diff --git a/InCallUI/tests/src/com/android/incallui/ContactsPreferencesFactoryTest.java b/InCallUI/tests/src/com/android/incallui/ContactsPreferencesFactoryTest.java deleted file mode 100644 index bf915553b..000000000 --- a/InCallUI/tests/src/com/android/incallui/ContactsPreferencesFactoryTest.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import com.android.dialer.compat.UserManagerCompat; -import android.test.AndroidTestCase; -import android.test.suitebuilder.annotation.SmallTest; - -import com.android.contacts.common.preference.ContactsPreferences; - -import org.mockito.Mockito; - -@SmallTest -public class ContactsPreferencesFactoryTest extends AndroidTestCase { - - public void testNewContactsPreferences_Unlocked() { - if (!UserManagerCompat.isUserUnlocked(getContext())) { - return; - } - assertNotNull(ContactsPreferencesFactory.newContactsPreferences(getContext())); - } - - public void testNewContactsPreferences_Locked() { - if (UserManagerCompat.isUserUnlocked(getContext())) { - return; - } - assertNull(ContactsPreferencesFactory.newContactsPreferences(getContext())); - } - - public void testNewContactsPreferences_TestInstance() { - ContactsPreferences testInstance = Mockito.mock(ContactsPreferences.class); - ContactsPreferencesFactory.setTestInstance(testInstance); - // Assert that it returns the same object always - assertSame(testInstance, ContactsPreferencesFactory.newContactsPreferences(getContext())); - assertSame(testInstance, ContactsPreferencesFactory.newContactsPreferences(getContext())); - } -} diff --git a/InCallUI/tests/src/com/android/incallui/ExternalCallListTest.java b/InCallUI/tests/src/com/android/incallui/ExternalCallListTest.java deleted file mode 100644 index 59434700c..000000000 --- a/InCallUI/tests/src/com/android/incallui/ExternalCallListTest.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.content.ComponentName; -import android.content.Context; -import android.net.Uri; -import android.os.Bundle; -import android.telecom.*; -import android.telecom.Call; -import android.test.AndroidTestCase; - -import com.android.contacts.common.compat.CallSdkCompat; - -import java.lang.reflect.Constructor; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -public class ExternalCallListTest extends AndroidTestCase { - - private static class Listener implements ExternalCallList.ExternalCallListener { - private CountDownLatch mCallAddedLatch = new CountDownLatch(1); - private CountDownLatch mCallRemovedLatch = new CountDownLatch(1); - private CountDownLatch mCallUpdatedLatch = new CountDownLatch(1); - - @Override - public void onExternalCallAdded(Call call) { - mCallAddedLatch.countDown(); - } - - @Override - public void onExternalCallRemoved(Call call) { - mCallRemovedLatch.countDown(); - } - - @Override - public void onExternalCallUpdated(Call call) { - mCallUpdatedLatch.countDown(); - } - - public boolean awaitCallAdded() { - try { - return mCallAddedLatch.await(WAIT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - return false; - } - } - - public boolean awaitCallRemoved() { - try { - return mCallRemovedLatch.await(WAIT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - return false; - } - } - - public boolean awaitCallUpdated() { - try { - return mCallUpdatedLatch.await(WAIT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - return false; - } - } - } - - private static final int WAIT_TIMEOUT_MILLIS = 5000; - - private ExternalCallList mExternalCallList = new ExternalCallList(); - private Listener mExternalCallListener = new Listener(); - - @Override - public void setUp() throws Exception { - super.setUp(); - mExternalCallList.addExternalCallListener(mExternalCallListener); - } - - public void testAddCallSuccess() { - TestTelecomCall call = getTestCall(CallSdkCompat.Details.PROPERTY_IS_EXTERNAL_CALL); - mExternalCallList.onCallAdded(call.getCall()); - assertTrue(mExternalCallListener.awaitCallAdded()); - } - - public void testAddCallFail() { - TestTelecomCall call = getTestCall(0 /* no properties */); - try { - mExternalCallList.onCallAdded(call.getCall()); - fail(); - } catch (IllegalArgumentException e) { - } - } - - public void testUpdateCall() { - TestTelecomCall call = getTestCall(CallSdkCompat.Details.PROPERTY_IS_EXTERNAL_CALL); - mExternalCallList.onCallAdded(call.getCall()); - assertTrue(mExternalCallListener.awaitCallAdded()); - - call.forceDetailsUpdate(); - assertTrue(mExternalCallListener.awaitCallUpdated()); - } - - public void testRemoveCall() { - TestTelecomCall call = getTestCall(CallSdkCompat.Details.PROPERTY_IS_EXTERNAL_CALL); - mExternalCallList.onCallAdded(call.getCall()); - assertTrue(mExternalCallListener.awaitCallAdded()); - - mExternalCallList.onCallRemoved(call.getCall()); - assertTrue(mExternalCallListener.awaitCallRemoved()); - } - - private TestTelecomCall getTestCall(int properties) { - TestTelecomCall testCall = TestTelecomCall.createInstance( - "1", - Uri.parse("tel:650-555-1212"), /* handle */ - TelecomManager.PRESENTATION_ALLOWED, /* handlePresentation */ - "Joe", /* callerDisplayName */ - TelecomManager.PRESENTATION_ALLOWED, /* callerDisplayNamePresentation */ - new PhoneAccountHandle(new ComponentName("test", "class"), - "handle"), /* accountHandle */ - CallSdkCompat.Details.CAPABILITY_CAN_PULL_CALL, /* capabilities */ - properties, /* properties */ - null, /* disconnectCause */ - 0, /* connectTimeMillis */ - null, /* GatewayInfo */ - VideoProfile.STATE_AUDIO_ONLY, /* videoState */ - null, /* statusHints */ - null, /* extras */ - null /* intentExtras */); - return testCall; - } -} diff --git a/InCallUI/tests/src/com/android/incallui/ExternalCallNotifierTest.java b/InCallUI/tests/src/com/android/incallui/ExternalCallNotifierTest.java deleted file mode 100644 index 64ddd2ea5..000000000 --- a/InCallUI/tests/src/com/android/incallui/ExternalCallNotifierTest.java +++ /dev/null @@ -1,214 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import static org.mockito.Matchers.anyInt; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.anyBoolean; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.eq; -import static org.mockito.Mockito.timeout; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.android.contacts.common.preference.ContactsPreferences; - -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.mockito.Spy; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; - -import android.app.Notification; -import android.app.NotificationManager; -import android.content.ComponentName; -import android.content.Context; -import android.content.res.Resources; -import android.net.Uri; -import android.telecom.*; -import android.telecom.Call; -import android.telephony.TelephonyManager; -import android.test.AndroidTestCase; -import android.test.mock.MockContext; - -import com.android.contacts.common.compat.CallSdkCompat; - -/** - * Unit tests for {@link ExternalCallNotifier}. - */ -public class ExternalCallNotifierTest extends AndroidTestCase { - private static final int TIMEOUT_MILLIS = 5000; - private static final String NAME_PRIMARY = "Full Name"; - private static final String NAME_ALTERNATIVE = "Name, Full"; - private static final String LOCATION = "US"; - private static final String NUMBER = "6505551212"; - - @Mock private ContactsPreferences mContactsPreferences; - @Mock private NotificationManager mNotificationManager; - @Mock private MockContext mMockContext; - @Mock private Resources mResources; - @Mock private StatusBarNotifier mStatusBarNotifier; - @Mock private ContactInfoCache mContactInfoCache; - @Mock private TelecomManager mTelecomManager; - @Mock private TelephonyManager mTelephonyManager; - @Mock private ProximitySensor mProximitySensor; - @Mock private CallList mCallList; - private InCallPresenter mInCallPresenter; - private ExternalCallNotifier mExternalCallNotifier; - private ContactInfoCache.ContactCacheEntry mContactInfo; - - @Override - public void setUp() throws Exception { - super.setUp(); - MockitoAnnotations.initMocks(this); - - when(mContactsPreferences.getDisplayOrder()) - .thenReturn(ContactsPreferences.DISPLAY_ORDER_PRIMARY); - - // Setup the mock context to return mocks for some of the needed services; the notification - // service is especially important as we want to be able to intercept calls into it and - // validate the notifcations. - when(mMockContext.getSystemService(eq(Context.NOTIFICATION_SERVICE))) - .thenReturn(mNotificationManager); - when(mMockContext.getSystemService(eq(Context.TELECOM_SERVICE))) - .thenReturn(mTelecomManager); - when(mMockContext.getSystemService(eq(Context.TELEPHONY_SERVICE))) - .thenReturn(mTelephonyManager); - - // These aspects of the context are used by the notification builder to build the actual - // notification; we will rely on the actual implementations of these. - when(mMockContext.getPackageManager()).thenReturn(mContext.getPackageManager()); - when(mMockContext.getResources()).thenReturn(mContext.getResources()); - when(mMockContext.getApplicationInfo()).thenReturn(mContext.getApplicationInfo()); - when(mMockContext.getContentResolver()).thenReturn(mContext.getContentResolver()); - when(mMockContext.getPackageName()).thenReturn(mContext.getPackageName()); - - ContactsPreferencesFactory.setTestInstance(null); - mExternalCallNotifier = new ExternalCallNotifier(mMockContext, mContactInfoCache); - - // We don't directly use the InCallPresenter in the test, or even in ExternalCallNotifier - // itself. However, ExternalCallNotifier needs to make instances of - // com.android.incallui.Call for the purpose of performing contact cache lookups. The - // Call class depends on the static InCallPresenter for a number of things, so we need to - // set it up here to prevent crashes. - mInCallPresenter = InCallPresenter.getInstance(); - mInCallPresenter.setUp(mMockContext, mCallList, new ExternalCallList(), - null, mStatusBarNotifier, mExternalCallNotifier, mContactInfoCache, - mProximitySensor); - - // Unlocked all contact info is available - mContactInfo = new ContactInfoCache.ContactCacheEntry(); - mContactInfo.namePrimary = NAME_PRIMARY; - mContactInfo.nameAlternative = NAME_ALTERNATIVE; - mContactInfo.location = LOCATION; - mContactInfo.number = NUMBER; - - // Given the mock ContactInfoCache cache, we need to mock out what happens when the - // ExternalCallNotifier calls into the contact info cache to do a lookup. We will always - // return mock info stored in mContactInfo. - doAnswer(new Answer() { - @Override - public Object answer(InvocationOnMock invocation) throws Throwable { - Object[] args = invocation.getArguments(); - com.android.incallui.Call call = (com.android.incallui.Call) args[0]; - ContactInfoCache.ContactInfoCacheCallback callback - = (ContactInfoCache.ContactInfoCacheCallback) args[2]; - callback.onContactInfoComplete(call.getId(), mContactInfo); - return null; - } - }).when(mContactInfoCache).findInfo(any(com.android.incallui.Call.class), anyBoolean(), - any(ContactInfoCache.ContactInfoCacheCallback.class)); - } - - @Override - public void tearDown() throws Exception { - super.tearDown(); - ContactsPreferencesFactory.setTestInstance(null); - mInCallPresenter.tearDown(); - } - - public void testPostNonPullable() { - TestTelecomCall call = getTestCall(false); - mExternalCallNotifier.onExternalCallAdded(call.getCall()); - Notification notification = verifyNotificationPosted(); - assertNull(notification.actions); - } - - public void testPostPullable() { - TestTelecomCall call = getTestCall(true); - mExternalCallNotifier.onExternalCallAdded(call.getCall()); - Notification notification = verifyNotificationPosted(); - assertEquals(1, notification.actions.length); - } - - public void testNotificationDismissed() { - TestTelecomCall call = getTestCall(false); - mExternalCallNotifier.onExternalCallAdded(call.getCall()); - verifyNotificationPosted(); - - mExternalCallNotifier.onExternalCallRemoved(call.getCall()); - verify(mNotificationManager, timeout(TIMEOUT_MILLIS)).cancel(eq("EXTERNAL_CALL"), eq(0)); - } - - public void testNotificationUpdated() { - TestTelecomCall call = getTestCall(false); - mExternalCallNotifier.onExternalCallAdded(call.getCall()); - verifyNotificationPosted(); - - call.setCapabilities(CallSdkCompat.Details.CAPABILITY_CAN_PULL_CALL); - mExternalCallNotifier.onExternalCallUpdated(call.getCall()); - - ArgumentCaptor notificationCaptor = - ArgumentCaptor.forClass(Notification.class); - verify(mNotificationManager, timeout(TIMEOUT_MILLIS).times(2)) - .notify(eq("EXTERNAL_CALL"), eq(0), notificationCaptor.capture()); - Notification notification1 = notificationCaptor.getAllValues().get(0); - assertNull(notification1.actions); - Notification notification2 = notificationCaptor.getAllValues().get(1); - assertEquals(1, notification2.actions.length); - } - - private Notification verifyNotificationPosted() { - ArgumentCaptor notificationCaptor = - ArgumentCaptor.forClass(Notification.class); - verify(mNotificationManager, timeout(TIMEOUT_MILLIS)) - .notify(eq("EXTERNAL_CALL"), eq(0), notificationCaptor.capture()); - return notificationCaptor.getValue(); - } - - private TestTelecomCall getTestCall(boolean canPull) { - TestTelecomCall testCall = TestTelecomCall.createInstance( - "1", - Uri.parse("tel:650-555-1212"), /* handle */ - TelecomManager.PRESENTATION_ALLOWED, /* handlePresentation */ - "Joe", /* callerDisplayName */ - TelecomManager.PRESENTATION_ALLOWED, /* callerDisplayNamePresentation */ - new PhoneAccountHandle(new ComponentName("test", "class"), - "handle"), /* accountHandle */ - canPull ? CallSdkCompat.Details.CAPABILITY_CAN_PULL_CALL : 0, /* capabilities */ - CallSdkCompat.Details.PROPERTY_IS_EXTERNAL_CALL, /* properties */ - null, /* disconnectCause */ - 0, /* connectTimeMillis */ - null, /* GatewayInfo */ - VideoProfile.STATE_AUDIO_ONLY, /* videoState */ - null, /* statusHints */ - null, /* extras */ - null /* intentExtras */); - return testCall; - } -} diff --git a/InCallUI/tests/src/com/android/incallui/InCallContactInteractionsTest.java b/InCallUI/tests/src/com/android/incallui/InCallContactInteractionsTest.java deleted file mode 100644 index 625cda448..000000000 --- a/InCallUI/tests/src/com/android/incallui/InCallContactInteractionsTest.java +++ /dev/null @@ -1,325 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.location.Address; -import android.test.AndroidTestCase; -import android.util.Pair; - -import com.android.incallui.InCallContactInteractions.BusinessContextInfo; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Calendar; -import java.util.List; -import java.util.Locale; - -/** - * Tests for InCallContactInteractions class methods for formatting info for display. - * - * NOTE: tests assume system settings are set to 12hr time format and US locale. This means that - * the output of InCallContactInteractions methods are compared against strings in 12hr time format - * and US locale address formatting unless otherwise specified. - */ -public class InCallContactInteractionsTest extends AndroidTestCase { - private InCallContactInteractions mInCallContactInteractions; - private static final float TEST_DISTANCE = (float) 1234.56; - - @Override - protected void setUp() { - mInCallContactInteractions = new InCallContactInteractions(mContext, true /* isBusiness */); - } - - public void testIsOpenNow_NowMatchesOpenTime() { - assertEquals(mContext.getString(R.string.open_now), - mInCallContactInteractions.constructHoursInfo( - getTestCalendarWithHour(8), - Arrays.asList( - Pair.create( - getTestCalendarWithHour(8), - getTestCalendarWithHour(20)))) - .heading); - } - - public void testIsOpenNow_ClosingAfterMidnight() { - assertEquals(mContext.getString(R.string.open_now), - mInCallContactInteractions.constructHoursInfo( - getTestCalendarWithHour(10), - Arrays.asList( - Pair.create( - getTestCalendarWithHour(8), - getTestCalendarWithHourAndDaysFromToday(1, 1)))) - .heading); - } - - public void testIsOpenNow_Open24Hours() { - assertEquals(mContext.getString(R.string.open_now), - mInCallContactInteractions.constructHoursInfo( - getTestCalendarWithHour(10), - Arrays.asList( - Pair.create( - getTestCalendarWithHour(8), - getTestCalendarWithHourAndDaysFromToday(8, 1)))) - .heading); - } - - public void testIsOpenNow_AfterMiddayBreak() { - assertEquals(mContext.getString(R.string.open_now), - mInCallContactInteractions.constructHoursInfo( - getTestCalendarWithHour(13), - Arrays.asList( - Pair.create( - getTestCalendarWithHour(8), - getTestCalendarWithHour(10)), - Pair.create( - getTestCalendarWithHour(12), - getTestCalendarWithHour(15)))) - .heading); - } - - public void testIsClosedNow_DuringMiddayBreak() { - assertEquals(mContext.getString(R.string.closed_now), - mInCallContactInteractions.constructHoursInfo( - getTestCalendarWithHour(11), - Arrays.asList( - Pair.create( - getTestCalendarWithHour(8), - getTestCalendarWithHour(10)), - Pair.create( - getTestCalendarWithHour(12), - getTestCalendarWithHour(15)))) - .heading); - } - - public void testIsClosedNow_BeforeOpen() { - assertEquals(mContext.getString(R.string.closed_now), - mInCallContactInteractions.constructHoursInfo( - getTestCalendarWithHour(6), - Arrays.asList( - Pair.create( - getTestCalendarWithHour(8), - getTestCalendarWithHour(20)))) - .heading); - } - - public void testIsClosedNow_NowMatchesClosedTime() { - assertEquals(mContext.getString(R.string.closed_now), - mInCallContactInteractions.constructHoursInfo( - getTestCalendarWithHour(20), - Arrays.asList( - Pair.create( - getTestCalendarWithHour(8), - getTestCalendarWithHour(20)))) - .heading); - } - - public void testIsClosedNow_AfterClosed() { - assertEquals(mContext.getString(R.string.closed_now), - mInCallContactInteractions.constructHoursInfo( - getTestCalendarWithHour(21), - Arrays.asList( - Pair.create( - getTestCalendarWithHour(8), - getTestCalendarWithHour(20)))) - .heading); - } - - public void testOpeningHours_SingleOpenRangeWhileOpen() { - assertEquals("8:00 AM - 8:00 PM", - mInCallContactInteractions.constructHoursInfo( - getTestCalendarWithHour(12), - Arrays.asList( - Pair.create( - getTestCalendarWithHour(8), - getTestCalendarWithHour(20)))) - .detail); - } - - public void testOpeningHours_TwoOpenRangesWhileOpen() { - assertEquals("8:00 AM - 10:00 AM, 12:00 PM - 3:00 PM", - mInCallContactInteractions.constructHoursInfo( - getTestCalendarWithHour(12), - Arrays.asList( - Pair.create( - getTestCalendarWithHour(8), - getTestCalendarWithHour(10)), - Pair.create( - getTestCalendarWithHour(12), - getTestCalendarWithHour(15)))) - .detail); - } - - public void testOpeningHours_AfterClosedNoTomorrow() { - assertEquals("Closed today at 8:00 PM", - mInCallContactInteractions.constructHoursInfo( - getTestCalendarWithHour(21), - Arrays.asList( - Pair.create( - getTestCalendarWithHour(8), - getTestCalendarWithHour(20)))) - .detail); - } - - public void testOpeningHours_NotOpenTodayOpenTomorrow() { - assertEquals("Opens tomorrow at 8:00 AM", - mInCallContactInteractions.constructHoursInfo( - getTestCalendarWithHour(21), - Arrays.asList( - Pair.create( - getTestCalendarWithHourAndDaysFromToday(8, 1), - getTestCalendarWithHourAndDaysFromToday(10, 1)))) - .detail); - } - - public void testMultipleOpenRanges_BeforeOpen() { - assertEquals("Opens today at 8:00 AM", - mInCallContactInteractions.constructHoursInfo( - getTestCalendarWithHour(7), - getMultipleOpeningHours()) - .detail); - } - - public void testMultipleOpenRanges_DuringFirstRange() { - assertEquals("Closes at 10:00 AM", - mInCallContactInteractions.constructHoursInfo( - getTestCalendarWithHour(9), - getMultipleOpeningHours()) - .detail); - } - - public void testMultipleOpenRanges_BeforeMiddleRange() { - assertEquals("Opens today at 12:00 PM", - mInCallContactInteractions.constructHoursInfo( - getTestCalendarWithHour(11), - getMultipleOpeningHours()) - .detail); - } - - public void testMultipleOpeningHours_DuringLastRange() { - assertEquals("Closes at 9:00 PM", - mInCallContactInteractions.constructHoursInfo( - getTestCalendarWithHour(19), - getMultipleOpeningHours()) - .detail); - } - - public void testMultipleOpeningHours_AfterClose() { - assertEquals("Opens tomorrow at 8:00 AM", - mInCallContactInteractions.constructHoursInfo( - getTestCalendarWithHour(22), - getMultipleOpeningHours()) - .detail); - } - - public void testNotOpenTodayOrTomorrow() { - assertEquals(null, - mInCallContactInteractions.constructHoursInfo( - getTestCalendarWithHour(21), - new ArrayList>())); - } - - public void testLocationInfo_ForUS() { - BusinessContextInfo info = - mInCallContactInteractions.constructLocationInfo( - Locale.US, - getAddressForTest(), - TEST_DISTANCE); - assertEquals("0.8 mi away", info.heading); - assertEquals("Test address, Test locality", info.detail); - } - - public void testLocationInfo_ForNotUS() { - BusinessContextInfo info = - mInCallContactInteractions.constructLocationInfo( - Locale.CANADA, - getAddressForTest(), - TEST_DISTANCE); - assertEquals("1.2 km away", info.heading); - assertEquals("Test address, Test locality", info.detail); - } - - public void testLocationInfo_NoLocality() { - Address address = getAddressForTest(); - address.setLocality(null); - BusinessContextInfo info = - mInCallContactInteractions.constructLocationInfo( - Locale.CANADA, - address, - TEST_DISTANCE); - assertEquals("1.2 km away", info.heading); - assertEquals("Test address", info.detail); - } - - public void testLocationInfo_NoAddress() { - BusinessContextInfo info = - mInCallContactInteractions.constructLocationInfo( - Locale.CANADA, - null, - TEST_DISTANCE); - assertEquals(null, info); - } - - public void testLocationInfo_NoDistance() { - BusinessContextInfo info = - mInCallContactInteractions.constructLocationInfo( - Locale.US, - getAddressForTest(), - DistanceHelper.DISTANCE_NOT_FOUND); - assertEquals(null, info.heading); - } - - private Address getAddressForTest() { - Address address = new Address(Locale.US); - address.setAddressLine(0, "Test address"); - address.setLocality("Test locality"); - return address; - } - - private Calendar getTestCalendarWithHour(int hour) { - Calendar calendar = Calendar.getInstance(); - calendar.set(Calendar.HOUR_OF_DAY, hour); - calendar.set(Calendar.MINUTE, 0); - calendar.set(Calendar.SECOND, 0); - calendar.set(Calendar.MILLISECOND, 0); - return calendar; - } - - private Calendar getTestCalendarWithHourAndDaysFromToday(int hour, int daysFromToday) { - Calendar calendar = getTestCalendarWithHour(hour); - calendar.add(Calendar.DATE, daysFromToday); - return calendar; - } - - private List> getMultipleOpeningHours() { - return Arrays.asList( - Pair.create( - getTestCalendarWithHour(8), - getTestCalendarWithHour(10)), - Pair.create( - getTestCalendarWithHour(12), - getTestCalendarWithHour(15)), - Pair.create( - getTestCalendarWithHour(17), - getTestCalendarWithHour(21)), - Pair.create( - getTestCalendarWithHourAndDaysFromToday(8, 1), - getTestCalendarWithHourAndDaysFromToday(10, 1)), - Pair.create( - getTestCalendarWithHourAndDaysFromToday(12, 1), - getTestCalendarWithHourAndDaysFromToday(8, 1))); - } -} diff --git a/InCallUI/tests/src/com/android/incallui/InCallPresenterTest.java b/InCallUI/tests/src/com/android/incallui/InCallPresenterTest.java deleted file mode 100644 index f0f08ab68..000000000 --- a/InCallUI/tests/src/com/android/incallui/InCallPresenterTest.java +++ /dev/null @@ -1,198 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.content.Context; -import android.content.Intent; -import android.telecom.PhoneAccountHandle; -import android.telephony.TelephonyManager; -import android.test.InstrumentationTestCase; -import android.test.suitebuilder.annotation.MediumTest; - -import com.android.incallui.InCallPresenter.InCallState; - -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; - -@MediumTest -public class InCallPresenterTest extends InstrumentationTestCase { - private MockCallListWrapper mCallList; - @Mock private InCallActivity mInCallActivity; - @Mock private AudioModeProvider mAudioModeProvider; - @Mock private StatusBarNotifier mStatusBarNotifier; - @Mock private ExternalCallNotifier mExternalCallNotifier; - @Mock private ContactInfoCache mContactInfoCache; - @Mock private ProximitySensor mProximitySensor; - - InCallPresenter mInCallPresenter; - @Mock private Context mContext; - @Mock private TelephonyManager mTelephonyManager; - - @Override - protected void setUp() throws Exception { - super.setUp(); - System.setProperty("dexmaker.dexcache", - getInstrumentation().getTargetContext().getCacheDir().getPath()); - MockitoAnnotations.initMocks(this); - mCallList = new MockCallListWrapper(); - - when(mContext.getSystemService(Context.TELEPHONY_SERVICE)).thenReturn(mTelephonyManager); - - mInCallPresenter = InCallPresenter.getInstance(); - mInCallPresenter.setUp(mContext, mCallList.getCallList(), new ExternalCallList(), - mAudioModeProvider, mStatusBarNotifier, mExternalCallNotifier, mContactInfoCache, - mProximitySensor); - } - - @Override - protected void tearDown() throws Exception { - // The tear down method needs to run in the main thread since there is an explicit check - // inside TelecomAdapter.getInstance(). - getInstrumentation().runOnMainSync(new Runnable() { - @Override - public void run() { - mInCallPresenter.unsetActivity(mInCallActivity); - mInCallPresenter.tearDown(); - InCallPresenter.setInstance(null); - } - }); - } - - public void testOnActivitySet_finishesActivityWhenNoCalls() { - mInCallPresenter.setActivity(mInCallActivity); - - verify(mInCallActivity).finish(); - } - - public void testOnCallListChange_sendsNotificationWhenInCall() { - mCallList.setHasCall(Call.State.INCOMING, true); - - mInCallPresenter.onCallListChange(mCallList.getCallList()); - - verify(mStatusBarNotifier).updateNotification(InCallState.INCOMING, - mCallList.getCallList()); - verifyInCallActivityNotStarted(); - } - - /** - * This behavior is required to ensure that the screen is turned on again by the restarting - * activity. - */ - public void testOnCallListChange_handlesCallWaitingWhenScreenOffShouldRestartActivity() { - mCallList.setHasCall(Call.State.ACTIVE, true); - - mInCallPresenter.onCallListChange(mCallList.getCallList()); - mInCallPresenter.setActivity(mInCallActivity); - - // Pretend that there is a call waiting and the screen is off - when(mInCallActivity.isDestroyed()).thenReturn(false); - when(mInCallActivity.isFinishing()).thenReturn(false); - when(mProximitySensor.isScreenReallyOff()).thenReturn(true); - mCallList.setHasCall(Call.State.INCOMING, true); - - mInCallPresenter.onCallListChange(mCallList.getCallList()); - verify(mInCallActivity).finish(); - } - - /** - * Verifies that the PENDING_OUTGOING -> IN_CALL transition brings up InCallActivity so - * that it can display an error dialog. - */ - public void testOnCallListChange_pendingOutgoingToInCallTransitionShowsUiForErrorDialog() { - mCallList.setHasCall(Call.State.CONNECTING, true); - - mInCallPresenter.onCallListChange(mCallList.getCallList()); - - mCallList.setHasCall(Call.State.CONNECTING, false); - mCallList.setHasCall(Call.State.ACTIVE, true); - - mInCallPresenter.onCallListChange(mCallList.getCallList()); - - verify(mContext).startActivity(InCallPresenter.getInstance().getInCallIntent(false, false)); - verifyIncomingCallNotificationNotSent(); - } - - /** - * Verifies that if there is a call in the SELECT_PHONE_ACCOUNT state, InCallActivity is displayed - * to display the account picker. - */ - public void testOnCallListChange_noAccountProvidedForCallShowsUiForAccountPicker() { - mCallList.setHasCall(Call.State.SELECT_PHONE_ACCOUNT, true); - mInCallPresenter.onCallListChange(mCallList.getCallList()); - - verify(mContext).startActivity(InCallPresenter.getInstance().getInCallIntent(false, false)); - verifyIncomingCallNotificationNotSent(); - } - - /** - * Verifies that for an expected call state change (e.g. NO_CALLS -> PENDING_OUTGOING), - * InCallActivity is not displayed. - */ - public void testOnCallListChange_noCallsToPendingOutgoingDoesNotShowUi() { - mCallList.setHasCall(Call.State.CONNECTING, true); - mInCallPresenter.onCallListChange(mCallList.getCallList()); - - verifyInCallActivityNotStarted(); - verifyIncomingCallNotificationNotSent(); - } - - public void testOnCallListChange_LastCallDisconnectedNoCallsLeftFinishesUi() { - mCallList.setHasCall(Call.State.DISCONNECTED, true); - mInCallPresenter.onCallListChange(mCallList.getCallList()); - - mInCallPresenter.setActivity(mInCallActivity); - - verify(mInCallActivity, never()).finish(); - - // Last remaining disconnected call is removed from CallList, activity should shut down. - mCallList.setHasCall(Call.State.DISCONNECTED, false); - mInCallPresenter.onCallListChange(mCallList.getCallList()); - verify(mInCallActivity).finish(); - } - - - //TODO - public void testCircularReveal_startsCircularRevealForOutgoingCalls() { - - } - - public void testCircularReveal_waitTillCircularRevealSentBeforeShowingCallCard() { - } - - public void testHangupOngoingCall_disconnectsCallCorrectly() { - } - - public void testAnswerIncomingCall() { - } - - public void testDeclineIncomingCall() { - } - - private void verifyInCallActivityNotStarted() { - verify(mContext, never()).startActivity(Mockito.any(Intent.class)); - } - - private void verifyIncomingCallNotificationNotSent() { - verify(mStatusBarNotifier, never()).updateNotification(Mockito.any(InCallState.class), - Mockito.any(CallList.class)); - } -} diff --git a/InCallUI/tests/src/com/android/incallui/LatencyReportTest.java b/InCallUI/tests/src/com/android/incallui/LatencyReportTest.java deleted file mode 100644 index 9d8a5131b..000000000 --- a/InCallUI/tests/src/com/android/incallui/LatencyReportTest.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import static com.android.incallui.LatencyReport.INVALID_TIME; - -import android.os.Bundle; -import android.telecom.Connection; -import android.test.AndroidTestCase; -import android.test.suitebuilder.annotation.SmallTest; - -import java.util.ArrayList; -import java.util.Arrays; - -public class LatencyReportTest extends AndroidTestCase { - public void testEmptyInit() { - LatencyReport report = new LatencyReport(); - assertEquals(INVALID_TIME, report.getCreatedTimeMillis()); - assertEquals(INVALID_TIME, report.getTelecomRoutingStartTimeMillis()); - assertEquals(INVALID_TIME, report.getTelecomRoutingEndTimeMillis()); - assertTrue(report.getCallAddedTimeMillis() > 0); - } - - public void testCallBlocking() { - LatencyReport report = new LatencyReport(); - assertEquals(INVALID_TIME, report.getCallBlockingTimeMillis()); - report.onCallBlockingDone(); - assertTrue(report.getCallBlockingTimeMillis() > 0); - } - - public void testNotificationShown() { - LatencyReport report = new LatencyReport(); - assertEquals(INVALID_TIME, report.getCallNotificationTimeMillis()); - report.onNotificationShown(); - assertTrue(report.getCallNotificationTimeMillis() > 0); - } - - public void testInCallUiShown() { - LatencyReport report = new LatencyReport(); - assertEquals(INVALID_TIME, report.getInCallUiShownTimeMillis()); - report.onInCallUiShown(false); - assertTrue(report.getInCallUiShownTimeMillis() > 0); - assertFalse(report.getDidDisplayHeadsUpNotification()); - } -} diff --git a/InCallUI/tests/src/com/android/incallui/MockCallListWrapper.java b/InCallUI/tests/src/com/android/incallui/MockCallListWrapper.java deleted file mode 100644 index 369c4303f..000000000 --- a/InCallUI/tests/src/com/android/incallui/MockCallListWrapper.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import static org.mockito.Mockito.anyInt; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.when; - -import android.telecom.PhoneAccountHandle; - -import org.mockito.Mockito; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; - -import java.util.HashSet; - -/** - * Provides an instance of a mock CallList, and provides utility methods to put the CallList into - * various states (e.g. incoming call, active call, call waiting). - */ -public class MockCallListWrapper { - private CallList mCallList; - private HashSet mCallSet = new HashSet<>(); - - public MockCallListWrapper() { - mCallList = Mockito.mock(CallList.class); - mCallList = spy(new CallList()); - when(mCallList.getFirstCallWithState(anyInt())).thenAnswer(new Answer() { - @Override - public Call answer(InvocationOnMock i) throws Throwable { - Object[] args = i.getArguments(); - final int state = (int) args[0]; - if (mCallSet.contains(state)) { - return getMockCall(state); - } else { - return null; - } - } - }); - } - - public CallList getCallList() { - return mCallList; - } - - public void setHasCall(int state, boolean hasCall) { - if (hasCall) { - mCallSet.add(state); - } else { - mCallSet.remove(state); - } - } - - private static Call getMockCall(int state) { - return getMockCall(state, state != Call.State.SELECT_PHONE_ACCOUNT); - } - - private static Call getMockCall(int state, boolean hasAccountHandle) { - final Call call = Mockito.mock(Call.class); - when(call.getState()).thenReturn(Integer.valueOf(state)); - if (hasAccountHandle) { - when(call.getAccountHandle()).thenReturn(new PhoneAccountHandle(null, null)); - } - return call; - } -} diff --git a/InCallUI/tests/src/com/android/incallui/ProximitySensorTest.java b/InCallUI/tests/src/com/android/incallui/ProximitySensorTest.java deleted file mode 100644 index 1c8f34721..000000000 --- a/InCallUI/tests/src/com/android/incallui/ProximitySensorTest.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import static org.mockito.Mockito.anyBoolean; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -import android.test.InstrumentationTestCase; -import android.test.suitebuilder.annotation.MediumTest; - -import com.android.incallui.InCallPresenter.InCallState; - -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; - -@MediumTest -public class ProximitySensorTest extends InstrumentationTestCase { - @Mock private AccelerometerListener mAccelerometerListener; - private MockCallListWrapper mCallList; - - @Override - protected void setUp() throws Exception { - super.setUp(); - System.setProperty("dexmaker.dexcache", - getInstrumentation().getTargetContext().getCacheDir().getPath()); - MockitoAnnotations.initMocks(this); - mCallList = new MockCallListWrapper(); - } - - public void testAccelerometerBehaviorOnDisplayChange() { - final ProximitySensor proximitySensor = - new ProximitySensor( - getInstrumentation().getContext(), - new AudioModeProvider(), - mAccelerometerListener); - verify(mAccelerometerListener, never()).enable(anyBoolean()); - proximitySensor.onStateChange(null, InCallState.OUTGOING, mCallList.getCallList()); - verify(mAccelerometerListener).enable(true); - verify(mAccelerometerListener, never()).enable(false); - - proximitySensor.onDisplayStateChanged(false); - verify(mAccelerometerListener).enable(true); - verify(mAccelerometerListener).enable(false); - - proximitySensor.onDisplayStateChanged(true); - verify(mAccelerometerListener, times(2)).enable(true); - verify(mAccelerometerListener).enable(false); - } -} diff --git a/InCallUI/tests/src/com/android/incallui/StatusBarNotifierTest.java b/InCallUI/tests/src/com/android/incallui/StatusBarNotifierTest.java deleted file mode 100644 index 4c55ddcc0..000000000 --- a/InCallUI/tests/src/com/android/incallui/StatusBarNotifierTest.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import android.test.AndroidTestCase; -import android.test.suitebuilder.annotation.MediumTest; - -import com.android.contacts.common.preference.ContactsPreferences; -import com.android.incallui.ContactInfoCache.ContactCacheEntry; - -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; - -@MediumTest -public class StatusBarNotifierTest extends AndroidTestCase { - - private static final String NAME_PRIMARY = "Full Name"; - private static final String NAME_ALTERNATIVE = "Name, Full"; - private static final String LOCATION = "US"; - private static final String NUMBER = "8006459001"; - - @Mock private Call mCall; - @Mock private ContactsPreferences mContactsPreferences; - private ContactCacheEntry mUnlockedContactInfo; - private ContactCacheEntry mLockedContactInfo; - - @Override - public void setUp() throws Exception { - super.setUp(); - MockitoAnnotations.initMocks(this); - - Mockito.when(mContactsPreferences.getDisplayOrder()) - .thenReturn(ContactsPreferences.DISPLAY_ORDER_PRIMARY); - - // Unlocked all contact info is available - mUnlockedContactInfo = new ContactCacheEntry(); - mUnlockedContactInfo.namePrimary = NAME_PRIMARY; - mUnlockedContactInfo.nameAlternative = NAME_ALTERNATIVE; - mUnlockedContactInfo.location = LOCATION; - mUnlockedContactInfo.number = NUMBER; - - // Locked only number and location are available - mLockedContactInfo = new ContactCacheEntry(); - mLockedContactInfo .location = LOCATION; - mLockedContactInfo .number = NUMBER; - } - - @Override - public void tearDown() throws Exception { - super.tearDown(); - ContactsPreferencesFactory.setTestInstance(null); - } - - public void testGetContentTitle_ConferenceCall() { - ContactsPreferencesFactory.setTestInstance(null); - StatusBarNotifier statusBarNotifier = new StatusBarNotifier(mContext, null); - - Mockito.when(mCall.isConferenceCall()).thenReturn(true); - Mockito.when(mCall.hasProperty(Mockito.anyInt())).thenReturn(false); - - assertEquals(mContext.getResources().getString(R.string.card_title_conf_call), - statusBarNotifier.getContentTitle(null, mCall)); - } - - public void testGetContentTitle_Unlocked() { - ContactsPreferencesFactory.setTestInstance(mContactsPreferences); - StatusBarNotifier statusBarNotifier = new StatusBarNotifier(mContext, null); - assertEquals(NAME_PRIMARY, statusBarNotifier.getContentTitle(mUnlockedContactInfo, mCall)); - } - - public void testGetContentTitle_Locked() { - ContactsPreferencesFactory.setTestInstance(null); - StatusBarNotifier statusBarNotifier = new StatusBarNotifier(mContext, null); - assertEquals(NUMBER, statusBarNotifier.getContentTitle(mLockedContactInfo, mCall)); - } - - public void testGetContentTitle_EmptyPreferredName() { - ContactCacheEntry contactCacheEntry = new ContactCacheEntry(); - contactCacheEntry.number = NUMBER; - StatusBarNotifier statusBarNotifier = new StatusBarNotifier(mContext, null); - assertEquals(NUMBER, statusBarNotifier.getContentTitle(contactCacheEntry, mCall)); - } -} diff --git a/InCallUI/tests/src/com/android/incallui/TestTelecomCall.java b/InCallUI/tests/src/com/android/incallui/TestTelecomCall.java deleted file mode 100644 index 48ac6e18f..000000000 --- a/InCallUI/tests/src/com/android/incallui/TestTelecomCall.java +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui; - -import com.google.common.base.Preconditions; - -import android.content.Context; -import android.net.Uri; -import android.os.Bundle; -import android.support.annotation.Nullable; -import android.telecom.DisconnectCause; -import android.telecom.GatewayInfo; -import android.telecom.PhoneAccountHandle; -import android.telecom.StatusHints; - -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; - -/** - * Wrapper class which uses reflection to create instances of {@link android.telecom.Call} for use - * with unit testing. Since {@link android.telecom.Call} is final, it cannot be mocked using - * mockito, and since all setter methods are hidden, it is necessary to use reflection. In the - * future, it would be desirable to replace this if a different mocking solution is used. - */ -public class TestTelecomCall { - - private android.telecom.Call mCall; - - public static @Nullable TestTelecomCall createInstance(String callId, - Uri handle, - int handlePresentation, - String callerDisplayName, - int callerDisplayNamePresentation, - PhoneAccountHandle accountHandle, - int capabilities, - int properties, - DisconnectCause disconnectCause, - long connectTimeMillis, - GatewayInfo gatewayInfo, - int videoState, - StatusHints statusHints, - Bundle extras, - Bundle intentExtras) { - - try { - // Phone and InCall adapter are @hide, so we cannot refer to them directly. - Class phoneClass = Class.forName("android.telecom.Phone"); - Class incallAdapterClass = Class.forName("android.telecom.InCallAdapter"); - Class callClass = android.telecom.Call.class; - Constructor cons = callClass - .getDeclaredConstructor(phoneClass, String.class, incallAdapterClass); - cons.setAccessible(true); - - android.telecom.Call call = (android.telecom.Call) cons.newInstance(null, callId, null); - - // Create an instance of the call details. - Class callDetailsClass = android.telecom.Call.Details.class; - Constructor detailsCons = callDetailsClass.getDeclaredConstructor( - String.class, /* telecomCallId */ - Uri.class, /* handle */ - int.class, /* handlePresentation */ - String.class, /* callerDisplayName */ - int.class, /* callerDisplayNamePresentation */ - PhoneAccountHandle.class, /* accountHandle */ - int.class, /* capabilities */ - int.class, /* properties */ - DisconnectCause.class, /* disconnectCause */ - long.class, /* connectTimeMillis */ - GatewayInfo.class, /* gatewayInfo */ - int.class, /* videoState */ - StatusHints.class, /* statusHints */ - Bundle.class, /* extras */ - Bundle.class /* intentExtras */); - detailsCons.setAccessible(true); - - android.telecom.Call.Details details = (android.telecom.Call.Details) - detailsCons.newInstance(callId, handle, handlePresentation, callerDisplayName, - callerDisplayNamePresentation, accountHandle, capabilities, properties, - disconnectCause, connectTimeMillis, gatewayInfo, videoState, - statusHints, - extras, intentExtras); - - // Finally, set this as the details of the call. - Field detailsField = call.getClass().getDeclaredField("mDetails"); - detailsField.setAccessible(true); - detailsField.set(call, details); - - return new TestTelecomCall(call); - } catch (NoSuchMethodException nsm) { - return null; - } catch (ClassNotFoundException cnf) { - return null; - } catch (IllegalAccessException e) { - return null; - } catch (InstantiationException e) { - return null; - } catch (InvocationTargetException e) { - return null; - } catch (NoSuchFieldException e) { - return null; - } - } - - private TestTelecomCall(android.telecom.Call call) { - mCall = call; - } - - public android.telecom.Call getCall() { - return mCall; - } - - public void forceDetailsUpdate() { - Preconditions.checkNotNull(mCall); - - try { - Method method = mCall.getClass().getDeclaredMethod("fireDetailsChanged", - android.telecom.Call.Details.class); - method.setAccessible(true); - method.invoke(mCall, mCall.getDetails()); - } catch (NoSuchMethodException e) { - Log.e(this, "forceDetailsUpdate", e); - } catch (InvocationTargetException e) { - Log.e(this, "forceDetailsUpdate", e); - } catch (IllegalAccessException e) { - Log.e(this, "forceDetailsUpdate", e); - } - } - - public void setCapabilities(int capabilities) { - Preconditions.checkNotNull(mCall); - try { - Field field = mCall.getDetails().getClass().getDeclaredField("mCallCapabilities"); - field.setAccessible(true); - field.set(mCall.getDetails(), capabilities); - } catch (IllegalAccessException e) { - Log.e(this, "setProperties", e); - } catch (NoSuchFieldException e) { - Log.e(this, "setProperties", e); - } - } - - public void setCall(android.telecom.Call call) { - mCall = call; - } -} diff --git a/InCallUI/tests/src/com/android/incallui/async/SingleProdThreadExecutor.java b/InCallUI/tests/src/com/android/incallui/async/SingleProdThreadExecutor.java deleted file mode 100644 index 5717c9478..000000000 --- a/InCallUI/tests/src/com/android/incallui/async/SingleProdThreadExecutor.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui.async; - -import java.util.concurrent.Executors; - -import javax.annotation.concurrent.ThreadSafe; - -/** - * {@link PausableExecutor} for use in tests. It is intended to be used between one test thread - * and one prod thread. See {@link com.android.incallui.ringtone.InCallTonePlayerTest} for example - * usage. - */ -@ThreadSafe -public final class SingleProdThreadExecutor implements PausableExecutor { - - private int mMilestonesReached; - private int mMilestonesAcked; - private boolean mHasAckedAllMilestones; - - @Override - public synchronized void milestone() { - ++mMilestonesReached; - notify(); - while (!mHasAckedAllMilestones && mMilestonesReached > mMilestonesAcked) { - try { - wait(); - } catch (InterruptedException e) {} - } - } - - @Override - public synchronized void ackMilestoneForTesting() { - ++mMilestonesAcked; - notify(); - } - - @Override - public synchronized void ackAllMilestonesForTesting() { - mHasAckedAllMilestones = true; - notify(); - } - - @Override - public synchronized void awaitMilestoneForTesting() throws InterruptedException { - while (!mHasAckedAllMilestones && mMilestonesReached <= mMilestonesAcked) { - wait(); - } - } - - @Override - public synchronized void execute(Runnable command) { - Executors.newSingleThreadExecutor().execute(command); - } -} diff --git a/InCallUI/tests/src/com/android/incallui/ringtone/DialerRingtoneManagerTest.java b/InCallUI/tests/src/com/android/incallui/ringtone/DialerRingtoneManagerTest.java deleted file mode 100644 index 01db20272..000000000 --- a/InCallUI/tests/src/com/android/incallui/ringtone/DialerRingtoneManagerTest.java +++ /dev/null @@ -1,219 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.incallui.ringtone; - -import android.media.RingtoneManager; -import android.net.Uri; -import android.test.AndroidTestCase; -import android.test.suitebuilder.annotation.SmallTest; - -import com.android.contacts.common.compat.CompatUtils; -import com.android.incallui.Call; -import com.android.incallui.Call.State; -import com.android.incallui.CallList; - -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; - -@SmallTest -public class DialerRingtoneManagerTest extends AndroidTestCase { - - private static final Uri RINGTONE_URI = RingtoneManager - .getDefaultUri(RingtoneManager.TYPE_RINGTONE); - - @Mock private InCallTonePlayer mInCallTonePlayer; - @Mock private CallList mCallList; - @Mock private Call mCall; - private DialerRingtoneManager mRingtoneManagerEnabled; - private DialerRingtoneManager mRingtoneManagerDisabled; - - @Override - public void setUp() throws Exception { - super.setUp(); - MockitoAnnotations.initMocks(this); - mRingtoneManagerEnabled = new DialerRingtoneManager(mInCallTonePlayer, mCallList); - mRingtoneManagerEnabled.setDialerRingingEnabledForTesting(true); - mRingtoneManagerDisabled = new DialerRingtoneManager(mInCallTonePlayer, mCallList); - mRingtoneManagerDisabled.setDialerRingingEnabledForTesting(false); - } - - public void testNullInCallTonePlayer() { - try { - new DialerRingtoneManager(null, mCallList); - fail(); - } catch (NullPointerException e) {} - } - - public void testNullCallList() { - try { - new DialerRingtoneManager(mInCallTonePlayer, null); - fail(); - } catch (NullPointerException e) {} - } - - public void testShouldPlayRingtone_M() { - if (CompatUtils.isNCompatible()) { - return; - } - assertFalse(mRingtoneManagerEnabled.shouldPlayRingtone(0, RINGTONE_URI)); - } - - public void testShouldPlayRingtone_N_NullUri() { - if (!CompatUtils.isNCompatible()) { - return; - } - assertFalse(mRingtoneManagerEnabled.shouldPlayRingtone(State.INCOMING, null)); - } - - public void testShouldPlayRingtone_N_Disabled() { - if (!CompatUtils.isNCompatible()) { - return; - } - assertFalse(mRingtoneManagerDisabled.shouldPlayRingtone(State.INCOMING, RINGTONE_URI)); - } - - public void testShouldPlayRingtone_N_NotIncoming() { - if (!CompatUtils.isNCompatible()) { - return; - } - assertFalse(mRingtoneManagerEnabled.shouldPlayRingtone(State.ACTIVE, RINGTONE_URI)); - } - - // Specific case for call waiting since that needs its own sound - public void testShouldPlayRingtone_N_CallWaitingByState() { - if (!CompatUtils.isNCompatible()) { - return; - } - assertFalse(mRingtoneManagerEnabled.shouldPlayRingtone(State.CALL_WAITING, RINGTONE_URI)); - } - - public void testShouldPlayRingtone_N_CallWaitingByActiveCall() { - if (!CompatUtils.isNCompatible()) { - return; - } - Mockito.when(mCallList.getActiveCall()).thenReturn(mCall); - assertFalse(mRingtoneManagerEnabled.shouldPlayRingtone(State.INCOMING, RINGTONE_URI)); - } - - public void testShouldPlayRingtone_N() { - if (!CompatUtils.isNCompatible()) { - return; - } - assertTrue(mRingtoneManagerEnabled.shouldPlayRingtone(State.INCOMING, RINGTONE_URI)); - } - - public void testShouldPlayCallWaitingTone_M() { - if (CompatUtils.isNCompatible()) { - return; - } - assertFalse(mRingtoneManagerEnabled.shouldPlayCallWaitingTone(0)); - } - - public void testShouldPlayCallWaitingTone_N_Disabled() { - if (!CompatUtils.isNCompatible()) { - return; - } - assertFalse(mRingtoneManagerDisabled.shouldPlayCallWaitingTone(State.CALL_WAITING)); - } - - public void testShouldPlayCallWaitingTone_N_NotCallWaiting() { - if (!CompatUtils.isNCompatible()) { - return; - } - assertFalse(mRingtoneManagerEnabled.shouldPlayCallWaitingTone(State.ACTIVE)); - } - - // Specific case for incoming since it plays its own sound - public void testShouldPlayCallWaitingTone_N_Incoming() { - if (!CompatUtils.isNCompatible()) { - return; - } - assertFalse(mRingtoneManagerEnabled.shouldPlayCallWaitingTone(State.INCOMING)); - } - - public void testShouldPlayCallWaitingTone_N_AlreadyPlaying() { - if (!CompatUtils.isNCompatible()) { - return; - } - Mockito.when(mInCallTonePlayer.isPlayingTone()).thenReturn(true); - assertFalse(mRingtoneManagerEnabled.shouldPlayCallWaitingTone(State.CALL_WAITING)); - } - - public void testShouldPlayCallWaitingTone_N_ByState() { - if (!CompatUtils.isNCompatible()) { - return; - } - assertTrue(mRingtoneManagerEnabled.shouldPlayCallWaitingTone(State.CALL_WAITING)); - } - - public void testShouldPlayCallWaitingTone_N_ByActiveCall() { - if (!CompatUtils.isNCompatible()) { - return; - } - Mockito.when(mCallList.getActiveCall()).thenReturn(mCall); - assertTrue(mRingtoneManagerEnabled.shouldPlayCallWaitingTone(State.INCOMING)); - } - - public void testPlayCallWaitingTone_M() { - if (CompatUtils.isNCompatible()) { - return; - } - mRingtoneManagerEnabled.playCallWaitingTone(); - Mockito.verify(mInCallTonePlayer, Mockito.never()).play(Mockito.anyInt()); - } - - public void testPlayCallWaitingTone_N_NotEnabled() { - if (!CompatUtils.isNCompatible()) { - return; - } - mRingtoneManagerDisabled.playCallWaitingTone(); - Mockito.verify(mInCallTonePlayer, Mockito.never()).play(Mockito.anyInt()); - } - - public void testPlayCallWaitingTone_N() { - if (!CompatUtils.isNCompatible()) { - return; - } - mRingtoneManagerEnabled.playCallWaitingTone(); - Mockito.verify(mInCallTonePlayer).play(Mockito.anyInt()); - } - - public void testStopCallWaitingTone_M() { - if (CompatUtils.isNCompatible()) { - return; - } - mRingtoneManagerEnabled.stopCallWaitingTone(); - Mockito.verify(mInCallTonePlayer, Mockito.never()).stop(); - } - - public void testStopCallWaitingTone_N_NotEnabled() { - if (!CompatUtils.isNCompatible()) { - return; - } - mRingtoneManagerDisabled.stopCallWaitingTone(); - Mockito.verify(mInCallTonePlayer, Mockito.never()).stop(); - } - - public void testStopCallWaitingTone_N() { - if (!CompatUtils.isNCompatible()) { - return; - } - mRingtoneManagerEnabled.stopCallWaitingTone(); - Mockito.verify(mInCallTonePlayer).stop(); - } -} diff --git a/InCallUI/tests/src/com/android/incallui/ringtone/InCallTonePlayerTest.java b/InCallUI/tests/src/com/android/incallui/ringtone/InCallTonePlayerTest.java deleted file mode 100644 index bde5c50e4..000000000 --- a/InCallUI/tests/src/com/android/incallui/ringtone/InCallTonePlayerTest.java +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.incallui.ringtone; - -import android.media.AudioManager; -import android.media.ToneGenerator; -import android.test.AndroidTestCase; -import android.test.suitebuilder.annotation.SmallTest; - -import com.android.incallui.async.PausableExecutor; -import com.android.incallui.async.SingleProdThreadExecutor; - -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; - -@SmallTest -public class InCallTonePlayerTest extends AndroidTestCase { - - @Mock private ToneGeneratorFactory mToneGeneratorFactory; - @Mock private ToneGenerator mToneGenerator; - private InCallTonePlayer mInCallTonePlayer; - - /* - * InCallTonePlayer milestones: - * 1) After tone starts playing - * 2) After tone finishes waiting (could have timed out) - * 3) After cleaning up state to allow new tone to play - */ - private PausableExecutor mExecutor; - - @Override - public void setUp() throws Exception { - super.setUp(); - MockitoAnnotations.initMocks(this); - Mockito.when(mToneGeneratorFactory.newInCallToneGenerator(Mockito.anyInt(), - Mockito.anyInt())).thenReturn(mToneGenerator); - mExecutor = new SingleProdThreadExecutor(); - mInCallTonePlayer = new InCallTonePlayer(mToneGeneratorFactory, mExecutor); - } - - @Override - public void tearDown() throws Exception { - super.tearDown(); - // Stop any playing so the InCallTonePlayer isn't stuck waiting for the tone to complete - mInCallTonePlayer.stop(); - // Ack all milestones to ensure that the prod thread doesn't block forever - mExecutor.ackAllMilestonesForTesting(); - } - - public void testIsPlayingTone_False() { - assertFalse(mInCallTonePlayer.isPlayingTone()); - } - - public void testIsPlayingTone_True() throws InterruptedException { - mInCallTonePlayer.play(InCallTonePlayer.TONE_CALL_WAITING); - mExecutor.awaitMilestoneForTesting(); - - assertTrue(mInCallTonePlayer.isPlayingTone()); - } - - public void testPlay_InvalidTone() { - try { - mInCallTonePlayer.play(Integer.MIN_VALUE); - fail(); - } catch (IllegalArgumentException e) {} - } - - public void testPlay_CurrentlyPlaying() throws InterruptedException { - mInCallTonePlayer.play(InCallTonePlayer.TONE_CALL_WAITING); - mExecutor.awaitMilestoneForTesting(); - try { - mInCallTonePlayer.play(InCallTonePlayer.TONE_CALL_WAITING); - fail(); - } catch (IllegalStateException e) {} - } - - public void testPlay_VoiceCallStream() throws InterruptedException { - mInCallTonePlayer.play(InCallTonePlayer.TONE_CALL_WAITING); - mExecutor.awaitMilestoneForTesting(); - Mockito.verify(mToneGeneratorFactory).newInCallToneGenerator(AudioManager.STREAM_VOICE_CALL, - InCallTonePlayer.VOLUME_RELATIVE_HIGH_PRIORITY); - } - - public void testPlay_Single() throws InterruptedException { - mInCallTonePlayer.play(InCallTonePlayer.TONE_CALL_WAITING); - mExecutor.awaitMilestoneForTesting(); - mExecutor.ackMilestoneForTesting(); - mInCallTonePlayer.stop(); - mExecutor.ackMilestoneForTesting(); - mExecutor.awaitMilestoneForTesting(); - mExecutor.ackMilestoneForTesting(); - - Mockito.verify(mToneGenerator).startTone(ToneGenerator.TONE_SUP_CALL_WAITING); - } - - public void testPlay_Consecutive() throws InterruptedException { - mInCallTonePlayer.play(InCallTonePlayer.TONE_CALL_WAITING); - mExecutor.awaitMilestoneForTesting(); - mExecutor.ackMilestoneForTesting(); - // Prevent waiting forever - mInCallTonePlayer.stop(); - mExecutor.ackMilestoneForTesting(); - mExecutor.awaitMilestoneForTesting(); - mExecutor.ackMilestoneForTesting(); - - mInCallTonePlayer.play(InCallTonePlayer.TONE_CALL_WAITING); - mExecutor.awaitMilestoneForTesting(); - mExecutor.ackMilestoneForTesting(); - mInCallTonePlayer.stop(); - mExecutor.ackMilestoneForTesting(); - mExecutor.awaitMilestoneForTesting(); - mExecutor.ackMilestoneForTesting(); - - Mockito.verify(mToneGenerator, Mockito.times(2)) - .startTone(ToneGenerator.TONE_SUP_CALL_WAITING); - } - - public void testStop_NotPlaying() { - // No crash - mInCallTonePlayer.stop(); - } - - public void testStop() throws InterruptedException { - mInCallTonePlayer.play(InCallTonePlayer.TONE_CALL_WAITING); - mExecutor.awaitMilestoneForTesting(); - - mInCallTonePlayer.stop(); - mExecutor.ackMilestoneForTesting(); - mExecutor.awaitMilestoneForTesting(); - - assertFalse(mInCallTonePlayer.isPlayingTone()); - } -} diff --git a/InCallUI/tests/src/com/android/incallui/spam/SpamCallListListenerTest.java b/InCallUI/tests/src/com/android/incallui/spam/SpamCallListListenerTest.java deleted file mode 100644 index fb0b460b6..000000000 --- a/InCallUI/tests/src/com/android/incallui/spam/SpamCallListListenerTest.java +++ /dev/null @@ -1,206 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - - -package com.android.incallui.spam; - -import static org.mockito.Matchers.anyInt; -import static org.mockito.Mockito.doCallRealMethod; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.eq; -import static org.mockito.Mockito.any; - -import android.content.ContentValues; -import android.content.Context; -import android.provider.CallLog; -import android.telecom.DisconnectCause; -import android.test.InstrumentationTestCase; - -import com.android.contacts.common.test.mocks.ContactsMockContext; -import com.android.contacts.common.test.mocks.MockContentProvider; -import com.android.dialer.calllog.CallLogAsyncTaskUtil; -import com.android.dialer.util.AsyncTaskExecutors; -import com.android.dialer.util.FakeAsyncTaskExecutor; -import com.android.dialer.util.TelecomUtil; -import com.android.incallui.Call; -import com.android.incallui.Call.CallHistoryStatus; -import com.android.incallui.Call.LogState; - -public class SpamCallListListenerTest extends InstrumentationTestCase { - private static final String NUMBER = "+18005657862"; - private static final int DURATION = 100; - - private TestSpamCallListListener mListener; - private FakeAsyncTaskExecutor mFakeAsyncTaskExecutor; - private ContactsMockContext mContext; - - @Override - public void setUp() throws Exception { - super.setUp(); - mContext = new ContactsMockContext(getInstrumentation().getContext(), CallLog.AUTHORITY); - mListener = new TestSpamCallListListener(mContext); - mFakeAsyncTaskExecutor = new FakeAsyncTaskExecutor(getInstrumentation()); - AsyncTaskExecutors.setFactoryForTest(mFakeAsyncTaskExecutor.getFactory()); - } - - @Override - public void tearDown() throws Exception { - AsyncTaskExecutors.setFactoryForTest(null); - CallLogAsyncTaskUtil.resetForTest(); - super.tearDown(); - } - - public void testOutgoingCall() { - Call call = getMockCall(NUMBER, false, 0, Call.CALL_HISTORY_STATUS_NOT_PRESENT, - LogState.LOOKUP_UNKNOWN, DisconnectCause.REMOTE); - mListener.onDisconnect(call); - assertFalse(mListener.mShowNotificationCalled); - } - - public void testIncomingCall_UnknownNumber() { - Call call = getMockCall(null, true, DURATION, Call.CALL_HISTORY_STATUS_NOT_PRESENT, - LogState.LOOKUP_UNKNOWN, DisconnectCause.REMOTE); - mListener.onDisconnect(call); - assertFalse(mListener.mShowNotificationCalled); - } - - public void testIncomingCall_Rejected() { - Call call = getMockCall(NUMBER, true, 0, Call.CALL_HISTORY_STATUS_NOT_PRESENT, - LogState.LOOKUP_UNKNOWN, DisconnectCause.REJECTED); - mListener.onDisconnect(call); - assertFalse(mListener.mShowNotificationCalled); - } - public void testIncomingCall_HangUpLocal() { - Call call = getMockCall(NUMBER, true, DURATION, Call.CALL_HISTORY_STATUS_NOT_PRESENT, - LogState.LOOKUP_UNKNOWN, DisconnectCause.LOCAL); - mListener.onDisconnect(call); - assertTrue(mListener.mShowNotificationCalled); - } - - public void testIncomingCall_HangUpRemote() { - Call call = getMockCall(NUMBER, true, DURATION, Call.CALL_HISTORY_STATUS_NOT_PRESENT, - LogState.LOOKUP_UNKNOWN, DisconnectCause.REMOTE); - mListener.onDisconnect(call); - assertTrue(mListener.mShowNotificationCalled); - } - - public void testIncomingCall_ValidNumber_NotInCallHistory_InContacts() { - Call call = getMockCall(NUMBER, true, 0, Call.CALL_HISTORY_STATUS_NOT_PRESENT, - LogState.LOOKUP_LOCAL_CONTACT, DisconnectCause.REJECTED); - mListener.onDisconnect(call); - assertFalse(mListener.mShowNotificationCalled); - } - - public void testIncomingCall_ValidNumber_InCallHistory_InContacts() { - Call call = getMockCall(NUMBER, true, 0, Call.CALL_HISTORY_STATUS_PRESENT, - LogState.LOOKUP_LOCAL_CONTACT, DisconnectCause.REJECTED); - mListener.onDisconnect(call); - assertFalse(mListener.mShowNotificationCalled); - } - - public void testIncomingCall_ValidNumber_InCallHistory_NotInContacts() { - Call call = getMockCall(NUMBER, true, 0, Call.CALL_HISTORY_STATUS_PRESENT, - LogState.LOOKUP_UNKNOWN, DisconnectCause.REJECTED); - mListener.onDisconnect(call); - assertFalse(mListener.mShowNotificationCalled); - } - - public void testIncomingCall_ValidNumber_NotInCallHistory_NotInContacts() throws Throwable { - Call call = getMockCall(NUMBER, true, DURATION, Call.CALL_HISTORY_STATUS_NOT_PRESENT, - LogState.LOOKUP_UNKNOWN, DisconnectCause.LOCAL); - mListener.onDisconnect(call); - assertTrue(mListener.mShowNotificationCalled); - } - - public void testIncomingCall_CheckCallHistory_NumberExists() throws Throwable { - final Call call = getMockCall(NUMBER, true, DURATION, Call.CALL_HISTORY_STATUS_UNKNOWN, - LogState.LOOKUP_UNKNOWN, DisconnectCause.LOCAL); - expectCallLogQuery(NUMBER, true); - incomingCall(call); - verify(call).setCallHistoryStatus(eq(Call.CALL_HISTORY_STATUS_PRESENT)); - assertFalse(mListener.mShowNotificationCalled); - } - - public void testIncomingCall_CheckCallHistory_NumberNotExists() throws Throwable { - final Call call = getMockCall(NUMBER, true, DURATION, Call.CALL_HISTORY_STATUS_UNKNOWN, - LogState.LOOKUP_UNKNOWN, DisconnectCause.LOCAL); - expectCallLogQuery(NUMBER, false); - incomingCall(call); - verify(call).setCallHistoryStatus(eq(Call.CALL_HISTORY_STATUS_NOT_PRESENT)); - assertTrue(mListener.mShowNotificationCalled); - } - - private void incomingCall(final Call call) throws Throwable { - runTestOnUiThread(new Runnable() { - @Override - public void run() { - mListener.onIncomingCall(call); - } - }); - getInstrumentation().waitForIdleSync(); - mFakeAsyncTaskExecutor.runTask(CallLogAsyncTaskUtil.Tasks.GET_NUMBER_IN_CALL_HISTORY); - mListener.onDisconnect(call); - } - - private void expectCallLogQuery(String number, boolean inCallHistory) { - MockContentProvider.Query query = mContext.getContactsProvider() - .expectQuery(TelecomUtil.getCallLogUri(mContext)) - .withSelection(CallLog.Calls.NUMBER + " = ?", number) - .withProjection(CallLog.Calls._ID) - .withAnySortOrder(); - ContentValues values = new ContentValues(); - values.put(CallLog.Calls.NUMBER, number); - if (inCallHistory) { - query.returnRow(values); - } else { - query.returnEmptyCursor(); - } - } - - private static Call getMockCall(String number, - boolean isIncoming, - int duration, - @CallHistoryStatus int callHistoryStatus, - int contactLookupResult, - int disconnectCause) { - Call call = mock(Call.class); - LogState logState = new LogState(); - logState.isIncoming = isIncoming; - logState.duration = duration; - logState.contactLookupResult = contactLookupResult; - when(call.getDisconnectCause()).thenReturn(new DisconnectCause(disconnectCause)); - when(call.getLogState()).thenReturn(logState); - when(call.getNumber()).thenReturn(number); - doCallRealMethod().when(call).setCallHistoryStatus(anyInt()); - when(call.getCallHistoryStatus()).thenCallRealMethod(); - call.setCallHistoryStatus(callHistoryStatus); - return call; - } - - private static class TestSpamCallListListener extends SpamCallListListener { - private boolean mShowNotificationCalled; - - public TestSpamCallListListener(Context context) { - super(context); - } - - void showNotification(String number) { - mShowNotificationCalled = true; - } - } -} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..c5b1efa7a --- /dev/null +++ b/LICENSE @@ -0,0 +1,190 @@ + + Copyright (c) 2005-2008, The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + diff --git a/assets/product/res/drawable-hdpi/product_logo_avatar_anonymous_color_120.png b/assets/product/res/drawable-hdpi/product_logo_avatar_anonymous_color_120.png new file mode 100644 index 000000000..70d3011dd Binary files /dev/null and b/assets/product/res/drawable-hdpi/product_logo_avatar_anonymous_color_120.png differ diff --git a/assets/product/res/drawable-hdpi/product_logo_avatar_anonymous_white_color_120.png b/assets/product/res/drawable-hdpi/product_logo_avatar_anonymous_white_color_120.png new file mode 100644 index 000000000..4068d5aa7 Binary files /dev/null and b/assets/product/res/drawable-hdpi/product_logo_avatar_anonymous_white_color_120.png differ diff --git a/assets/product/res/drawable-mdpi/product_logo_avatar_anonymous_color_120.png b/assets/product/res/drawable-mdpi/product_logo_avatar_anonymous_color_120.png new file mode 100644 index 000000000..60d3c3a49 Binary files /dev/null and b/assets/product/res/drawable-mdpi/product_logo_avatar_anonymous_color_120.png differ diff --git a/assets/product/res/drawable-mdpi/product_logo_avatar_anonymous_white_color_120.png b/assets/product/res/drawable-mdpi/product_logo_avatar_anonymous_white_color_120.png new file mode 100644 index 000000000..0524cf053 Binary files /dev/null and b/assets/product/res/drawable-mdpi/product_logo_avatar_anonymous_white_color_120.png differ diff --git a/assets/product/res/drawable-xhdpi/product_logo_avatar_anonymous_color_120.png b/assets/product/res/drawable-xhdpi/product_logo_avatar_anonymous_color_120.png new file mode 100644 index 000000000..ec99ca6b8 Binary files /dev/null and b/assets/product/res/drawable-xhdpi/product_logo_avatar_anonymous_color_120.png differ diff --git a/assets/product/res/drawable-xhdpi/product_logo_avatar_anonymous_white_color_120.png b/assets/product/res/drawable-xhdpi/product_logo_avatar_anonymous_white_color_120.png new file mode 100644 index 000000000..ba27ee76b Binary files /dev/null and b/assets/product/res/drawable-xhdpi/product_logo_avatar_anonymous_white_color_120.png differ diff --git a/assets/product/res/drawable-xxhdpi/product_logo_avatar_anonymous_color_120.png b/assets/product/res/drawable-xxhdpi/product_logo_avatar_anonymous_color_120.png new file mode 100644 index 000000000..2b009a3da Binary files /dev/null and b/assets/product/res/drawable-xxhdpi/product_logo_avatar_anonymous_color_120.png differ diff --git a/assets/product/res/drawable-xxhdpi/product_logo_avatar_anonymous_white_color_120.png b/assets/product/res/drawable-xxhdpi/product_logo_avatar_anonymous_white_color_120.png new file mode 100644 index 000000000..2dc724899 Binary files /dev/null and b/assets/product/res/drawable-xxhdpi/product_logo_avatar_anonymous_white_color_120.png differ diff --git a/assets/product/res/drawable-xxxhdpi/product_logo_avatar_anonymous_color_120.png b/assets/product/res/drawable-xxxhdpi/product_logo_avatar_anonymous_color_120.png new file mode 100644 index 000000000..4b111b1ca Binary files /dev/null and b/assets/product/res/drawable-xxxhdpi/product_logo_avatar_anonymous_color_120.png differ diff --git a/assets/product/res/drawable-xxxhdpi/product_logo_avatar_anonymous_white_color_120.png b/assets/product/res/drawable-xxxhdpi/product_logo_avatar_anonymous_white_color_120.png new file mode 100644 index 000000000..230be8ceb Binary files /dev/null and b/assets/product/res/drawable-xxxhdpi/product_logo_avatar_anonymous_white_color_120.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_arrow_back_white_24.png b/assets/quantum/res/drawable-hdpi/quantum_ic_arrow_back_white_24.png new file mode 100644 index 000000000..cd1972677 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_arrow_back_white_24.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_arrow_drop_down_white_18.png b/assets/quantum/res/drawable-hdpi/quantum_ic_arrow_drop_down_white_18.png new file mode 100644 index 000000000..41541bb0d Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_arrow_drop_down_white_18.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_bluetooth_audio_grey600_24.png b/assets/quantum/res/drawable-hdpi/quantum_ic_bluetooth_audio_grey600_24.png new file mode 100644 index 000000000..ec2349ca8 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_bluetooth_audio_grey600_24.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_bluetooth_audio_white_36.png b/assets/quantum/res/drawable-hdpi/quantum_ic_bluetooth_audio_white_36.png new file mode 100644 index 000000000..398f0a938 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_bluetooth_audio_white_36.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_call_end_white_24.png b/assets/quantum/res/drawable-hdpi/quantum_ic_call_end_white_24.png new file mode 100644 index 000000000..625b827c4 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_call_end_white_24.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_call_end_white_36.png b/assets/quantum/res/drawable-hdpi/quantum_ic_call_end_white_36.png new file mode 100644 index 000000000..51456d3d5 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_call_end_white_36.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_call_merge_white_36.png b/assets/quantum/res/drawable-hdpi/quantum_ic_call_merge_white_36.png new file mode 100644 index 000000000..b7aba8072 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_call_merge_white_36.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_call_white_18.png b/assets/quantum/res/drawable-hdpi/quantum_ic_call_white_18.png new file mode 100644 index 000000000..0bdc56be6 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_call_white_18.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_call_white_24.png b/assets/quantum/res/drawable-hdpi/quantum_ic_call_white_24.png new file mode 100644 index 000000000..4dc506515 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_call_white_24.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_camera_alt_white_24.png b/assets/quantum/res/drawable-hdpi/quantum_ic_camera_alt_white_24.png new file mode 100644 index 000000000..497c88ca8 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_camera_alt_white_24.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_camera_alt_white_48.png b/assets/quantum/res/drawable-hdpi/quantum_ic_camera_alt_white_48.png new file mode 100644 index 000000000..c8e69dceb Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_camera_alt_white_48.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_camera_front_white_36.png b/assets/quantum/res/drawable-hdpi/quantum_ic_camera_front_white_36.png new file mode 100644 index 000000000..f8394dc4f Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_camera_front_white_36.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_camera_rear_white_36.png b/assets/quantum/res/drawable-hdpi/quantum_ic_camera_rear_white_36.png new file mode 100644 index 000000000..0354d41ca Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_camera_rear_white_36.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_check_black_24.png b/assets/quantum/res/drawable-hdpi/quantum_ic_check_black_24.png new file mode 100644 index 000000000..e802d90ae Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_check_black_24.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_check_black_36.png b/assets/quantum/res/drawable-hdpi/quantum_ic_check_black_36.png new file mode 100644 index 000000000..4992b76b0 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_check_black_36.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_check_white_48.png b/assets/quantum/res/drawable-hdpi/quantum_ic_check_white_48.png new file mode 100644 index 000000000..2c2ad771f Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_check_white_48.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_close_white_24.png b/assets/quantum/res/drawable-hdpi/quantum_ic_close_white_24.png new file mode 100644 index 000000000..ceb1a1eeb Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_close_white_24.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_close_white_36.png b/assets/quantum/res/drawable-hdpi/quantum_ic_close_white_36.png new file mode 100644 index 000000000..86bd673af Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_close_white_36.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_dialpad_white_36.png b/assets/quantum/res/drawable-hdpi/quantum_ic_dialpad_white_36.png new file mode 100644 index 000000000..82710e72a Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_dialpad_white_36.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_forward_white_24.png b/assets/quantum/res/drawable-hdpi/quantum_ic_forward_white_24.png new file mode 100644 index 000000000..a0711d377 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_forward_white_24.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_fullscreen_exit_white_48.png b/assets/quantum/res/drawable-hdpi/quantum_ic_fullscreen_exit_white_48.png new file mode 100644 index 000000000..159bea7fd Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_fullscreen_exit_white_48.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_fullscreen_white_36.png b/assets/quantum/res/drawable-hdpi/quantum_ic_fullscreen_white_36.png new file mode 100644 index 000000000..9e3b97cac Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_fullscreen_white_36.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_fullscreen_white_48.png b/assets/quantum/res/drawable-hdpi/quantum_ic_fullscreen_white_48.png new file mode 100644 index 000000000..9b8131124 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_fullscreen_white_48.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_group_white_36.png b/assets/quantum/res/drawable-hdpi/quantum_ic_group_white_36.png new file mode 100644 index 000000000..cbdef2f3e Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_group_white_36.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_hd_white_24.png b/assets/quantum/res/drawable-hdpi/quantum_ic_hd_white_24.png new file mode 100644 index 000000000..35bf51a4f Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_hd_white_24.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_headset_grey600_24.png b/assets/quantum/res/drawable-hdpi/quantum_ic_headset_grey600_24.png new file mode 100644 index 000000000..e859c2f31 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_headset_grey600_24.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_headset_white_36.png b/assets/quantum/res/drawable-hdpi/quantum_ic_headset_white_36.png new file mode 100644 index 000000000..f77f24767 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_headset_white_36.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_message_white_24.png b/assets/quantum/res/drawable-hdpi/quantum_ic_message_white_24.png new file mode 100644 index 000000000..57177b7c6 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_message_white_24.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_mic_off_black_24.png b/assets/quantum/res/drawable-hdpi/quantum_ic_mic_off_black_24.png new file mode 100644 index 000000000..1755dbf3f Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_mic_off_black_24.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_mic_off_white_36.png b/assets/quantum/res/drawable-hdpi/quantum_ic_mic_off_white_36.png new file mode 100644 index 000000000..203cb8a9f Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_mic_off_white_36.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_network_wifi_white_24.png b/assets/quantum/res/drawable-hdpi/quantum_ic_network_wifi_white_24.png new file mode 100644 index 000000000..8df91f236 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_network_wifi_white_24.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_pause_white_36.png b/assets/quantum/res/drawable-hdpi/quantum_ic_pause_white_36.png new file mode 100644 index 000000000..1d024393a Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_pause_white_36.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_person_add_white_24.png b/assets/quantum/res/drawable-hdpi/quantum_ic_person_add_white_24.png new file mode 100644 index 000000000..10ae5a70c Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_person_add_white_24.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_photo_library_white_24.png b/assets/quantum/res/drawable-hdpi/quantum_ic_photo_library_white_24.png new file mode 100644 index 000000000..c4a2229e9 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_photo_library_white_24.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_photo_white_24.png b/assets/quantum/res/drawable-hdpi/quantum_ic_photo_white_24.png new file mode 100644 index 000000000..b414cf5b6 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_photo_white_24.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_photo_white_48.png b/assets/quantum/res/drawable-hdpi/quantum_ic_photo_white_48.png new file mode 100644 index 000000000..f9f1defa6 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_photo_white_48.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_report_white_18.png b/assets/quantum/res/drawable-hdpi/quantum_ic_report_white_18.png new file mode 100644 index 000000000..f0bb6f5be Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_report_white_18.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_send_black_24.png b/assets/quantum/res/drawable-hdpi/quantum_ic_send_black_24.png new file mode 100644 index 000000000..f5518772a Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_send_black_24.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_send_white_24.png b/assets/quantum/res/drawable-hdpi/quantum_ic_send_white_24.png new file mode 100644 index 000000000..5d4ad4b02 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_send_white_24.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_swap_calls_white_36.png b/assets/quantum/res/drawable-hdpi/quantum_ic_swap_calls_white_36.png new file mode 100644 index 000000000..8c3a0edaa Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_swap_calls_white_36.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_switch_camera_white_36.png b/assets/quantum/res/drawable-hdpi/quantum_ic_switch_camera_white_36.png new file mode 100644 index 000000000..21b4e4388 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_switch_camera_white_36.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_switch_video_white_36.png b/assets/quantum/res/drawable-hdpi/quantum_ic_switch_video_white_36.png new file mode 100644 index 000000000..029b07736 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_switch_video_white_36.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_undo_white_48.png b/assets/quantum/res/drawable-hdpi/quantum_ic_undo_white_48.png new file mode 100644 index 000000000..4366bb082 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_undo_white_48.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_videocam_off_white_24.png b/assets/quantum/res/drawable-hdpi/quantum_ic_videocam_off_white_24.png new file mode 100644 index 000000000..aaf5ac208 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_videocam_off_white_24.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_videocam_off_white_36.png b/assets/quantum/res/drawable-hdpi/quantum_ic_videocam_off_white_36.png new file mode 100644 index 000000000..f2e461a9f Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_videocam_off_white_36.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_videocam_white_18.png b/assets/quantum/res/drawable-hdpi/quantum_ic_videocam_white_18.png new file mode 100644 index 000000000..abf478ada Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_videocam_white_18.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_videocam_white_24.png b/assets/quantum/res/drawable-hdpi/quantum_ic_videocam_white_24.png new file mode 100644 index 000000000..d83e0d50c Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_videocam_white_24.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_videocam_white_36.png b/assets/quantum/res/drawable-hdpi/quantum_ic_videocam_white_36.png new file mode 100644 index 000000000..49562a640 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_videocam_white_36.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_voicemail_white_18.png b/assets/quantum/res/drawable-hdpi/quantum_ic_voicemail_white_18.png new file mode 100644 index 000000000..23e8997f2 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_voicemail_white_18.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_volume_up_grey600_24.png b/assets/quantum/res/drawable-hdpi/quantum_ic_volume_up_grey600_24.png new file mode 100644 index 000000000..49eb8fcc3 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_volume_up_grey600_24.png differ diff --git a/assets/quantum/res/drawable-hdpi/quantum_ic_volume_up_white_36.png b/assets/quantum/res/drawable-hdpi/quantum_ic_volume_up_white_36.png new file mode 100644 index 000000000..62d22bec8 Binary files /dev/null and b/assets/quantum/res/drawable-hdpi/quantum_ic_volume_up_white_36.png differ diff --git a/assets/quantum/res/drawable-ldrtl-hdpi/quantum_ic_arrow_back_white_24.png b/assets/quantum/res/drawable-ldrtl-hdpi/quantum_ic_arrow_back_white_24.png new file mode 100644 index 000000000..f51755762 Binary files /dev/null and b/assets/quantum/res/drawable-ldrtl-hdpi/quantum_ic_arrow_back_white_24.png differ diff --git a/assets/quantum/res/drawable-ldrtl-hdpi/quantum_ic_send_black_24.png b/assets/quantum/res/drawable-ldrtl-hdpi/quantum_ic_send_black_24.png new file mode 100644 index 000000000..c0ab46f5d Binary files /dev/null and b/assets/quantum/res/drawable-ldrtl-hdpi/quantum_ic_send_black_24.png differ diff --git a/assets/quantum/res/drawable-ldrtl-hdpi/quantum_ic_send_white_24.png b/assets/quantum/res/drawable-ldrtl-hdpi/quantum_ic_send_white_24.png new file mode 100644 index 000000000..b8d4ce444 Binary files /dev/null and b/assets/quantum/res/drawable-ldrtl-hdpi/quantum_ic_send_white_24.png differ diff --git a/assets/quantum/res/drawable-ldrtl-hdpi/quantum_ic_undo_white_48.png b/assets/quantum/res/drawable-ldrtl-hdpi/quantum_ic_undo_white_48.png new file mode 100644 index 000000000..6c8174f3a Binary files /dev/null and b/assets/quantum/res/drawable-ldrtl-hdpi/quantum_ic_undo_white_48.png differ diff --git a/assets/quantum/res/drawable-ldrtl-mdpi/quantum_ic_arrow_back_white_24.png b/assets/quantum/res/drawable-ldrtl-mdpi/quantum_ic_arrow_back_white_24.png new file mode 100644 index 000000000..22a1140ae Binary files /dev/null and b/assets/quantum/res/drawable-ldrtl-mdpi/quantum_ic_arrow_back_white_24.png differ diff --git a/assets/quantum/res/drawable-ldrtl-mdpi/quantum_ic_send_black_24.png b/assets/quantum/res/drawable-ldrtl-mdpi/quantum_ic_send_black_24.png new file mode 100644 index 000000000..f2232c7db Binary files /dev/null and b/assets/quantum/res/drawable-ldrtl-mdpi/quantum_ic_send_black_24.png differ diff --git a/assets/quantum/res/drawable-ldrtl-mdpi/quantum_ic_send_white_24.png b/assets/quantum/res/drawable-ldrtl-mdpi/quantum_ic_send_white_24.png new file mode 100644 index 000000000..7933f42f0 Binary files /dev/null and b/assets/quantum/res/drawable-ldrtl-mdpi/quantum_ic_send_white_24.png differ diff --git a/assets/quantum/res/drawable-ldrtl-mdpi/quantum_ic_undo_white_48.png b/assets/quantum/res/drawable-ldrtl-mdpi/quantum_ic_undo_white_48.png new file mode 100644 index 000000000..b47cef666 Binary files /dev/null and b/assets/quantum/res/drawable-ldrtl-mdpi/quantum_ic_undo_white_48.png differ diff --git a/assets/quantum/res/drawable-ldrtl-xhdpi/quantum_ic_arrow_back_white_24.png b/assets/quantum/res/drawable-ldrtl-xhdpi/quantum_ic_arrow_back_white_24.png new file mode 100644 index 000000000..d858f18e6 Binary files /dev/null and b/assets/quantum/res/drawable-ldrtl-xhdpi/quantum_ic_arrow_back_white_24.png differ diff --git a/assets/quantum/res/drawable-ldrtl-xhdpi/quantum_ic_send_black_24.png b/assets/quantum/res/drawable-ldrtl-xhdpi/quantum_ic_send_black_24.png new file mode 100644 index 000000000..3a25fbb86 Binary files /dev/null and b/assets/quantum/res/drawable-ldrtl-xhdpi/quantum_ic_send_black_24.png differ diff --git a/assets/quantum/res/drawable-ldrtl-xhdpi/quantum_ic_send_white_24.png b/assets/quantum/res/drawable-ldrtl-xhdpi/quantum_ic_send_white_24.png new file mode 100644 index 000000000..4735a7d71 Binary files /dev/null and b/assets/quantum/res/drawable-ldrtl-xhdpi/quantum_ic_send_white_24.png differ diff --git a/assets/quantum/res/drawable-ldrtl-xhdpi/quantum_ic_undo_white_48.png b/assets/quantum/res/drawable-ldrtl-xhdpi/quantum_ic_undo_white_48.png new file mode 100644 index 000000000..6a984c4f1 Binary files /dev/null and b/assets/quantum/res/drawable-ldrtl-xhdpi/quantum_ic_undo_white_48.png differ diff --git a/assets/quantum/res/drawable-ldrtl-xxhdpi/quantum_ic_arrow_back_white_24.png b/assets/quantum/res/drawable-ldrtl-xxhdpi/quantum_ic_arrow_back_white_24.png new file mode 100644 index 000000000..614ad49a3 Binary files /dev/null and b/assets/quantum/res/drawable-ldrtl-xxhdpi/quantum_ic_arrow_back_white_24.png differ diff --git a/assets/quantum/res/drawable-ldrtl-xxhdpi/quantum_ic_send_black_24.png b/assets/quantum/res/drawable-ldrtl-xxhdpi/quantum_ic_send_black_24.png new file mode 100644 index 000000000..ba6dd1a96 Binary files /dev/null and b/assets/quantum/res/drawable-ldrtl-xxhdpi/quantum_ic_send_black_24.png differ diff --git a/assets/quantum/res/drawable-ldrtl-xxhdpi/quantum_ic_send_white_24.png b/assets/quantum/res/drawable-ldrtl-xxhdpi/quantum_ic_send_white_24.png new file mode 100644 index 000000000..4a9e2c24a Binary files /dev/null and b/assets/quantum/res/drawable-ldrtl-xxhdpi/quantum_ic_send_white_24.png differ diff --git a/assets/quantum/res/drawable-ldrtl-xxhdpi/quantum_ic_undo_white_48.png b/assets/quantum/res/drawable-ldrtl-xxhdpi/quantum_ic_undo_white_48.png new file mode 100644 index 000000000..907911055 Binary files /dev/null and b/assets/quantum/res/drawable-ldrtl-xxhdpi/quantum_ic_undo_white_48.png differ diff --git a/assets/quantum/res/drawable-ldrtl-xxxhdpi/quantum_ic_arrow_back_white_24.png b/assets/quantum/res/drawable-ldrtl-xxxhdpi/quantum_ic_arrow_back_white_24.png new file mode 100644 index 000000000..d409b544b Binary files /dev/null and b/assets/quantum/res/drawable-ldrtl-xxxhdpi/quantum_ic_arrow_back_white_24.png differ diff --git a/assets/quantum/res/drawable-ldrtl-xxxhdpi/quantum_ic_send_black_24.png b/assets/quantum/res/drawable-ldrtl-xxxhdpi/quantum_ic_send_black_24.png new file mode 100644 index 000000000..92117e109 Binary files /dev/null and b/assets/quantum/res/drawable-ldrtl-xxxhdpi/quantum_ic_send_black_24.png differ diff --git a/assets/quantum/res/drawable-ldrtl-xxxhdpi/quantum_ic_send_white_24.png b/assets/quantum/res/drawable-ldrtl-xxxhdpi/quantum_ic_send_white_24.png new file mode 100644 index 000000000..0167ac829 Binary files /dev/null and b/assets/quantum/res/drawable-ldrtl-xxxhdpi/quantum_ic_send_white_24.png differ diff --git a/assets/quantum/res/drawable-ldrtl-xxxhdpi/quantum_ic_undo_white_48.png b/assets/quantum/res/drawable-ldrtl-xxxhdpi/quantum_ic_undo_white_48.png new file mode 100644 index 000000000..aa7a91943 Binary files /dev/null and b/assets/quantum/res/drawable-ldrtl-xxxhdpi/quantum_ic_undo_white_48.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_arrow_back_white_24.png b/assets/quantum/res/drawable-mdpi/quantum_ic_arrow_back_white_24.png new file mode 100644 index 000000000..4ef72eec9 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_arrow_back_white_24.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_arrow_drop_down_white_18.png b/assets/quantum/res/drawable-mdpi/quantum_ic_arrow_drop_down_white_18.png new file mode 100644 index 000000000..7c1fc3d7c Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_arrow_drop_down_white_18.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_bluetooth_audio_grey600_24.png b/assets/quantum/res/drawable-mdpi/quantum_ic_bluetooth_audio_grey600_24.png new file mode 100644 index 000000000..de635e034 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_bluetooth_audio_grey600_24.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_bluetooth_audio_white_36.png b/assets/quantum/res/drawable-mdpi/quantum_ic_bluetooth_audio_white_36.png new file mode 100644 index 000000000..046372d0d Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_bluetooth_audio_white_36.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_call_end_white_24.png b/assets/quantum/res/drawable-mdpi/quantum_ic_call_end_white_24.png new file mode 100644 index 000000000..378272ffc Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_call_end_white_24.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_call_end_white_36.png b/assets/quantum/res/drawable-mdpi/quantum_ic_call_end_white_36.png new file mode 100644 index 000000000..625b827c4 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_call_end_white_36.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_call_merge_white_36.png b/assets/quantum/res/drawable-mdpi/quantum_ic_call_merge_white_36.png new file mode 100644 index 000000000..a2eb54bab Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_call_merge_white_36.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_call_white_18.png b/assets/quantum/res/drawable-mdpi/quantum_ic_call_white_18.png new file mode 100644 index 000000000..bd5748575 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_call_white_18.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_call_white_24.png b/assets/quantum/res/drawable-mdpi/quantum_ic_call_white_24.png new file mode 100644 index 000000000..77f9de5e3 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_call_white_24.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_camera_alt_white_24.png b/assets/quantum/res/drawable-mdpi/quantum_ic_camera_alt_white_24.png new file mode 100644 index 000000000..e83052200 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_camera_alt_white_24.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_camera_alt_white_48.png b/assets/quantum/res/drawable-mdpi/quantum_ic_camera_alt_white_48.png new file mode 100644 index 000000000..be9fb226a Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_camera_alt_white_48.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_camera_front_white_36.png b/assets/quantum/res/drawable-mdpi/quantum_ic_camera_front_white_36.png new file mode 100644 index 000000000..f36dcd496 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_camera_front_white_36.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_camera_rear_white_36.png b/assets/quantum/res/drawable-mdpi/quantum_ic_camera_rear_white_36.png new file mode 100644 index 000000000..4a5494f28 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_camera_rear_white_36.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_check_black_24.png b/assets/quantum/res/drawable-mdpi/quantum_ic_check_black_24.png new file mode 100644 index 000000000..1c14c9c44 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_check_black_24.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_check_black_36.png b/assets/quantum/res/drawable-mdpi/quantum_ic_check_black_36.png new file mode 100644 index 000000000..e802d90ae Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_check_black_36.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_check_white_48.png b/assets/quantum/res/drawable-mdpi/quantum_ic_check_white_48.png new file mode 100644 index 000000000..3b2b65d26 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_check_white_48.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_close_white_24.png b/assets/quantum/res/drawable-mdpi/quantum_ic_close_white_24.png new file mode 100644 index 000000000..af7f8288d Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_close_white_24.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_close_white_36.png b/assets/quantum/res/drawable-mdpi/quantum_ic_close_white_36.png new file mode 100644 index 000000000..ceb1a1eeb Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_close_white_36.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_dialpad_white_36.png b/assets/quantum/res/drawable-mdpi/quantum_ic_dialpad_white_36.png new file mode 100644 index 000000000..9037f94e8 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_dialpad_white_36.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_forward_white_24.png b/assets/quantum/res/drawable-mdpi/quantum_ic_forward_white_24.png new file mode 100644 index 000000000..65f73299f Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_forward_white_24.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_fullscreen_exit_white_48.png b/assets/quantum/res/drawable-mdpi/quantum_ic_fullscreen_exit_white_48.png new file mode 100644 index 000000000..364bad0b8 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_fullscreen_exit_white_48.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_fullscreen_white_36.png b/assets/quantum/res/drawable-mdpi/quantum_ic_fullscreen_white_36.png new file mode 100644 index 000000000..c150cb58d Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_fullscreen_white_36.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_fullscreen_white_48.png b/assets/quantum/res/drawable-mdpi/quantum_ic_fullscreen_white_48.png new file mode 100644 index 000000000..4423c7ce9 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_fullscreen_white_48.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_group_white_36.png b/assets/quantum/res/drawable-mdpi/quantum_ic_group_white_36.png new file mode 100644 index 000000000..ff698afc0 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_group_white_36.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_hd_white_24.png b/assets/quantum/res/drawable-mdpi/quantum_ic_hd_white_24.png new file mode 100644 index 000000000..30938fe4d Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_hd_white_24.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_headset_grey600_24.png b/assets/quantum/res/drawable-mdpi/quantum_ic_headset_grey600_24.png new file mode 100644 index 000000000..371efd382 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_headset_grey600_24.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_headset_white_36.png b/assets/quantum/res/drawable-mdpi/quantum_ic_headset_white_36.png new file mode 100644 index 000000000..d25d3888e Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_headset_white_36.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_message_white_24.png b/assets/quantum/res/drawable-mdpi/quantum_ic_message_white_24.png new file mode 100644 index 000000000..3072b7569 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_message_white_24.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_mic_off_black_24.png b/assets/quantum/res/drawable-mdpi/quantum_ic_mic_off_black_24.png new file mode 100644 index 000000000..da605a5a1 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_mic_off_black_24.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_mic_off_white_36.png b/assets/quantum/res/drawable-mdpi/quantum_ic_mic_off_white_36.png new file mode 100644 index 000000000..6fccf5d09 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_mic_off_white_36.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_network_wifi_white_24.png b/assets/quantum/res/drawable-mdpi/quantum_ic_network_wifi_white_24.png new file mode 100644 index 000000000..1c3e8b987 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_network_wifi_white_24.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_pause_white_36.png b/assets/quantum/res/drawable-mdpi/quantum_ic_pause_white_36.png new file mode 100644 index 000000000..4d2ea05c4 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_pause_white_36.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_person_add_white_24.png b/assets/quantum/res/drawable-mdpi/quantum_ic_person_add_white_24.png new file mode 100644 index 000000000..38e0a2882 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_person_add_white_24.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_photo_library_white_24.png b/assets/quantum/res/drawable-mdpi/quantum_ic_photo_library_white_24.png new file mode 100644 index 000000000..02ef4cdb0 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_photo_library_white_24.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_photo_white_24.png b/assets/quantum/res/drawable-mdpi/quantum_ic_photo_white_24.png new file mode 100644 index 000000000..d474bd577 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_photo_white_24.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_photo_white_48.png b/assets/quantum/res/drawable-mdpi/quantum_ic_photo_white_48.png new file mode 100644 index 000000000..2642b9e09 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_photo_white_48.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_report_white_18.png b/assets/quantum/res/drawable-mdpi/quantum_ic_report_white_18.png new file mode 100644 index 000000000..63ef73683 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_report_white_18.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_send_black_24.png b/assets/quantum/res/drawable-mdpi/quantum_ic_send_black_24.png new file mode 100644 index 000000000..8bff465eb Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_send_black_24.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_send_white_24.png b/assets/quantum/res/drawable-mdpi/quantum_ic_send_white_24.png new file mode 100644 index 000000000..b58afb0b4 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_send_white_24.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_swap_calls_white_36.png b/assets/quantum/res/drawable-mdpi/quantum_ic_swap_calls_white_36.png new file mode 100644 index 000000000..9491f2d1a Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_swap_calls_white_36.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_switch_camera_white_36.png b/assets/quantum/res/drawable-mdpi/quantum_ic_switch_camera_white_36.png new file mode 100644 index 000000000..c318983c7 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_switch_camera_white_36.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_switch_video_white_36.png b/assets/quantum/res/drawable-mdpi/quantum_ic_switch_video_white_36.png new file mode 100644 index 000000000..2bf8a169e Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_switch_video_white_36.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_undo_white_48.png b/assets/quantum/res/drawable-mdpi/quantum_ic_undo_white_48.png new file mode 100644 index 000000000..b67f6a911 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_undo_white_48.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_videocam_off_white_24.png b/assets/quantum/res/drawable-mdpi/quantum_ic_videocam_off_white_24.png new file mode 100644 index 000000000..d1cca6f0a Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_videocam_off_white_24.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_videocam_off_white_36.png b/assets/quantum/res/drawable-mdpi/quantum_ic_videocam_off_white_36.png new file mode 100644 index 000000000..aaf5ac208 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_videocam_off_white_36.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_videocam_white_18.png b/assets/quantum/res/drawable-mdpi/quantum_ic_videocam_white_18.png new file mode 100644 index 000000000..1dafd4927 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_videocam_white_18.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_videocam_white_24.png b/assets/quantum/res/drawable-mdpi/quantum_ic_videocam_white_24.png new file mode 100644 index 000000000..d146209a5 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_videocam_white_24.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_videocam_white_36.png b/assets/quantum/res/drawable-mdpi/quantum_ic_videocam_white_36.png new file mode 100644 index 000000000..d83e0d50c Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_videocam_white_36.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_voicemail_white_18.png b/assets/quantum/res/drawable-mdpi/quantum_ic_voicemail_white_18.png new file mode 100644 index 000000000..fa4755769 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_voicemail_white_18.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_volume_up_grey600_24.png b/assets/quantum/res/drawable-mdpi/quantum_ic_volume_up_grey600_24.png new file mode 100644 index 000000000..d6cea3667 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_volume_up_grey600_24.png differ diff --git a/assets/quantum/res/drawable-mdpi/quantum_ic_volume_up_white_36.png b/assets/quantum/res/drawable-mdpi/quantum_ic_volume_up_white_36.png new file mode 100644 index 000000000..57d787163 Binary files /dev/null and b/assets/quantum/res/drawable-mdpi/quantum_ic_volume_up_white_36.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_arrow_back_white_24.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_arrow_back_white_24.png new file mode 100644 index 000000000..832f5a361 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_arrow_back_white_24.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_arrow_drop_down_white_18.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_arrow_drop_down_white_18.png new file mode 100644 index 000000000..4c6076df7 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_arrow_drop_down_white_18.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_bluetooth_audio_grey600_24.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_bluetooth_audio_grey600_24.png new file mode 100644 index 000000000..eea1bbf04 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_bluetooth_audio_grey600_24.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_bluetooth_audio_white_36.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_bluetooth_audio_white_36.png new file mode 100644 index 000000000..d5022d063 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_bluetooth_audio_white_36.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_call_end_white_24.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_call_end_white_24.png new file mode 100644 index 000000000..a4fe6889d Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_call_end_white_24.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_call_end_white_36.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_call_end_white_36.png new file mode 100644 index 000000000..e1831d7af Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_call_end_white_36.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_call_merge_white_36.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_call_merge_white_36.png new file mode 100644 index 000000000..01daecf65 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_call_merge_white_36.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_call_white_18.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_call_white_18.png new file mode 100644 index 000000000..4dc506515 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_call_white_18.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_call_white_24.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_call_white_24.png new file mode 100644 index 000000000..ef45e933a Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_call_white_24.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_camera_alt_white_24.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_camera_alt_white_24.png new file mode 100644 index 000000000..be9fb226a Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_camera_alt_white_24.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_camera_alt_white_48.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_camera_alt_white_48.png new file mode 100644 index 000000000..777658e95 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_camera_alt_white_48.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_camera_front_white_36.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_camera_front_white_36.png new file mode 100644 index 000000000..3eb24d1f2 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_camera_front_white_36.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_camera_rear_white_36.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_camera_rear_white_36.png new file mode 100644 index 000000000..8392b2a88 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_camera_rear_white_36.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_check_black_24.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_check_black_24.png new file mode 100644 index 000000000..64a4944f7 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_check_black_24.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_check_black_36.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_check_black_36.png new file mode 100644 index 000000000..b26a2c05e Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_check_black_36.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_check_white_48.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_check_white_48.png new file mode 100644 index 000000000..d670618c7 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_check_white_48.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_close_white_24.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_close_white_24.png new file mode 100644 index 000000000..b7c7ffd0e Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_close_white_24.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_close_white_36.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_close_white_36.png new file mode 100644 index 000000000..6b717e0dd Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_close_white_36.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_dialpad_white_36.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_dialpad_white_36.png new file mode 100644 index 000000000..175000510 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_dialpad_white_36.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_forward_white_24.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_forward_white_24.png new file mode 100644 index 000000000..7a5df52bf Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_forward_white_24.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_fullscreen_exit_white_48.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_fullscreen_exit_white_48.png new file mode 100644 index 000000000..ef360fe40 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_fullscreen_exit_white_48.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_fullscreen_white_36.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_fullscreen_white_36.png new file mode 100644 index 000000000..9b8131124 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_fullscreen_white_36.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_fullscreen_white_48.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_fullscreen_white_48.png new file mode 100644 index 000000000..c1dcfb290 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_fullscreen_white_48.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_group_white_36.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_group_white_36.png new file mode 100644 index 000000000..6c68435fb Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_group_white_36.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_hd_white_24.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_hd_white_24.png new file mode 100644 index 000000000..4c954d86f Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_hd_white_24.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_headset_grey600_24.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_headset_grey600_24.png new file mode 100644 index 000000000..f7dbee156 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_headset_grey600_24.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_headset_white_36.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_headset_white_36.png new file mode 100644 index 000000000..82db5427b Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_headset_white_36.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_message_white_24.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_message_white_24.png new file mode 100644 index 000000000..763767b4f Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_message_white_24.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_mic_off_black_24.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_mic_off_black_24.png new file mode 100644 index 000000000..fa741be1c Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_mic_off_black_24.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_mic_off_white_36.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_mic_off_white_36.png new file mode 100644 index 000000000..7a15a9ea9 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_mic_off_white_36.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_network_wifi_white_24.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_network_wifi_white_24.png new file mode 100644 index 000000000..ca927f3de Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_network_wifi_white_24.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_pause_white_36.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_pause_white_36.png new file mode 100644 index 000000000..7192ad487 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_pause_white_36.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_person_add_white_24.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_person_add_white_24.png new file mode 100644 index 000000000..7e7c289d4 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_person_add_white_24.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_photo_library_white_24.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_photo_library_white_24.png new file mode 100644 index 000000000..4bd2898a8 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_photo_library_white_24.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_photo_white_24.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_photo_white_24.png new file mode 100644 index 000000000..2642b9e09 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_photo_white_24.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_photo_white_48.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_photo_white_48.png new file mode 100644 index 000000000..2ffdb55f2 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_photo_white_48.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_report_white_18.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_report_white_18.png new file mode 100644 index 000000000..dc0c995c1 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_report_white_18.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_send_black_24.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_send_black_24.png new file mode 100644 index 000000000..fd36be8b9 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_send_black_24.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_send_white_24.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_send_white_24.png new file mode 100644 index 000000000..ef59e7767 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_send_white_24.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_swap_calls_white_36.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_swap_calls_white_36.png new file mode 100644 index 000000000..698cd5d75 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_swap_calls_white_36.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_switch_camera_white_36.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_switch_camera_white_36.png new file mode 100644 index 000000000..bee95a1d4 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_switch_camera_white_36.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_switch_video_white_36.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_switch_video_white_36.png new file mode 100644 index 000000000..1a7423f5f Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_switch_video_white_36.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_undo_white_48.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_undo_white_48.png new file mode 100644 index 000000000..a5e719cdf Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_undo_white_48.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_videocam_off_white_24.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_videocam_off_white_24.png new file mode 100644 index 000000000..5d540589b Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_videocam_off_white_24.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_videocam_off_white_36.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_videocam_off_white_36.png new file mode 100644 index 000000000..69565f2c7 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_videocam_off_white_36.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_videocam_white_18.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_videocam_white_18.png new file mode 100644 index 000000000..d83e0d50c Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_videocam_white_18.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_videocam_white_24.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_videocam_white_24.png new file mode 100644 index 000000000..1b2583d34 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_videocam_white_24.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_videocam_white_36.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_videocam_white_36.png new file mode 100644 index 000000000..44c28e2f2 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_videocam_white_36.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_voicemail_white_18.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_voicemail_white_18.png new file mode 100644 index 000000000..03a62e15f Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_voicemail_white_18.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_volume_up_grey600_24.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_volume_up_grey600_24.png new file mode 100644 index 000000000..a45093ff7 Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_volume_up_grey600_24.png differ diff --git a/assets/quantum/res/drawable-xhdpi/quantum_ic_volume_up_white_36.png b/assets/quantum/res/drawable-xhdpi/quantum_ic_volume_up_white_36.png new file mode 100644 index 000000000..2e751a40f Binary files /dev/null and b/assets/quantum/res/drawable-xhdpi/quantum_ic_volume_up_white_36.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_arrow_back_white_24.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_arrow_back_white_24.png new file mode 100644 index 000000000..32a6d91ce Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_arrow_back_white_24.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_arrow_drop_down_white_18.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_arrow_drop_down_white_18.png new file mode 100644 index 000000000..2609ae134 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_arrow_drop_down_white_18.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_bluetooth_audio_grey600_24.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_bluetooth_audio_grey600_24.png new file mode 100644 index 000000000..99f57c12a Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_bluetooth_audio_grey600_24.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_bluetooth_audio_white_36.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_bluetooth_audio_white_36.png new file mode 100644 index 000000000..6842da6d0 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_bluetooth_audio_white_36.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_call_end_white_24.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_call_end_white_24.png new file mode 100644 index 000000000..e1831d7af Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_call_end_white_24.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_call_end_white_36.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_call_end_white_36.png new file mode 100644 index 000000000..13ffc2ad7 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_call_end_white_36.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_call_merge_white_36.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_call_merge_white_36.png new file mode 100644 index 000000000..cefef6551 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_call_merge_white_36.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_call_white_18.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_call_white_18.png new file mode 100644 index 000000000..6f4dcea1f Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_call_white_18.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_call_white_24.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_call_white_24.png new file mode 100644 index 000000000..90ead2e45 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_call_white_24.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_camera_alt_white_24.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_camera_alt_white_24.png new file mode 100644 index 000000000..c8e69dceb Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_camera_alt_white_24.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_camera_alt_white_48.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_camera_alt_white_48.png new file mode 100644 index 000000000..a4e7aea72 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_camera_alt_white_48.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_camera_front_white_36.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_camera_front_white_36.png new file mode 100644 index 000000000..115cdb72b Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_camera_front_white_36.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_camera_rear_white_36.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_camera_rear_white_36.png new file mode 100644 index 000000000..087508fdb Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_camera_rear_white_36.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_check_black_24.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_check_black_24.png new file mode 100644 index 000000000..b26a2c05e Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_check_black_24.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_check_black_36.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_check_black_36.png new file mode 100644 index 000000000..1e6101fd9 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_check_black_36.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_check_white_48.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_check_white_48.png new file mode 100644 index 000000000..bfd7b82aa Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_check_white_48.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_close_white_24.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_close_white_24.png new file mode 100644 index 000000000..6b717e0dd Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_close_white_24.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_close_white_36.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_close_white_36.png new file mode 100644 index 000000000..3b28ea4b0 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_close_white_36.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_dialpad_white_36.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_dialpad_white_36.png new file mode 100644 index 000000000..54ebbafae Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_dialpad_white_36.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_forward_white_24.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_forward_white_24.png new file mode 100644 index 000000000..7bd5b1635 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_forward_white_24.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_fullscreen_exit_white_48.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_fullscreen_exit_white_48.png new file mode 100644 index 000000000..b7f4133fd Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_fullscreen_exit_white_48.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_fullscreen_white_36.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_fullscreen_white_36.png new file mode 100644 index 000000000..ca9135b49 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_fullscreen_white_36.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_fullscreen_white_48.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_fullscreen_white_48.png new file mode 100644 index 000000000..a0a1b4d4f Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_fullscreen_white_48.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_group_white_36.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_group_white_36.png new file mode 100644 index 000000000..57f87aec0 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_group_white_36.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_hd_white_24.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_hd_white_24.png new file mode 100644 index 000000000..dd08bbbec Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_hd_white_24.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_headset_grey600_24.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_headset_grey600_24.png new file mode 100644 index 000000000..de1739bf4 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_headset_grey600_24.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_headset_white_36.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_headset_white_36.png new file mode 100644 index 000000000..a0d8b14c0 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_headset_white_36.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_message_white_24.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_message_white_24.png new file mode 100644 index 000000000..0a79824b8 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_message_white_24.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_mic_off_black_24.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_mic_off_black_24.png new file mode 100644 index 000000000..084bf3c9f Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_mic_off_black_24.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_mic_off_white_36.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_mic_off_white_36.png new file mode 100644 index 000000000..585d38326 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_mic_off_white_36.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_network_wifi_white_24.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_network_wifi_white_24.png new file mode 100644 index 000000000..75469cd85 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_network_wifi_white_24.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_pause_white_36.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_pause_white_36.png new file mode 100644 index 000000000..a03bad27e Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_pause_white_36.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_person_add_white_24.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_person_add_white_24.png new file mode 100644 index 000000000..8f744f039 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_person_add_white_24.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_photo_library_white_24.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_photo_library_white_24.png new file mode 100644 index 000000000..497479291 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_photo_library_white_24.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_photo_white_24.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_photo_white_24.png new file mode 100644 index 000000000..f9f1defa6 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_photo_white_24.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_photo_white_48.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_photo_white_48.png new file mode 100644 index 000000000..3fe5c5ceb Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_photo_white_48.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_report_white_18.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_report_white_18.png new file mode 100644 index 000000000..919a872e0 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_report_white_18.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_send_black_24.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_send_black_24.png new file mode 100644 index 000000000..7caa9db4a Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_send_black_24.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_send_white_24.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_send_white_24.png new file mode 100644 index 000000000..0c5256413 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_send_white_24.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_swap_calls_white_36.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_swap_calls_white_36.png new file mode 100644 index 000000000..140da28a8 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_swap_calls_white_36.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_switch_camera_white_36.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_switch_camera_white_36.png new file mode 100644 index 000000000..52b298e0d Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_switch_camera_white_36.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_switch_video_white_36.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_switch_video_white_36.png new file mode 100644 index 000000000..0e0f3c26f Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_switch_video_white_36.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_undo_white_48.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_undo_white_48.png new file mode 100644 index 000000000..8745f69ff Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_undo_white_48.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_videocam_off_white_24.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_videocam_off_white_24.png new file mode 100644 index 000000000..69565f2c7 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_videocam_off_white_24.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_videocam_off_white_36.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_videocam_off_white_36.png new file mode 100644 index 000000000..ff8483295 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_videocam_off_white_36.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_videocam_white_18.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_videocam_white_18.png new file mode 100644 index 000000000..49562a640 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_videocam_white_18.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_videocam_white_24.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_videocam_white_24.png new file mode 100644 index 000000000..44c28e2f2 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_videocam_white_24.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_videocam_white_36.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_videocam_white_36.png new file mode 100644 index 000000000..839af26f8 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_videocam_white_36.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_voicemail_white_18.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_voicemail_white_18.png new file mode 100644 index 000000000..d839f2167 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_voicemail_white_18.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_volume_up_grey600_24.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_volume_up_grey600_24.png new file mode 100644 index 000000000..413b38652 Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_volume_up_grey600_24.png differ diff --git a/assets/quantum/res/drawable-xxhdpi/quantum_ic_volume_up_white_36.png b/assets/quantum/res/drawable-xxhdpi/quantum_ic_volume_up_white_36.png new file mode 100644 index 000000000..96c1f982f Binary files /dev/null and b/assets/quantum/res/drawable-xxhdpi/quantum_ic_volume_up_white_36.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_arrow_back_white_24.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_arrow_back_white_24.png new file mode 100644 index 000000000..e27034d67 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_arrow_back_white_24.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_arrow_drop_down_white_18.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_arrow_drop_down_white_18.png new file mode 100644 index 000000000..c19c19d2b Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_arrow_drop_down_white_18.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_bluetooth_audio_grey600_24.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_bluetooth_audio_grey600_24.png new file mode 100644 index 000000000..1595be169 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_bluetooth_audio_grey600_24.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_bluetooth_audio_white_36.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_bluetooth_audio_white_36.png new file mode 100644 index 000000000..3fe7c2350 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_bluetooth_audio_white_36.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_call_end_white_24.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_call_end_white_24.png new file mode 100644 index 000000000..8801d0ded Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_call_end_white_24.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_call_end_white_36.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_call_end_white_36.png new file mode 100644 index 000000000..c8099a1a1 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_call_end_white_36.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_call_merge_white_36.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_call_merge_white_36.png new file mode 100644 index 000000000..9419ffbbc Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_call_merge_white_36.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_call_white_18.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_call_white_18.png new file mode 100644 index 000000000..90ead2e45 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_call_white_18.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_call_white_24.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_call_white_24.png new file mode 100644 index 000000000..b0e020573 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_call_white_24.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_camera_alt_white_24.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_camera_alt_white_24.png new file mode 100644 index 000000000..777658e95 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_camera_alt_white_24.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_camera_alt_white_48.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_camera_alt_white_48.png new file mode 100644 index 000000000..f2fe54bd5 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_camera_alt_white_48.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_camera_front_white_36.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_camera_front_white_36.png new file mode 100644 index 000000000..9f43e2073 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_camera_front_white_36.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_camera_rear_white_36.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_camera_rear_white_36.png new file mode 100644 index 000000000..e3cac8e74 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_camera_rear_white_36.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_check_black_24.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_check_black_24.png new file mode 100644 index 000000000..2f6d6386d Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_check_black_24.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_check_black_36.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_check_black_36.png new file mode 100644 index 000000000..5697dba95 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_check_black_36.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_check_white_48.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_check_white_48.png new file mode 100644 index 000000000..23a197082 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_check_white_48.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_close_white_24.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_close_white_24.png new file mode 100644 index 000000000..396419219 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_close_white_24.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_close_white_36.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_close_white_36.png new file mode 100644 index 000000000..4927bc242 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_close_white_36.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_dialpad_white_36.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_dialpad_white_36.png new file mode 100644 index 000000000..a53aeb1d3 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_dialpad_white_36.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_forward_white_24.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_forward_white_24.png new file mode 100644 index 000000000..428009cfe Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_forward_white_24.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_fullscreen_exit_white_48.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_fullscreen_exit_white_48.png new file mode 100644 index 000000000..b47b3f8bd Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_fullscreen_exit_white_48.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_fullscreen_white_36.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_fullscreen_white_36.png new file mode 100644 index 000000000..a0a1b4d4f Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_fullscreen_white_36.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_fullscreen_white_48.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_fullscreen_white_48.png new file mode 100644 index 000000000..ea9f18ae6 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_fullscreen_white_48.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_group_white_36.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_group_white_36.png new file mode 100644 index 000000000..9ec120f5f Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_group_white_36.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_hd_white_24.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_hd_white_24.png new file mode 100644 index 000000000..3f87b882e Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_hd_white_24.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_headset_grey600_24.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_headset_grey600_24.png new file mode 100644 index 000000000..e968fa7d1 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_headset_grey600_24.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_headset_white_36.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_headset_white_36.png new file mode 100644 index 000000000..89b991047 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_headset_white_36.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_message_white_24.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_message_white_24.png new file mode 100644 index 000000000..fa7c17ac4 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_message_white_24.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_mic_off_black_24.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_mic_off_black_24.png new file mode 100644 index 000000000..90d0606a4 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_mic_off_black_24.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_mic_off_white_36.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_mic_off_white_36.png new file mode 100644 index 000000000..b0a10fbf6 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_mic_off_white_36.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_network_wifi_white_24.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_network_wifi_white_24.png new file mode 100644 index 000000000..eb284e383 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_network_wifi_white_24.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_pause_white_36.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_pause_white_36.png new file mode 100644 index 000000000..3ea7e03e5 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_pause_white_36.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_person_add_white_24.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_person_add_white_24.png new file mode 100644 index 000000000..2fa2cca80 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_person_add_white_24.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_photo_library_white_24.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_photo_library_white_24.png new file mode 100644 index 000000000..8627f4276 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_photo_library_white_24.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_photo_white_24.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_photo_white_24.png new file mode 100644 index 000000000..2ffdb55f2 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_photo_white_24.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_photo_white_48.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_photo_white_48.png new file mode 100644 index 000000000..7d5091ded Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_photo_white_48.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_report_white_18.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_report_white_18.png new file mode 100644 index 000000000..aed766804 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_report_white_18.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_send_black_24.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_send_black_24.png new file mode 100644 index 000000000..b1a0bce48 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_send_black_24.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_send_white_24.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_send_white_24.png new file mode 100644 index 000000000..9dfa888c1 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_send_white_24.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_swap_calls_white_36.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_swap_calls_white_36.png new file mode 100644 index 000000000..f8470b5dc Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_swap_calls_white_36.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_switch_camera_white_36.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_switch_camera_white_36.png new file mode 100644 index 000000000..8cbb600a4 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_switch_camera_white_36.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_switch_video_white_36.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_switch_video_white_36.png new file mode 100644 index 000000000..854e78301 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_switch_video_white_36.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_undo_white_48.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_undo_white_48.png new file mode 100644 index 000000000..6d703c6ae Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_undo_white_48.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_videocam_off_white_24.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_videocam_off_white_24.png new file mode 100644 index 000000000..bf37b57f9 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_videocam_off_white_24.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_videocam_off_white_36.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_videocam_off_white_36.png new file mode 100644 index 000000000..7a915c30d Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_videocam_off_white_36.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_videocam_white_18.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_videocam_white_18.png new file mode 100644 index 000000000..44c28e2f2 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_videocam_white_18.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_videocam_white_24.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_videocam_white_24.png new file mode 100644 index 000000000..ed20c0706 Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_videocam_white_24.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_videocam_white_36.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_videocam_white_36.png new file mode 100644 index 000000000..eff5923da Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_videocam_white_36.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_voicemail_white_18.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_voicemail_white_18.png new file mode 100644 index 000000000..28b8e936a Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_voicemail_white_18.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_volume_up_grey600_24.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_volume_up_grey600_24.png new file mode 100644 index 000000000..429dc02df Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_volume_up_grey600_24.png differ diff --git a/assets/quantum/res/drawable-xxxhdpi/quantum_ic_volume_up_white_36.png b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_volume_up_white_36.png new file mode 100644 index 000000000..fd633b6cb Binary files /dev/null and b/assets/quantum/res/drawable-xxxhdpi/quantum_ic_volume_up_white_36.png differ diff --git a/build-app.gradle b/build-app.gradle deleted file mode 100644 index 2ea437619..000000000 --- a/build-app.gradle +++ /dev/null @@ -1,39 +0,0 @@ -apply plugin: 'com.android.application' - -android { - defaultConfig { - minSdkVersion 23 - targetSdkVersion 23 - multiDexEnabled true - } - - sourceSets.main { - java.srcDirs = ['src', 'src-pre-N', 'InCallUI/src'] - manifest.srcFile 'AndroidManifest.xml' - res.srcDirs = ['res'] - } - - sourceSets.androidTest { - java.srcDirs = ['tests/src'] - res.srcDirs = ['test/res'] - } -} - -dependencies { - compile 'com.android.support:support-v4:23.1.+' - compile 'com.android.support:support-v13:23.1.+' - compile 'com.android.support:appcompat-v7:23.1.+' - compile 'com.android.support:cardview-v7:23.1.+' - compile 'com.android.support:design:23.1.+' - compile 'com.android.support:recyclerview-v7:23.1.+' - - compile project(':android-common') - compile project(':guava') - compile project(':libphonenumber') - compile project(':jsr305') - compile project(':vcard') - - compile project(':contactscommon') - compile project(':incallui') - compile project(':phonecommon') -} diff --git a/build-library.gradle b/build-library.gradle deleted file mode 100644 index a2394aac8..000000000 --- a/build-library.gradle +++ /dev/null @@ -1,39 +0,0 @@ -apply plugin: 'com.android.library' - -android { - defaultConfig { - minSdkVersion 23 - targetSdkVersion 23 - multiDexEnabled true - } - - sourceSets.main { - java.srcDirs = ['src', 'src-pre-N', 'InCallUI/src'] - manifest.srcFile 'AndroidManifest.xml' - res.srcDirs = ['res'] - } - - sourceSets.androidTest { - java.srcDirs = ['tests/src'] - res.srcDirs = ['test/res'] - } -} - -dependencies { - compile 'com.android.support:support-v4:23.1.+' - compile 'com.android.support:support-v13:23.1.+' - compile 'com.android.support:appcompat-v7:23.1.+' - compile 'com.android.support:cardview-v7:23.1.+' - compile 'com.android.support:design:23.1.+' - compile 'com.android.support:recyclerview-v7:23.1.+' - - compile project(':android-common') - compile project(':guava') - compile project(':libphonenumber') - compile project(':jsr305') - compile project(':vcard') - - compile project(':contactscommon') - compile project(':incallui') - compile project(':phonecommon') -} diff --git a/java/com/android/contacts/common/AndroidManifest.xml b/java/com/android/contacts/common/AndroidManifest.xml new file mode 100644 index 000000000..eae70cd30 --- /dev/null +++ b/java/com/android/contacts/common/AndroidManifest.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/java/com/android/contacts/common/Bindings.java b/java/com/android/contacts/common/Bindings.java new file mode 100644 index 000000000..29cf7950a --- /dev/null +++ b/java/com/android/contacts/common/Bindings.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common; + +import android.content.Context; +import com.android.contacts.common.bindings.ContactsCommonBindings; +import com.android.contacts.common.bindings.ContactsCommonBindingsFactory; +import com.android.contacts.common.bindings.ContactsCommonBindingsStub; +import java.util.Objects; + +/** Accessor for the contacts common bindings. */ +public class Bindings { + + private static ContactsCommonBindings instance; + + private Bindings() {} + + public static ContactsCommonBindings get(Context context) { + Objects.requireNonNull(context); + if (instance != null) { + return instance; + } + + Context application = context.getApplicationContext(); + if (application instanceof ContactsCommonBindingsFactory) { + instance = ((ContactsCommonBindingsFactory) application).newContactsCommonBindings(); + } + + if (instance == null) { + instance = new ContactsCommonBindingsStub(); + } + return instance; + } + + public static void setForTesting(ContactsCommonBindings testInstance) { + instance = testInstance; + } +} diff --git a/java/com/android/contacts/common/ClipboardUtils.java b/java/com/android/contacts/common/ClipboardUtils.java new file mode 100644 index 000000000..9345b0f9c --- /dev/null +++ b/java/com/android/contacts/common/ClipboardUtils.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.contacts.common; + +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.text.TextUtils; +import android.widget.Toast; + +public class ClipboardUtils { + + private static final String TAG = "ClipboardUtils"; + + private ClipboardUtils() {} + + /** + * Copy a text to clipboard. + * + * @param context Context + * @param label Label to show to the user describing this clip. + * @param text Text to copy. + * @param showToast If {@code true}, a toast is shown to the user. + */ + public static void copyText( + Context context, CharSequence label, CharSequence text, boolean showToast) { + if (TextUtils.isEmpty(text)) { + return; + } + + ClipboardManager clipboardManager = + (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clipData = ClipData.newPlainText(label == null ? "" : label, text); + clipboardManager.setPrimaryClip(clipData); + + if (showToast) { + String toastText = context.getString(R.string.toast_text_copied); + Toast.makeText(context, toastText, Toast.LENGTH_SHORT).show(); + } + } +} diff --git a/java/com/android/contacts/common/Collapser.java b/java/com/android/contacts/common/Collapser.java new file mode 100644 index 000000000..0b5c48bf2 --- /dev/null +++ b/java/com/android/contacts/common/Collapser.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common; + +import android.content.Context; +import java.util.Iterator; +import java.util.List; + +/** + * Class used for collapsing data items into groups of similar items. The data items that should be + * collapsible should implement the Collapsible interface. The class also contains a utility + * function that takes an ArrayList of items and returns a list of the same items collapsed into + * groups. + */ +public final class Collapser { + + /* + * The Collapser uses an n^2 algorithm so we don't want it to run on + * lists beyond a certain size. This specifies the maximum size to collapse. + */ + private static final int MAX_LISTSIZE_TO_COLLAPSE = 20; + + /* + * This utility class cannot be instantiated. + */ + private Collapser() {} + + /** + * Collapses a list of Collapsible items into a list of collapsed items. Items are collapsed if + * {@link Collapsible#shouldCollapseWith(Object)} returns true, and are collapsed through the + * {@Link Collapsible#collapseWith(Object)} function implemented by the data item. + * + * @param list List of Objects of type > to be collapsed. + */ + public static > void collapseList(List list, Context context) { + + int listSize = list.size(); + // The algorithm below is n^2 so don't run on long lists + if (listSize > MAX_LISTSIZE_TO_COLLAPSE) { + return; + } + + for (int i = 0; i < listSize; i++) { + T iItem = list.get(i); + if (iItem != null) { + for (int j = i + 1; j < listSize; j++) { + T jItem = list.get(j); + if (jItem != null) { + if (iItem.shouldCollapseWith(jItem, context)) { + iItem.collapseWith(jItem); + list.set(j, null); + } else if (jItem.shouldCollapseWith(iItem, context)) { + jItem.collapseWith(iItem); + list.set(i, null); + break; + } + } + } + } + } + + // Remove the null items + Iterator itr = list.iterator(); + while (itr.hasNext()) { + if (itr.next() == null) { + itr.remove(); + } + } + } + + /* + * Interface implemented by data types that can be collapsed into groups of similar data. This + * can be used for example to collapse similar contact data items into a single item. + */ + public interface Collapsible { + + void collapseWith(T t); + + boolean shouldCollapseWith(T t, Context context); + } +} diff --git a/java/com/android/contacts/common/ContactPhotoManager.java b/java/com/android/contacts/common/ContactPhotoManager.java new file mode 100644 index 000000000..834471047 --- /dev/null +++ b/java/com/android/contacts/common/ContactPhotoManager.java @@ -0,0 +1,487 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common; + +import android.content.ComponentCallbacks2; +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.net.Uri.Builder; +import android.support.annotation.VisibleForTesting; +import android.text.TextUtils; +import android.view.View; +import android.widget.ImageView; +import com.android.contacts.common.lettertiles.LetterTileDrawable; +import com.android.dialer.common.LogUtil; +import com.android.dialer.util.PermissionsUtil; + +/** Asynchronously loads contact photos and maintains a cache of photos. */ +public abstract class ContactPhotoManager implements ComponentCallbacks2 { + + /** Contact type constants used for default letter images */ + public static final int TYPE_PERSON = LetterTileDrawable.TYPE_PERSON; + + public static final int TYPE_BUSINESS = LetterTileDrawable.TYPE_BUSINESS; + public static final int TYPE_VOICEMAIL = LetterTileDrawable.TYPE_VOICEMAIL; + public static final int TYPE_DEFAULT = LetterTileDrawable.TYPE_DEFAULT; + /** Scale and offset default constants used for default letter images */ + public static final float SCALE_DEFAULT = 1.0f; + + public static final float OFFSET_DEFAULT = 0.0f; + public static final boolean IS_CIRCULAR_DEFAULT = false; + // TODO: Use LogUtil.isVerboseEnabled for DEBUG branches instead of a lint check. + // LINT.DoNotSubmitIf(true) + static final boolean DEBUG = false; + // LINT.DoNotSubmitIf(true) + static final boolean DEBUG_SIZES = false; + /** Uri-related constants used for default letter images */ + private static final String DISPLAY_NAME_PARAM_KEY = "display_name"; + + private static final String IDENTIFIER_PARAM_KEY = "identifier"; + private static final String CONTACT_TYPE_PARAM_KEY = "contact_type"; + private static final String SCALE_PARAM_KEY = "scale"; + private static final String OFFSET_PARAM_KEY = "offset"; + private static final String IS_CIRCULAR_PARAM_KEY = "is_circular"; + private static final String DEFAULT_IMAGE_URI_SCHEME = "defaultimage"; + private static final Uri DEFAULT_IMAGE_URI = Uri.parse(DEFAULT_IMAGE_URI_SCHEME + "://"); + public static final DefaultImageProvider DEFAULT_AVATAR = new LetterTileDefaultImageProvider(); + private static ContactPhotoManager sInstance; + + /** + * Given a {@link DefaultImageRequest}, returns an Uri that can be used to request a letter tile + * avatar when passed to the {@link ContactPhotoManager}. The internal implementation of this uri + * is not guaranteed to remain the same across application versions, so the actual uri should + * never be persisted in long-term storage and reused. + * + * @param request A {@link DefaultImageRequest} object with the fields configured to return a + * @return A Uri that when later passed to the {@link ContactPhotoManager} via {@link + * #loadPhoto(ImageView, Uri, int, boolean, boolean, DefaultImageRequest)}, can be used to + * request a default contact image, drawn as a letter tile using the parameters as configured + * in the provided {@link DefaultImageRequest} + */ + public static Uri getDefaultAvatarUriForContact(DefaultImageRequest request) { + final Builder builder = DEFAULT_IMAGE_URI.buildUpon(); + if (request != null) { + if (!TextUtils.isEmpty(request.displayName)) { + builder.appendQueryParameter(DISPLAY_NAME_PARAM_KEY, request.displayName); + } + if (!TextUtils.isEmpty(request.identifier)) { + builder.appendQueryParameter(IDENTIFIER_PARAM_KEY, request.identifier); + } + if (request.contactType != TYPE_DEFAULT) { + builder.appendQueryParameter(CONTACT_TYPE_PARAM_KEY, String.valueOf(request.contactType)); + } + if (request.scale != SCALE_DEFAULT) { + builder.appendQueryParameter(SCALE_PARAM_KEY, String.valueOf(request.scale)); + } + if (request.offset != OFFSET_DEFAULT) { + builder.appendQueryParameter(OFFSET_PARAM_KEY, String.valueOf(request.offset)); + } + if (request.isCircular != IS_CIRCULAR_DEFAULT) { + builder.appendQueryParameter(IS_CIRCULAR_PARAM_KEY, String.valueOf(request.isCircular)); + } + } + return builder.build(); + } + + /** + * Adds a business contact type encoded fragment to the URL. Used to ensure photo URLS from Nearby + * Places can be identified as business photo URLs rather than URLs for personal contact photos. + * + * @param photoUrl The photo URL to modify. + * @return URL with the contact type parameter added and set to TYPE_BUSINESS. + */ + public static String appendBusinessContactType(String photoUrl) { + Uri uri = Uri.parse(photoUrl); + Builder builder = uri.buildUpon(); + builder.encodedFragment(String.valueOf(TYPE_BUSINESS)); + return builder.build().toString(); + } + + /** + * Removes the contact type information stored in the photo URI encoded fragment. + * + * @param photoUri The photo URI to remove the contact type from. + * @return The photo URI with contact type removed. + */ + public static Uri removeContactType(Uri photoUri) { + String encodedFragment = photoUri.getEncodedFragment(); + if (!TextUtils.isEmpty(encodedFragment)) { + Builder builder = photoUri.buildUpon(); + builder.encodedFragment(null); + return builder.build(); + } + return photoUri; + } + + /** + * Inspects a photo URI to determine if the photo URI represents a business. + * + * @param photoUri The URI to inspect. + * @return Whether the URI represents a business photo or not. + */ + public static boolean isBusinessContactUri(Uri photoUri) { + if (photoUri == null) { + return false; + } + + String encodedFragment = photoUri.getEncodedFragment(); + return !TextUtils.isEmpty(encodedFragment) + && encodedFragment.equals(String.valueOf(TYPE_BUSINESS)); + } + + protected static DefaultImageRequest getDefaultImageRequestFromUri(Uri uri) { + final DefaultImageRequest request = + new DefaultImageRequest( + uri.getQueryParameter(DISPLAY_NAME_PARAM_KEY), + uri.getQueryParameter(IDENTIFIER_PARAM_KEY), + false); + try { + String contactType = uri.getQueryParameter(CONTACT_TYPE_PARAM_KEY); + if (!TextUtils.isEmpty(contactType)) { + request.contactType = Integer.valueOf(contactType); + } + + String scale = uri.getQueryParameter(SCALE_PARAM_KEY); + if (!TextUtils.isEmpty(scale)) { + request.scale = Float.valueOf(scale); + } + + String offset = uri.getQueryParameter(OFFSET_PARAM_KEY); + if (!TextUtils.isEmpty(offset)) { + request.offset = Float.valueOf(offset); + } + + String isCircular = uri.getQueryParameter(IS_CIRCULAR_PARAM_KEY); + if (!TextUtils.isEmpty(isCircular)) { + request.isCircular = Boolean.valueOf(isCircular); + } + } catch (NumberFormatException e) { + LogUtil.w( + "ContactPhotoManager.getDefaultImageRequestFromUri", + "Invalid DefaultImageRequest image parameters provided, ignoring and using " + + "defaults."); + } + + return request; + } + + public static ContactPhotoManager getInstance(Context context) { + if (sInstance == null) { + Context applicationContext = context.getApplicationContext(); + sInstance = createContactPhotoManager(applicationContext); + applicationContext.registerComponentCallbacks(sInstance); + if (PermissionsUtil.hasContactsPermissions(context)) { + sInstance.preloadPhotosInBackground(); + } + } + return sInstance; + } + + public static synchronized ContactPhotoManager createContactPhotoManager(Context context) { + return new ContactPhotoManagerImpl(context); + } + + @VisibleForTesting + public static void injectContactPhotoManagerForTesting(ContactPhotoManager photoManager) { + sInstance = photoManager; + } + + protected boolean isDefaultImageUri(Uri uri) { + return DEFAULT_IMAGE_URI_SCHEME.equals(uri.getScheme()); + } + + /** + * Load thumbnail image into the supplied image view. If the photo is already cached, it is + * displayed immediately. Otherwise a request is sent to load the photo from the database. + */ + public abstract void loadThumbnail( + ImageView view, + long photoId, + boolean darkTheme, + boolean isCircular, + DefaultImageRequest defaultImageRequest, + DefaultImageProvider defaultProvider); + + /** + * Calls {@link #loadThumbnail(ImageView, long, boolean, boolean, DefaultImageRequest, + * DefaultImageProvider)} using the {@link DefaultImageProvider} {@link #DEFAULT_AVATAR}. + */ + public final void loadThumbnail( + ImageView view, + long photoId, + boolean darkTheme, + boolean isCircular, + DefaultImageRequest defaultImageRequest) { + loadThumbnail(view, photoId, darkTheme, isCircular, defaultImageRequest, DEFAULT_AVATAR); + } + + /** + * Load photo into the supplied image view. If the photo is already cached, it is displayed + * immediately. Otherwise a request is sent to load the photo from the location specified by the + * URI. + * + * @param view The target view + * @param photoUri The uri of the photo to load + * @param requestedExtent Specifies an approximate Max(width, height) of the targetView. This is + * useful if the source image can be a lot bigger that the target, so that the decoding is + * done using efficient sampling. If requestedExtent is specified, no sampling of the image is + * performed + * @param darkTheme Whether the background is dark. This is used for default avatars + * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a default + * letter tile avatar should be drawn. + * @param defaultProvider The provider of default avatars (this is used if photoUri doesn't refer + * to an existing image) + */ + public abstract void loadPhoto( + ImageView view, + Uri photoUri, + int requestedExtent, + boolean darkTheme, + boolean isCircular, + DefaultImageRequest defaultImageRequest, + DefaultImageProvider defaultProvider); + + /** + * Calls {@link #loadPhoto(ImageView, Uri, int, boolean, boolean, DefaultImageRequest, + * DefaultImageProvider)} with {@link #DEFAULT_AVATAR} and {@code null} display names and lookup + * keys. + * + * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a default + * letter tile avatar should be drawn. + */ + public final void loadPhoto( + ImageView view, + Uri photoUri, + int requestedExtent, + boolean darkTheme, + boolean isCircular, + DefaultImageRequest defaultImageRequest) { + loadPhoto( + view, + photoUri, + requestedExtent, + darkTheme, + isCircular, + defaultImageRequest, + DEFAULT_AVATAR); + } + + /** + * Calls {@link #loadPhoto(ImageView, Uri, int, boolean, boolean, DefaultImageRequest, + * DefaultImageProvider)} with {@link #DEFAULT_AVATAR} and with the assumption, that the image is + * a thumbnail. + * + * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a default + * letter tile avatar should be drawn. + */ + public final void loadDirectoryPhoto( + ImageView view, + Uri photoUri, + boolean darkTheme, + boolean isCircular, + DefaultImageRequest defaultImageRequest) { + loadPhoto(view, photoUri, -1, darkTheme, isCircular, defaultImageRequest, DEFAULT_AVATAR); + } + + /** + * Remove photo from the supplied image view. This also cancels current pending load request + * inside this photo manager. + */ + public abstract void removePhoto(ImageView view); + + /** Cancels all pending requests to load photos asynchronously. */ + public abstract void cancelPendingRequests(View fragmentRootView); + + /** Temporarily stops loading photos from the database. */ + public abstract void pause(); + + /** Resumes loading photos from the database. */ + public abstract void resume(); + + /** + * Marks all cached photos for reloading. We can continue using cache but should also make sure + * the photos haven't changed in the background and notify the views if so. + */ + public abstract void refreshCache(); + + /** Initiates a background process that over time will fill up cache with preload photos. */ + public abstract void preloadPhotosInBackground(); + + // ComponentCallbacks2 + @Override + public void onConfigurationChanged(Configuration newConfig) {} + + // ComponentCallbacks2 + @Override + public void onLowMemory() {} + + // ComponentCallbacks2 + @Override + public void onTrimMemory(int level) {} + + /** + * Contains fields used to contain contact details and other user-defined settings that might be + * used by the ContactPhotoManager to generate a default contact image. This contact image takes + * the form of a letter or bitmap drawn on top of a colored tile. + */ + public static class DefaultImageRequest { + + /** + * Used to indicate that a drawable that represents a contact without any contact details should + * be returned. + */ + public static final DefaultImageRequest EMPTY_DEFAULT_IMAGE_REQUEST = new DefaultImageRequest(); + /** + * Used to indicate that a drawable that represents a business without a business photo should + * be returned. + */ + public static final DefaultImageRequest EMPTY_DEFAULT_BUSINESS_IMAGE_REQUEST = + new DefaultImageRequest(null, null, TYPE_BUSINESS, false); + /** + * Used to indicate that a circular drawable that represents a contact without any contact + * details should be returned. + */ + public static final DefaultImageRequest EMPTY_CIRCULAR_DEFAULT_IMAGE_REQUEST = + new DefaultImageRequest(null, null, true); + /** + * Used to indicate that a circular drawable that represents a business without a business photo + * should be returned. + */ + public static final DefaultImageRequest EMPTY_CIRCULAR_BUSINESS_IMAGE_REQUEST = + new DefaultImageRequest(null, null, TYPE_BUSINESS, true); + /** The contact's display name. The display name is used to */ + public String displayName; + /** + * A unique and deterministic string that can be used to identify this contact. This is usually + * the contact's lookup key, but other contact details can be used as well, especially for + * non-local or temporary contacts that might not have a lookup key. This is used to determine + * the color of the tile. + */ + public String identifier; + /** + * The type of this contact. This contact type may be used to decide the kind of image to use in + * the case where a unique letter cannot be generated from the contact's display name and + * identifier. See: {@link #TYPE_PERSON} {@link #TYPE_BUSINESS} {@link #TYPE_PERSON} {@link + * #TYPE_DEFAULT} + */ + public int contactType = TYPE_DEFAULT; + /** + * The amount to scale the letter or bitmap to, as a ratio of its default size (from a range of + * 0.0f to 2.0f). The default value is 1.0f. + */ + public float scale = SCALE_DEFAULT; + /** + * The amount to vertically offset the letter or image to within the tile. The provided offset + * must be within the range of -0.5f to 0.5f. If set to -0.5f, the letter will be shifted + * upwards by 0.5 times the height of the canvas it is being drawn on, which means it will be + * drawn with the center of the letter starting at the top edge of the canvas. If set to 0.5f, + * the letter will be shifted downwards by 0.5 times the height of the canvas it is being drawn + * on, which means it will be drawn with the center of the letter starting at the bottom edge of + * the canvas. The default is 0.0f, which means the letter is drawn in the exact vertical center + * of the tile. + */ + public float offset = OFFSET_DEFAULT; + /** Whether or not to draw the default image as a circle, instead of as a square/rectangle. */ + public boolean isCircular = false; + + public DefaultImageRequest() {} + + public DefaultImageRequest(String displayName, String identifier, boolean isCircular) { + this(displayName, identifier, TYPE_DEFAULT, SCALE_DEFAULT, OFFSET_DEFAULT, isCircular); + } + + public DefaultImageRequest( + String displayName, String identifier, int contactType, boolean isCircular) { + this(displayName, identifier, contactType, SCALE_DEFAULT, OFFSET_DEFAULT, isCircular); + } + + public DefaultImageRequest( + String displayName, + String identifier, + int contactType, + float scale, + float offset, + boolean isCircular) { + this.displayName = displayName; + this.identifier = identifier; + this.contactType = contactType; + this.scale = scale; + this.offset = offset; + this.isCircular = isCircular; + } + } + + public abstract static class DefaultImageProvider { + + /** + * Applies the default avatar to the ImageView. Extent is an indicator for the size (width or + * height). If darkTheme is set, the avatar is one that looks better on dark background + * + * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a default + * letter tile avatar should be drawn. + */ + public abstract void applyDefaultImage( + ImageView view, int extent, boolean darkTheme, DefaultImageRequest defaultImageRequest); + } + + /** + * A default image provider that applies a letter tile consisting of a colored background and a + * letter in the foreground as the default image for a contact. The color of the background and + * the type of letter is decided based on the contact's details. + */ + private static class LetterTileDefaultImageProvider extends DefaultImageProvider { + + public static Drawable getDefaultImageForContact( + Resources resources, DefaultImageRequest defaultImageRequest) { + final LetterTileDrawable drawable = new LetterTileDrawable(resources); + final int tileShape = + defaultImageRequest.isCircular + ? LetterTileDrawable.SHAPE_CIRCLE + : LetterTileDrawable.SHAPE_RECTANGLE; + if (defaultImageRequest != null) { + // If the contact identifier is null or empty, fallback to the + // displayName. In that case, use {@code null} for the contact's + // display name so that a default bitmap will be used instead of a + // letter + if (TextUtils.isEmpty(defaultImageRequest.identifier)) { + drawable.setCanonicalDialerLetterTileDetails( + null, defaultImageRequest.displayName, tileShape, defaultImageRequest.contactType); + } else { + drawable.setCanonicalDialerLetterTileDetails( + defaultImageRequest.displayName, + defaultImageRequest.identifier, + tileShape, + defaultImageRequest.contactType); + } + drawable.setScale(defaultImageRequest.scale); + drawable.setOffset(defaultImageRequest.offset); + } + return drawable; + } + + @Override + public void applyDefaultImage( + ImageView view, int extent, boolean darkTheme, DefaultImageRequest defaultImageRequest) { + final Drawable drawable = getDefaultImageForContact(view.getResources(), defaultImageRequest); + view.setImageDrawable(drawable); + } + } +} + diff --git a/java/com/android/contacts/common/ContactPhotoManagerImpl.java b/java/com/android/contacts/common/ContactPhotoManagerImpl.java new file mode 100644 index 000000000..2e6ff9fdc --- /dev/null +++ b/java/com/android/contacts/common/ContactPhotoManagerImpl.java @@ -0,0 +1,1262 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common; + +import android.app.ActivityManager; +import android.content.ComponentCallbacks2; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.Context; +import android.content.res.Resources; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.TransitionDrawable; +import android.media.ThumbnailUtils; +import android.net.TrafficStats; +import android.net.Uri; +import android.os.Handler; +import android.os.Handler.Callback; +import android.os.HandlerThread; +import android.os.Message; +import android.provider.ContactsContract; +import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.Contacts.Photo; +import android.provider.ContactsContract.Data; +import android.provider.ContactsContract.Directory; +import android.support.annotation.UiThread; +import android.support.annotation.WorkerThread; +import android.support.v4.graphics.drawable.RoundedBitmapDrawable; +import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory; +import android.text.TextUtils; +import android.util.LruCache; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import com.android.contacts.common.util.BitmapUtil; +import com.android.contacts.common.util.TrafficStatsTags; +import com.android.contacts.common.util.UriUtils; +import com.android.dialer.common.LogUtil; +import com.android.dialer.util.PermissionsUtil; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.ref.Reference; +import java.lang.ref.SoftReference; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map.Entry; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback { + + private static final String LOADER_THREAD_NAME = "ContactPhotoLoader"; + + private static final int FADE_TRANSITION_DURATION = 200; + + /** + * Type of message sent by the UI thread to itself to indicate that some photos need to be loaded. + */ + private static final int MESSAGE_REQUEST_LOADING = 1; + + /** Type of message sent by the loader thread to indicate that some photos have been loaded. */ + private static final int MESSAGE_PHOTOS_LOADED = 2; + + private static final String[] EMPTY_STRING_ARRAY = new String[0]; + + private static final String[] COLUMNS = new String[] {Photo._ID, Photo.PHOTO}; + + /** + * Dummy object used to indicate that a bitmap for a given key could not be stored in the cache. + */ + private static final BitmapHolder BITMAP_UNAVAILABLE; + /** Cache size for {@link #mBitmapHolderCache} for devices with "large" RAM. */ + private static final int HOLDER_CACHE_SIZE = 2000000; + /** Cache size for {@link #mBitmapCache} for devices with "large" RAM. */ + private static final int BITMAP_CACHE_SIZE = 36864 * 48; // 1728K + /** Height/width of a thumbnail image */ + private static int mThumbnailSize; + + static { + BITMAP_UNAVAILABLE = new BitmapHolder(new byte[0], 0); + BITMAP_UNAVAILABLE.bitmapRef = new SoftReference(null); + } + + private final Context mContext; + /** + * An LRU cache for bitmap holders. The cache contains bytes for photos just as they come from the + * database. Each holder has a soft reference to the actual bitmap. + */ + private final LruCache mBitmapHolderCache; + /** Cache size threshold at which bitmaps will not be preloaded. */ + private final int mBitmapHolderCacheRedZoneBytes; + /** + * Level 2 LRU cache for bitmaps. This is a smaller cache that holds the most recently used + * bitmaps to save time on decoding them from bytes (the bytes are stored in {@link + * #mBitmapHolderCache}. + */ + private final LruCache mBitmapCache; + /** + * A map from ImageView to the corresponding photo ID or uri, encapsulated in a request. The + * request may swapped out before the photo loading request is started. + */ + private final ConcurrentHashMap mPendingRequests = + new ConcurrentHashMap(); + /** Handler for messages sent to the UI thread. */ + private final Handler mMainThreadHandler = new Handler(this); + /** For debug: How many times we had to reload cached photo for a stale entry */ + private final AtomicInteger mStaleCacheOverwrite = new AtomicInteger(); + /** For debug: How many times we had to reload cached photo for a fresh entry. Should be 0. */ + private final AtomicInteger mFreshCacheOverwrite = new AtomicInteger(); + /** {@code true} if ALL entries in {@link #mBitmapHolderCache} are NOT fresh. */ + private volatile boolean mBitmapHolderCacheAllUnfresh = true; + /** Thread responsible for loading photos from the database. Created upon the first request. */ + private LoaderThread mLoaderThread; + /** A gate to make sure we only send one instance of MESSAGE_PHOTOS_NEEDED at a time. */ + private boolean mLoadingRequested; + /** Flag indicating if the image loading is paused. */ + private boolean mPaused; + /** The user agent string to use when loading URI based photos. */ + private String mUserAgent; + + public ContactPhotoManagerImpl(Context context) { + mContext = context; + + final ActivityManager am = + ((ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE)); + + final float cacheSizeAdjustment = (am.isLowRamDevice()) ? 0.5f : 1.0f; + + final int bitmapCacheSize = (int) (cacheSizeAdjustment * BITMAP_CACHE_SIZE); + mBitmapCache = + new LruCache(bitmapCacheSize) { + @Override + protected int sizeOf(Object key, Bitmap value) { + return value.getByteCount(); + } + + @Override + protected void entryRemoved( + boolean evicted, Object key, Bitmap oldValue, Bitmap newValue) { + if (DEBUG) { + dumpStats(); + } + } + }; + final int holderCacheSize = (int) (cacheSizeAdjustment * HOLDER_CACHE_SIZE); + mBitmapHolderCache = + new LruCache(holderCacheSize) { + @Override + protected int sizeOf(Object key, BitmapHolder value) { + return value.bytes != null ? value.bytes.length : 0; + } + + @Override + protected void entryRemoved( + boolean evicted, Object key, BitmapHolder oldValue, BitmapHolder newValue) { + if (DEBUG) { + dumpStats(); + } + } + }; + mBitmapHolderCacheRedZoneBytes = (int) (holderCacheSize * 0.75); + LogUtil.i( + "ContactPhotoManagerImpl.ContactPhotoManagerImpl", "cache adj: " + cacheSizeAdjustment); + if (DEBUG) { + LogUtil.d( + "ContactPhotoManagerImpl.ContactPhotoManagerImpl", + "Cache size: " + btk(mBitmapHolderCache.maxSize()) + " + " + btk(mBitmapCache.maxSize())); + } + + mThumbnailSize = + context.getResources().getDimensionPixelSize(R.dimen.contact_browser_list_item_photo_size); + + // Get a user agent string to use for URI photo requests. + mUserAgent = Bindings.get(context).getUserAgent(); + if (mUserAgent == null) { + mUserAgent = ""; + } + } + + /** Converts bytes to K bytes, rounding up. Used only for debug log. */ + private static String btk(int bytes) { + return ((bytes + 1023) / 1024) + "K"; + } + + private static final int safeDiv(int dividend, int divisor) { + return (divisor == 0) ? 0 : (dividend / divisor); + } + + private static boolean isChildView(View parent, View potentialChild) { + return potentialChild.getParent() != null + && (potentialChild.getParent() == parent + || (potentialChild.getParent() instanceof ViewGroup + && isChildView(parent, (ViewGroup) potentialChild.getParent()))); + } + + /** + * If necessary, decodes bytes stored in the holder to Bitmap. As long as the bitmap is held + * either by {@link #mBitmapCache} or by a soft reference in the holder, it will not be necessary + * to decode the bitmap. + */ + private static void inflateBitmap(BitmapHolder holder, int requestedExtent) { + final int sampleSize = + BitmapUtil.findOptimalSampleSize(holder.originalSmallerExtent, requestedExtent); + byte[] bytes = holder.bytes; + if (bytes == null || bytes.length == 0) { + return; + } + + if (sampleSize == holder.decodedSampleSize) { + // Check the soft reference. If will be retained if the bitmap is also + // in the LRU cache, so we don't need to check the LRU cache explicitly. + if (holder.bitmapRef != null) { + holder.bitmap = holder.bitmapRef.get(); + if (holder.bitmap != null) { + return; + } + } + } + + try { + Bitmap bitmap = BitmapUtil.decodeBitmapFromBytes(bytes, sampleSize); + + // TODO: As a temporary workaround while framework support is being added to + // clip non-square bitmaps into a perfect circle, manually crop the bitmap into + // into a square if it will be displayed as a thumbnail so that it can be cropped + // into a circle. + final int height = bitmap.getHeight(); + final int width = bitmap.getWidth(); + + // The smaller dimension of a scaled bitmap can range from anywhere from 0 to just + // below twice the length of a thumbnail image due to the way we calculate the optimal + // sample size. + if (height != width && Math.min(height, width) <= mThumbnailSize * 2) { + final int dimension = Math.min(height, width); + bitmap = ThumbnailUtils.extractThumbnail(bitmap, dimension, dimension); + } + // make bitmap mutable and draw size onto it + if (DEBUG_SIZES) { + Bitmap original = bitmap; + bitmap = bitmap.copy(bitmap.getConfig(), true); + original.recycle(); + Canvas canvas = new Canvas(bitmap); + Paint paint = new Paint(); + paint.setTextSize(16); + paint.setColor(Color.BLUE); + paint.setStyle(Style.FILL); + canvas.drawRect(0.0f, 0.0f, 50.0f, 20.0f, paint); + paint.setColor(Color.WHITE); + paint.setAntiAlias(true); + canvas.drawText(bitmap.getWidth() + "/" + sampleSize, 0, 15, paint); + } + + holder.decodedSampleSize = sampleSize; + holder.bitmap = bitmap; + holder.bitmapRef = new SoftReference(bitmap); + if (DEBUG) { + LogUtil.d( + "ContactPhotoManagerImpl.inflateBitmap", + "inflateBitmap " + + btk(bytes.length) + + " -> " + + bitmap.getWidth() + + "x" + + bitmap.getHeight() + + ", " + + btk(bitmap.getByteCount())); + } + } catch (OutOfMemoryError e) { + // Do nothing - the photo will appear to be missing + } + } + + /** Dump cache stats on logcat. */ + private void dumpStats() { + if (!DEBUG) { + return; + } + { + int numHolders = 0; + int rawBytes = 0; + int bitmapBytes = 0; + int numBitmaps = 0; + for (BitmapHolder h : mBitmapHolderCache.snapshot().values()) { + numHolders++; + if (h.bytes != null) { + rawBytes += h.bytes.length; + } + Bitmap b = h.bitmapRef != null ? h.bitmapRef.get() : null; + if (b != null) { + numBitmaps++; + bitmapBytes += b.getByteCount(); + } + } + LogUtil.d( + "ContactPhotoManagerImpl.dumpStats", + "L1: " + + btk(rawBytes) + + " + " + + btk(bitmapBytes) + + " = " + + btk(rawBytes + bitmapBytes) + + ", " + + numHolders + + " holders, " + + numBitmaps + + " bitmaps, avg: " + + btk(safeDiv(rawBytes, numHolders)) + + "," + + btk(safeDiv(bitmapBytes, numBitmaps))); + LogUtil.d( + "ContactPhotoManagerImpl.dumpStats", + "L1 Stats: " + + mBitmapHolderCache.toString() + + ", overwrite: fresh=" + + mFreshCacheOverwrite.get() + + " stale=" + + mStaleCacheOverwrite.get()); + } + + { + int numBitmaps = 0; + int bitmapBytes = 0; + for (Bitmap b : mBitmapCache.snapshot().values()) { + numBitmaps++; + bitmapBytes += b.getByteCount(); + } + LogUtil.d( + "ContactPhotoManagerImpl.dumpStats", + "L2: " + + btk(bitmapBytes) + + ", " + + numBitmaps + + " bitmaps" + + ", avg: " + + btk(safeDiv(bitmapBytes, numBitmaps))); + // We don't get from L2 cache, so L2 stats is meaningless. + } + } + + @Override + public void onTrimMemory(int level) { + if (DEBUG) { + LogUtil.d("ContactPhotoManagerImpl.onTrimMemory", "onTrimMemory: " + level); + } + if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) { + // Clear the caches. Note all pending requests will be removed too. + clear(); + } + } + + @Override + public void preloadPhotosInBackground() { + ensureLoaderThread(); + mLoaderThread.requestPreloading(); + } + + @Override + public void loadThumbnail( + ImageView view, + long photoId, + boolean darkTheme, + boolean isCircular, + DefaultImageRequest defaultImageRequest, + DefaultImageProvider defaultProvider) { + if (photoId == 0) { + // No photo is needed + defaultProvider.applyDefaultImage(view, -1, darkTheme, defaultImageRequest); + mPendingRequests.remove(view); + } else { + if (DEBUG) { + LogUtil.d("ContactPhotoManagerImpl.loadThumbnail", "loadPhoto request: " + photoId); + } + loadPhotoByIdOrUri( + view, Request.createFromThumbnailId(photoId, darkTheme, isCircular, defaultProvider)); + } + } + + @Override + public void loadPhoto( + ImageView view, + Uri photoUri, + int requestedExtent, + boolean darkTheme, + boolean isCircular, + DefaultImageRequest defaultImageRequest, + DefaultImageProvider defaultProvider) { + if (photoUri == null) { + // No photo is needed + defaultProvider.applyDefaultImage(view, requestedExtent, darkTheme, defaultImageRequest); + mPendingRequests.remove(view); + } else { + if (DEBUG) { + LogUtil.d("ContactPhotoManagerImpl.loadPhoto", "loadPhoto request: " + photoUri); + } + if (isDefaultImageUri(photoUri)) { + createAndApplyDefaultImageForUri( + view, photoUri, requestedExtent, darkTheme, isCircular, defaultProvider); + } else { + loadPhotoByIdOrUri( + view, + Request.createFromUri( + photoUri, requestedExtent, darkTheme, isCircular, defaultProvider)); + } + } + } + + private void createAndApplyDefaultImageForUri( + ImageView view, + Uri uri, + int requestedExtent, + boolean darkTheme, + boolean isCircular, + DefaultImageProvider defaultProvider) { + DefaultImageRequest request = getDefaultImageRequestFromUri(uri); + request.isCircular = isCircular; + defaultProvider.applyDefaultImage(view, requestedExtent, darkTheme, request); + } + + private void loadPhotoByIdOrUri(ImageView view, Request request) { + boolean loaded = loadCachedPhoto(view, request, false); + if (loaded) { + mPendingRequests.remove(view); + } else { + mPendingRequests.put(view, request); + if (!mPaused) { + // Send a request to start loading photos + requestLoading(); + } + } + } + + @Override + public void removePhoto(ImageView view) { + view.setImageDrawable(null); + mPendingRequests.remove(view); + } + + /** + * Cancels pending requests to load photos asynchronously for views inside {@param + * fragmentRootView}. If {@param fragmentRootView} is null, cancels all requests. + */ + @Override + public void cancelPendingRequests(View fragmentRootView) { + if (fragmentRootView == null) { + mPendingRequests.clear(); + return; + } + final Iterator> iterator = mPendingRequests.entrySet().iterator(); + while (iterator.hasNext()) { + final ImageView imageView = iterator.next().getKey(); + // If an ImageView is orphaned (currently scrap) or a child of fragmentRootView, then + // we can safely remove its request. + if (imageView.getParent() == null || isChildView(fragmentRootView, imageView)) { + iterator.remove(); + } + } + } + + @Override + public void refreshCache() { + if (mBitmapHolderCacheAllUnfresh) { + if (DEBUG) { + LogUtil.d("ContactPhotoManagerImpl.refreshCache", "refreshCache -- no fresh entries."); + } + return; + } + if (DEBUG) { + LogUtil.d("ContactPhotoManagerImpl.refreshCache", "refreshCache"); + } + mBitmapHolderCacheAllUnfresh = true; + for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) { + if (holder != BITMAP_UNAVAILABLE) { + holder.fresh = false; + } + } + } + + /** + * Checks if the photo is present in cache. If so, sets the photo on the view. + * + * @return false if the photo needs to be (re)loaded from the provider. + */ + @UiThread + private boolean loadCachedPhoto(ImageView view, Request request, boolean fadeIn) { + BitmapHolder holder = mBitmapHolderCache.get(request.getKey()); + if (holder == null) { + // The bitmap has not been loaded ==> show default avatar + request.applyDefaultImage(view, request.mIsCircular); + return false; + } + + if (holder.bytes == null) { + request.applyDefaultImage(view, request.mIsCircular); + return holder.fresh; + } + + Bitmap cachedBitmap = holder.bitmapRef == null ? null : holder.bitmapRef.get(); + if (cachedBitmap == null) { + request.applyDefaultImage(view, request.mIsCircular); + return false; + } + + final Drawable previousDrawable = view.getDrawable(); + if (fadeIn && previousDrawable != null) { + final Drawable[] layers = new Drawable[2]; + // Prevent cascade of TransitionDrawables. + if (previousDrawable instanceof TransitionDrawable) { + final TransitionDrawable previousTransitionDrawable = (TransitionDrawable) previousDrawable; + layers[0] = + previousTransitionDrawable.getDrawable( + previousTransitionDrawable.getNumberOfLayers() - 1); + } else { + layers[0] = previousDrawable; + } + layers[1] = getDrawableForBitmap(mContext.getResources(), cachedBitmap, request); + TransitionDrawable drawable = new TransitionDrawable(layers); + view.setImageDrawable(drawable); + drawable.startTransition(FADE_TRANSITION_DURATION); + } else { + view.setImageDrawable(getDrawableForBitmap(mContext.getResources(), cachedBitmap, request)); + } + + // Put the bitmap in the LRU cache. But only do this for images that are small enough + // (we require that at least six of those can be cached at the same time) + if (cachedBitmap.getByteCount() < mBitmapCache.maxSize() / 6) { + mBitmapCache.put(request.getKey(), cachedBitmap); + } + + // Soften the reference + holder.bitmap = null; + + return holder.fresh; + } + + /** + * Given a bitmap, returns a drawable that is configured to display the bitmap based on the + * specified request. + */ + private Drawable getDrawableForBitmap(Resources resources, Bitmap bitmap, Request request) { + if (request.mIsCircular) { + final RoundedBitmapDrawable drawable = RoundedBitmapDrawableFactory.create(resources, bitmap); + drawable.setAntiAlias(true); + drawable.setCornerRadius(bitmap.getHeight() / 2); + return drawable; + } else { + return new BitmapDrawable(resources, bitmap); + } + } + + public void clear() { + if (DEBUG) { + LogUtil.d("ContactPhotoManagerImpl.clear", "clear"); + } + mPendingRequests.clear(); + mBitmapHolderCache.evictAll(); + mBitmapCache.evictAll(); + } + + @Override + public void pause() { + mPaused = true; + } + + @Override + public void resume() { + mPaused = false; + if (DEBUG) { + dumpStats(); + } + if (!mPendingRequests.isEmpty()) { + requestLoading(); + } + } + + /** + * Sends a message to this thread itself to start loading images. If the current view contains + * multiple image views, all of those image views will get a chance to request their respective + * photos before any of those requests are executed. This allows us to load images in bulk. + */ + private void requestLoading() { + if (!mLoadingRequested) { + mLoadingRequested = true; + mMainThreadHandler.sendEmptyMessage(MESSAGE_REQUEST_LOADING); + } + } + + /** Processes requests on the main thread. */ + @Override + public boolean handleMessage(Message msg) { + switch (msg.what) { + case MESSAGE_REQUEST_LOADING: + { + mLoadingRequested = false; + if (!mPaused) { + ensureLoaderThread(); + mLoaderThread.requestLoading(); + } + return true; + } + + case MESSAGE_PHOTOS_LOADED: + { + if (!mPaused) { + processLoadedImages(); + } + if (DEBUG) { + dumpStats(); + } + return true; + } + } + return false; + } + + public void ensureLoaderThread() { + if (mLoaderThread == null) { + mLoaderThread = new LoaderThread(mContext.getContentResolver()); + mLoaderThread.start(); + } + } + + /** + * Goes over pending loading requests and displays loaded photos. If some of the photos still + * haven't been loaded, sends another request for image loading. + */ + private void processLoadedImages() { + final Iterator> iterator = mPendingRequests.entrySet().iterator(); + while (iterator.hasNext()) { + final Entry entry = iterator.next(); + // TODO: Temporarily disable contact photo fading in, until issues with + // RoundedBitmapDrawables overlapping the default image drawables are resolved. + final boolean loaded = loadCachedPhoto(entry.getKey(), entry.getValue(), false); + if (loaded) { + iterator.remove(); + } + } + + softenCache(); + + if (!mPendingRequests.isEmpty()) { + requestLoading(); + } + } + + /** + * Removes strong references to loaded bitmaps to allow them to be garbage collected if needed. + * Some of the bitmaps will still be retained by {@link #mBitmapCache}. + */ + private void softenCache() { + for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) { + holder.bitmap = null; + } + } + + /** Stores the supplied bitmap in cache. */ + private void cacheBitmap(Object key, byte[] bytes, boolean preloading, int requestedExtent) { + if (DEBUG) { + BitmapHolder prev = mBitmapHolderCache.get(key); + if (prev != null && prev.bytes != null) { + LogUtil.d( + "ContactPhotoManagerImpl.cacheBitmap", + "overwriting cache: key=" + key + (prev.fresh ? " FRESH" : " stale")); + if (prev.fresh) { + mFreshCacheOverwrite.incrementAndGet(); + } else { + mStaleCacheOverwrite.incrementAndGet(); + } + } + LogUtil.d( + "ContactPhotoManagerImpl.cacheBitmap", + "caching data: key=" + key + ", " + (bytes == null ? "" : btk(bytes.length))); + } + BitmapHolder holder = + new BitmapHolder(bytes, bytes == null ? -1 : BitmapUtil.getSmallerExtentFromBytes(bytes)); + + // Unless this image is being preloaded, decode it right away while + // we are still on the background thread. + if (!preloading) { + inflateBitmap(holder, requestedExtent); + } + + if (bytes != null) { + mBitmapHolderCache.put(key, holder); + if (mBitmapHolderCache.get(key) != holder) { + LogUtil.w("ContactPhotoManagerImpl.cacheBitmap", "bitmap too big to fit in cache."); + mBitmapHolderCache.put(key, BITMAP_UNAVAILABLE); + } + } else { + mBitmapHolderCache.put(key, BITMAP_UNAVAILABLE); + } + + mBitmapHolderCacheAllUnfresh = false; + } + + /** + * Populates an array of photo IDs that need to be loaded. Also decodes bitmaps that we have + * already loaded + */ + private void obtainPhotoIdsAndUrisToLoad( + Set photoIds, Set photoIdsAsStrings, Set uris) { + photoIds.clear(); + photoIdsAsStrings.clear(); + uris.clear(); + + boolean jpegsDecoded = false; + + /* + * Since the call is made from the loader thread, the map could be + * changing during the iteration. That's not really a problem: + * ConcurrentHashMap will allow those changes to happen without throwing + * exceptions. Since we may miss some requests in the situation of + * concurrent change, we will need to check the map again once loading + * is complete. + */ + Iterator iterator = mPendingRequests.values().iterator(); + while (iterator.hasNext()) { + Request request = iterator.next(); + final BitmapHolder holder = mBitmapHolderCache.get(request.getKey()); + if (holder == BITMAP_UNAVAILABLE) { + continue; + } + if (holder != null + && holder.bytes != null + && holder.fresh + && (holder.bitmapRef == null || holder.bitmapRef.get() == null)) { + // This was previously loaded but we don't currently have the inflated Bitmap + inflateBitmap(holder, request.getRequestedExtent()); + jpegsDecoded = true; + } else { + if (holder == null || !holder.fresh) { + if (request.isUriRequest()) { + uris.add(request); + } else { + photoIds.add(request.getId()); + photoIdsAsStrings.add(String.valueOf(request.mId)); + } + } + } + } + + if (jpegsDecoded) { + mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED); + } + } + + /** Maintains the state of a particular photo. */ + private static class BitmapHolder { + + final byte[] bytes; + final int originalSmallerExtent; + + volatile boolean fresh; + Bitmap bitmap; + Reference bitmapRef; + int decodedSampleSize; + + public BitmapHolder(byte[] bytes, int originalSmallerExtent) { + this.bytes = bytes; + this.fresh = true; + this.originalSmallerExtent = originalSmallerExtent; + } + } + + /** + * A holder for either a Uri or an id and a flag whether this was requested for the dark or light + * theme + */ + private static final class Request { + + private final long mId; + private final Uri mUri; + private final boolean mDarkTheme; + private final int mRequestedExtent; + private final DefaultImageProvider mDefaultProvider; + /** Whether or not the contact photo is to be displayed as a circle */ + private final boolean mIsCircular; + + private Request( + long id, + Uri uri, + int requestedExtent, + boolean darkTheme, + boolean isCircular, + DefaultImageProvider defaultProvider) { + mId = id; + mUri = uri; + mDarkTheme = darkTheme; + mIsCircular = isCircular; + mRequestedExtent = requestedExtent; + mDefaultProvider = defaultProvider; + } + + public static Request createFromThumbnailId( + long id, boolean darkTheme, boolean isCircular, DefaultImageProvider defaultProvider) { + return new Request(id, null /* no URI */, -1, darkTheme, isCircular, defaultProvider); + } + + public static Request createFromUri( + Uri uri, + int requestedExtent, + boolean darkTheme, + boolean isCircular, + DefaultImageProvider defaultProvider) { + return new Request( + 0 /* no ID */, uri, requestedExtent, darkTheme, isCircular, defaultProvider); + } + + public boolean isUriRequest() { + return mUri != null; + } + + public Uri getUri() { + return mUri; + } + + public long getId() { + return mId; + } + + public int getRequestedExtent() { + return mRequestedExtent; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (int) (mId ^ (mId >>> 32)); + result = prime * result + mRequestedExtent; + result = prime * result + ((mUri == null) ? 0 : mUri.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final Request that = (Request) obj; + if (mId != that.mId) { + return false; + } + if (mRequestedExtent != that.mRequestedExtent) { + return false; + } + if (!UriUtils.areEqual(mUri, that.mUri)) { + return false; + } + // Don't compare equality of mDarkTheme because it is only used in the default contact + // photo case. When the contact does have a photo, the contact photo is the same + // regardless of mDarkTheme, so we shouldn't need to put the photo request on the queue + // twice. + return true; + } + + public Object getKey() { + return mUri == null ? mId : mUri; + } + + /** + * Applies the default image to the current view. If the request is URI-based, looks for the + * contact type encoded fragment to determine if this is a request for a business photo, in + * which case we will load the default business photo. + * + * @param view The current image view to apply the image to. + * @param isCircular Whether the image is circular or not. + */ + public void applyDefaultImage(ImageView view, boolean isCircular) { + final DefaultImageRequest request; + + if (isCircular) { + request = + ContactPhotoManager.isBusinessContactUri(mUri) + ? DefaultImageRequest.EMPTY_CIRCULAR_BUSINESS_IMAGE_REQUEST + : DefaultImageRequest.EMPTY_CIRCULAR_DEFAULT_IMAGE_REQUEST; + } else { + request = + ContactPhotoManager.isBusinessContactUri(mUri) + ? DefaultImageRequest.EMPTY_DEFAULT_BUSINESS_IMAGE_REQUEST + : DefaultImageRequest.EMPTY_DEFAULT_IMAGE_REQUEST; + } + mDefaultProvider.applyDefaultImage(view, mRequestedExtent, mDarkTheme, request); + } + } + + /** The thread that performs loading of photos from the database. */ + private class LoaderThread extends HandlerThread implements Callback { + + private static final int BUFFER_SIZE = 1024 * 16; + private static final int MESSAGE_PRELOAD_PHOTOS = 0; + private static final int MESSAGE_LOAD_PHOTOS = 1; + + /** A pause between preload batches that yields to the UI thread. */ + private static final int PHOTO_PRELOAD_DELAY = 1000; + + /** Number of photos to preload per batch. */ + private static final int PRELOAD_BATCH = 25; + + /** + * Maximum number of photos to preload. If the cache size is 2Mb and the expected average size + * of a photo is 4kb, then this number should be 2Mb/4kb = 500. + */ + private static final int MAX_PHOTOS_TO_PRELOAD = 100; + + private static final int PRELOAD_STATUS_NOT_STARTED = 0; + private static final int PRELOAD_STATUS_IN_PROGRESS = 1; + private static final int PRELOAD_STATUS_DONE = 2; + private final ContentResolver mResolver; + private final StringBuilder mStringBuilder = new StringBuilder(); + private final Set mPhotoIds = new HashSet<>(); + private final Set mPhotoIdsAsStrings = new HashSet<>(); + private final Set mPhotoUris = new HashSet<>(); + private final List mPreloadPhotoIds = new ArrayList<>(); + private Handler mLoaderThreadHandler; + private byte[] mBuffer; + private int mPreloadStatus = PRELOAD_STATUS_NOT_STARTED; + + public LoaderThread(ContentResolver resolver) { + super(LOADER_THREAD_NAME); + mResolver = resolver; + } + + public void ensureHandler() { + if (mLoaderThreadHandler == null) { + mLoaderThreadHandler = new Handler(getLooper(), this); + } + } + + /** + * Kicks off preloading of the next batch of photos on the background thread. Preloading will + * happen after a delay: we want to yield to the UI thread as much as possible. + * + *

If preloading is already complete, does nothing. + */ + public void requestPreloading() { + if (mPreloadStatus == PRELOAD_STATUS_DONE) { + return; + } + + ensureHandler(); + if (mLoaderThreadHandler.hasMessages(MESSAGE_LOAD_PHOTOS)) { + return; + } + + mLoaderThreadHandler.sendEmptyMessageDelayed(MESSAGE_PRELOAD_PHOTOS, PHOTO_PRELOAD_DELAY); + } + + /** + * Sends a message to this thread to load requested photos. Cancels a preloading request, if + * any: we don't want preloading to impede loading of the photos we need to display now. + */ + public void requestLoading() { + ensureHandler(); + mLoaderThreadHandler.removeMessages(MESSAGE_PRELOAD_PHOTOS); + mLoaderThreadHandler.sendEmptyMessage(MESSAGE_LOAD_PHOTOS); + } + + /** + * Receives the above message, loads photos and then sends a message to the main thread to + * process them. + */ + @Override + public boolean handleMessage(Message msg) { + switch (msg.what) { + case MESSAGE_PRELOAD_PHOTOS: + preloadPhotosInBackground(); + break; + case MESSAGE_LOAD_PHOTOS: + loadPhotosInBackground(); + break; + } + return true; + } + + /** + * The first time it is called, figures out which photos need to be preloaded. Each subsequent + * call preloads the next batch of photos and requests another cycle of preloading after a + * delay. The whole process ends when we either run out of photos to preload or fill up cache. + */ + @WorkerThread + private void preloadPhotosInBackground() { + if (!PermissionsUtil.hasPermission(mContext, android.Manifest.permission.READ_CONTACTS)) { + return; + } + + if (mPreloadStatus == PRELOAD_STATUS_DONE) { + return; + } + + if (mPreloadStatus == PRELOAD_STATUS_NOT_STARTED) { + queryPhotosForPreload(); + if (mPreloadPhotoIds.isEmpty()) { + mPreloadStatus = PRELOAD_STATUS_DONE; + } else { + mPreloadStatus = PRELOAD_STATUS_IN_PROGRESS; + } + requestPreloading(); + return; + } + + if (mBitmapHolderCache.size() > mBitmapHolderCacheRedZoneBytes) { + mPreloadStatus = PRELOAD_STATUS_DONE; + return; + } + + mPhotoIds.clear(); + mPhotoIdsAsStrings.clear(); + + int count = 0; + int preloadSize = mPreloadPhotoIds.size(); + while (preloadSize > 0 && mPhotoIds.size() < PRELOAD_BATCH) { + preloadSize--; + count++; + Long photoId = mPreloadPhotoIds.get(preloadSize); + mPhotoIds.add(photoId); + mPhotoIdsAsStrings.add(photoId.toString()); + mPreloadPhotoIds.remove(preloadSize); + } + + loadThumbnails(true); + + if (preloadSize == 0) { + mPreloadStatus = PRELOAD_STATUS_DONE; + } + + LogUtil.v( + "ContactPhotoManagerImpl.preloadPhotosInBackground", + "preloaded " + count + " photos. cached bytes: " + mBitmapHolderCache.size()); + + requestPreloading(); + } + + @WorkerThread + private void queryPhotosForPreload() { + Cursor cursor = null; + try { + Uri uri = + Contacts.CONTENT_URI + .buildUpon() + .appendQueryParameter( + ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT)) + .appendQueryParameter( + ContactsContract.LIMIT_PARAM_KEY, String.valueOf(MAX_PHOTOS_TO_PRELOAD)) + .build(); + cursor = + mResolver.query( + uri, + new String[] {Contacts.PHOTO_ID}, + Contacts.PHOTO_ID + " NOT NULL AND " + Contacts.PHOTO_ID + "!=0", + null, + Contacts.STARRED + " DESC, " + Contacts.LAST_TIME_CONTACTED + " DESC"); + + if (cursor != null) { + while (cursor.moveToNext()) { + // Insert them in reverse order, because we will be taking + // them from the end of the list for loading. + mPreloadPhotoIds.add(0, cursor.getLong(0)); + } + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + + @WorkerThread + private void loadPhotosInBackground() { + if (!PermissionsUtil.hasPermission(mContext, android.Manifest.permission.READ_CONTACTS)) { + return; + } + obtainPhotoIdsAndUrisToLoad(mPhotoIds, mPhotoIdsAsStrings, mPhotoUris); + loadThumbnails(false); + loadUriBasedPhotos(); + requestPreloading(); + } + + /** Loads thumbnail photos with ids */ + @WorkerThread + private void loadThumbnails(boolean preloading) { + if (mPhotoIds.isEmpty()) { + return; + } + + // Remove loaded photos from the preload queue: we don't want + // the preloading process to load them again. + if (!preloading && mPreloadStatus == PRELOAD_STATUS_IN_PROGRESS) { + for (Long id : mPhotoIds) { + mPreloadPhotoIds.remove(id); + } + if (mPreloadPhotoIds.isEmpty()) { + mPreloadStatus = PRELOAD_STATUS_DONE; + } + } + + mStringBuilder.setLength(0); + mStringBuilder.append(Photo._ID + " IN("); + for (int i = 0; i < mPhotoIds.size(); i++) { + if (i != 0) { + mStringBuilder.append(','); + } + mStringBuilder.append('?'); + } + mStringBuilder.append(')'); + + Cursor cursor = null; + try { + if (DEBUG) { + LogUtil.d( + "ContactPhotoManagerImpl.loadThumbnails", + "loading " + TextUtils.join(",", mPhotoIdsAsStrings)); + } + cursor = + mResolver.query( + Data.CONTENT_URI, + COLUMNS, + mStringBuilder.toString(), + mPhotoIdsAsStrings.toArray(EMPTY_STRING_ARRAY), + null); + + if (cursor != null) { + while (cursor.moveToNext()) { + Long id = cursor.getLong(0); + byte[] bytes = cursor.getBlob(1); + cacheBitmap(id, bytes, preloading, -1); + mPhotoIds.remove(id); + } + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + + // Remaining photos were not found in the contacts database (but might be in profile). + for (Long id : mPhotoIds) { + if (ContactsContract.isProfileId(id)) { + Cursor profileCursor = null; + try { + profileCursor = + mResolver.query( + ContentUris.withAppendedId(Data.CONTENT_URI, id), COLUMNS, null, null, null); + if (profileCursor != null && profileCursor.moveToFirst()) { + cacheBitmap(profileCursor.getLong(0), profileCursor.getBlob(1), preloading, -1); + } else { + // Couldn't load a photo this way either. + cacheBitmap(id, null, preloading, -1); + } + } finally { + if (profileCursor != null) { + profileCursor.close(); + } + } + } else { + // Not a profile photo and not found - mark the cache accordingly + cacheBitmap(id, null, preloading, -1); + } + } + + mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED); + } + + /** + * Loads photos referenced with Uris. Those can be remote thumbnails (from directory searches), + * display photos etc + */ + @WorkerThread + private void loadUriBasedPhotos() { + for (Request uriRequest : mPhotoUris) { + // Keep the original URI and use this to key into the cache. Failure to do so will + // result in an image being continually reloaded into cache if the original URI + // has a contact type encodedFragment (eg nearby places business photo URLs). + Uri originalUri = uriRequest.getUri(); + + // Strip off the "contact type" we added to the URI to ensure it was identifiable as + // a business photo -- there is no need to pass this on to the server. + Uri uri = ContactPhotoManager.removeContactType(originalUri); + + if (mBuffer == null) { + mBuffer = new byte[BUFFER_SIZE]; + } + try { + if (DEBUG) { + LogUtil.d("ContactPhotoManagerImpl.loadUriBasedPhotos", "loading " + uri); + } + final String scheme = uri.getScheme(); + InputStream is = null; + if (scheme.equals("http") || scheme.equals("https")) { + TrafficStats.setThreadStatsTag(TrafficStatsTags.CONTACT_PHOTO_DOWNLOAD_TAG); + final HttpURLConnection connection = + (HttpURLConnection) new URL(uri.toString()).openConnection(); + + // Include the user agent if it is specified. + if (!TextUtils.isEmpty(mUserAgent)) { + connection.setRequestProperty("User-Agent", mUserAgent); + } + try { + is = connection.getInputStream(); + } catch (IOException e) { + connection.disconnect(); + is = null; + } + TrafficStats.clearThreadStatsTag(); + } else { + is = mResolver.openInputStream(uri); + } + if (is != null) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + int size; + while ((size = is.read(mBuffer)) != -1) { + baos.write(mBuffer, 0, size); + } + } finally { + is.close(); + } + cacheBitmap(originalUri, baos.toByteArray(), false, uriRequest.getRequestedExtent()); + mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED); + } else { + LogUtil.v("ContactPhotoManagerImpl.loadUriBasedPhotos", "cannot load photo " + uri); + cacheBitmap(originalUri, null, false, uriRequest.getRequestedExtent()); + } + } catch (final Exception | OutOfMemoryError ex) { + LogUtil.v("ContactPhotoManagerImpl.loadUriBasedPhotos", "cannot load photo " + uri, ex); + cacheBitmap(originalUri, null, false, uriRequest.getRequestedExtent()); + } + } + } + } +} diff --git a/java/com/android/contacts/common/ContactPresenceIconUtil.java b/java/com/android/contacts/common/ContactPresenceIconUtil.java new file mode 100644 index 000000000..eeaf652a8 --- /dev/null +++ b/java/com/android/contacts/common/ContactPresenceIconUtil.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.provider.ContactsContract.StatusUpdates; + +/** Define the contact present show policy in Contacts */ +public class ContactPresenceIconUtil { + + /** + * Get the presence icon resource according the status. + * + * @return null means don't show the status icon. + */ + public static Drawable getPresenceIcon(Context context, int status) { + // We don't show the offline status in Contacts + switch (status) { + case StatusUpdates.AVAILABLE: + case StatusUpdates.IDLE: + case StatusUpdates.AWAY: + case StatusUpdates.DO_NOT_DISTURB: + case StatusUpdates.INVISIBLE: + return context.getResources().getDrawable(StatusUpdates.getPresenceIconResourceId(status)); + case StatusUpdates.OFFLINE: + // The undefined status is treated as OFFLINE in getPresenceIconResourceId(); + default: + return null; + } + } +} diff --git a/java/com/android/contacts/common/ContactStatusUtil.java b/java/com/android/contacts/common/ContactStatusUtil.java new file mode 100644 index 000000000..97d84c876 --- /dev/null +++ b/java/com/android/contacts/common/ContactStatusUtil.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common; + +import android.content.Context; +import android.content.res.Resources; +import android.provider.ContactsContract.StatusUpdates; + +/** Provides static function to get default contact status message. */ +public class ContactStatusUtil { + + private static final String TAG = "ContactStatusUtil"; + + public static String getStatusString(Context context, int presence) { + Resources resources = context.getResources(); + switch (presence) { + case StatusUpdates.AVAILABLE: + return resources.getString(R.string.status_available); + case StatusUpdates.IDLE: + case StatusUpdates.AWAY: + return resources.getString(R.string.status_away); + case StatusUpdates.DO_NOT_DISTURB: + return resources.getString(R.string.status_busy); + case StatusUpdates.OFFLINE: + case StatusUpdates.INVISIBLE: + default: + return null; + } + } +} diff --git a/java/com/android/contacts/common/ContactTileLoaderFactory.java b/java/com/android/contacts/common/ContactTileLoaderFactory.java new file mode 100644 index 000000000..d71472ef8 --- /dev/null +++ b/java/com/android/contacts/common/ContactTileLoaderFactory.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.contacts.common; + +import android.content.Context; +import android.content.CursorLoader; +import android.net.Uri; +import android.provider.ContactsContract; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.Contacts; +import android.support.annotation.VisibleForTesting; + +/** + * Used to create {@link CursorLoader} which finds contacts information from the strequents table. + * + *

Only returns contacts with phone numbers. + */ +public final class ContactTileLoaderFactory { + + /** + * The _ID field returned for strequent items actually contains data._id instead of contacts._id + * because the query is performed on the data table. In order to obtain the contact id for + * strequent items, use Phone.contact_id instead. + */ + @VisibleForTesting + public static final String[] COLUMNS_PHONE_ONLY = + new String[] { + Contacts._ID, + Contacts.DISPLAY_NAME_PRIMARY, + Contacts.STARRED, + Contacts.PHOTO_URI, + Contacts.LOOKUP_KEY, + Phone.NUMBER, + Phone.TYPE, + Phone.LABEL, + Phone.IS_SUPER_PRIMARY, + Contacts.PINNED, + Phone.CONTACT_ID, + Contacts.DISPLAY_NAME_ALTERNATIVE, + }; + + public static CursorLoader createStrequentPhoneOnlyLoader(Context context) { + Uri uri = + Contacts.CONTENT_STREQUENT_URI + .buildUpon() + .appendQueryParameter(ContactsContract.STREQUENT_PHONE_ONLY, "true") + .build(); + + return new CursorLoader(context, uri, COLUMNS_PHONE_ONLY, null, null, null); + } +} diff --git a/java/com/android/contacts/common/ContactsUtils.java b/java/com/android/contacts/common/ContactsUtils.java new file mode 100644 index 000000000..60af44b9a --- /dev/null +++ b/java/com/android/contacts/common/ContactsUtils.java @@ -0,0 +1,265 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common; + +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.provider.ContactsContract.CommonDataKinds.Im; +import android.provider.ContactsContract.DisplayPhoto; +import android.support.annotation.IntDef; +import android.text.TextUtils; +import android.util.Pair; +import com.android.contacts.common.compat.ContactsCompat; +import com.android.contacts.common.compat.DirectoryCompat; +import com.android.contacts.common.model.AccountTypeManager; +import com.android.contacts.common.model.account.AccountWithDataSet; +import com.android.contacts.common.model.dataitem.ImDataItem; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.List; + +public class ContactsUtils { + + // Telecomm related schemes are in CallUtil + public static final String SCHEME_IMTO = "imto"; + public static final String SCHEME_MAILTO = "mailto"; + public static final String SCHEME_SMSTO = "smsto"; + public static final long USER_TYPE_CURRENT = 0; + public static final long USER_TYPE_WORK = 1; + private static final String TAG = "ContactsUtils"; + private static final int DEFAULT_THUMBNAIL_SIZE = 96; + private static int sThumbnailSize = -1; + + /** + * This looks up the provider name defined in ProviderNames from the predefined IM protocol id. + * This is used for interacting with the IM application. + * + * @param protocol the protocol ID + * @return the provider name the IM app uses for the given protocol, or null if no provider is + * defined for the given protocol + * @hide + */ + public static String lookupProviderNameFromId(int protocol) { + switch (protocol) { + case Im.PROTOCOL_GOOGLE_TALK: + return ProviderNames.GTALK; + case Im.PROTOCOL_AIM: + return ProviderNames.AIM; + case Im.PROTOCOL_MSN: + return ProviderNames.MSN; + case Im.PROTOCOL_YAHOO: + return ProviderNames.YAHOO; + case Im.PROTOCOL_ICQ: + return ProviderNames.ICQ; + case Im.PROTOCOL_JABBER: + return ProviderNames.JABBER; + case Im.PROTOCOL_SKYPE: + return ProviderNames.SKYPE; + case Im.PROTOCOL_QQ: + return ProviderNames.QQ; + } + return null; + } + + /** + * Test if the given {@link CharSequence} contains any graphic characters, first checking {@link + * TextUtils#isEmpty(CharSequence)} to handle null. + */ + public static boolean isGraphic(CharSequence str) { + return !TextUtils.isEmpty(str) && TextUtils.isGraphic(str); + } + + /** Returns true if two objects are considered equal. Two null references are equal here. */ + public static boolean areObjectsEqual(Object a, Object b) { + return a == b || (a != null && a.equals(b)); + } + + /** Returns true if two {@link Intent}s are both null, or have the same action. */ + public static final boolean areIntentActionEqual(Intent a, Intent b) { + if (a == b) { + return true; + } + if (a == null || b == null) { + return false; + } + return TextUtils.equals(a.getAction(), b.getAction()); + } + + public static boolean areGroupWritableAccountsAvailable(Context context) { + final List accounts = + AccountTypeManager.getInstance(context).getGroupWritableAccounts(); + return !accounts.isEmpty(); + } + + /** + * Returns the size (width and height) of thumbnail pictures as configured in the provider. This + * can safely be called from the UI thread, as the provider can serve this without performing a + * database access + */ + public static int getThumbnailSize(Context context) { + if (sThumbnailSize == -1) { + final Cursor c = + context + .getContentResolver() + .query( + DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI, + new String[] {DisplayPhoto.THUMBNAIL_MAX_DIM}, + null, + null, + null); + if (c != null) { + try { + if (c.moveToFirst()) { + sThumbnailSize = c.getInt(0); + } + } finally { + c.close(); + } + } + } + return sThumbnailSize != -1 ? sThumbnailSize : DEFAULT_THUMBNAIL_SIZE; + } + + private static Intent getCustomImIntent(ImDataItem im, int protocol) { + String host = im.getCustomProtocol(); + final String data = im.getData(); + if (TextUtils.isEmpty(data)) { + return null; + } + if (protocol != Im.PROTOCOL_CUSTOM) { + // Try bringing in a well-known host for specific protocols + host = ContactsUtils.lookupProviderNameFromId(protocol); + } + if (TextUtils.isEmpty(host)) { + return null; + } + final String authority = host.toLowerCase(); + final Uri imUri = + new Uri.Builder().scheme(SCHEME_IMTO).authority(authority).appendPath(data).build(); + final Intent intent = new Intent(Intent.ACTION_SENDTO, imUri); + return intent; + } + + /** + * Returns the proper Intent for an ImDatItem. If available, a secondary intent is stored in the + * second Pair slot + */ + public static Pair buildImIntent(Context context, ImDataItem im) { + Intent intent = null; + Intent secondaryIntent = null; + final boolean isEmail = im.isCreatedFromEmail(); + + if (!isEmail && !im.isProtocolValid()) { + return new Pair<>(null, null); + } + + final String data = im.getData(); + if (TextUtils.isEmpty(data)) { + return new Pair<>(null, null); + } + + final int protocol = isEmail ? Im.PROTOCOL_GOOGLE_TALK : im.getProtocol(); + + if (protocol == Im.PROTOCOL_GOOGLE_TALK) { + final int chatCapability = im.getChatCapability(); + if ((chatCapability & Im.CAPABILITY_HAS_CAMERA) != 0) { + intent = new Intent(Intent.ACTION_SENDTO, Uri.parse("xmpp:" + data + "?message")); + secondaryIntent = new Intent(Intent.ACTION_SENDTO, Uri.parse("xmpp:" + data + "?call")); + } else if ((chatCapability & Im.CAPABILITY_HAS_VOICE) != 0) { + // Allow Talking and Texting + intent = new Intent(Intent.ACTION_SENDTO, Uri.parse("xmpp:" + data + "?message")); + secondaryIntent = new Intent(Intent.ACTION_SENDTO, Uri.parse("xmpp:" + data + "?call")); + } else { + intent = new Intent(Intent.ACTION_SENDTO, Uri.parse("xmpp:" + data + "?message")); + } + } else { + // Build an IM Intent + intent = getCustomImIntent(im, protocol); + } + return new Pair<>(intent, secondaryIntent); + } + + /** + * Determine UserType from directory id and contact id. + * + *

3 types of query + * + *

1. 2 profile query: content://com.android.contacts/phone_lookup_enterprise/1234567890 + * personal and work contact are mixed into one cursor. no directory id. contact_id indicates if + * it's work contact + * + *

2. work local query: + * content://com.android.contacts/phone_lookup_enterprise/1234567890?directory=1000000000 either + * directory_id or contact_id is enough to identify work contact + * + *

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

Summary: If directory_id is not null, always use directory_id to identify work contact. + * (which is the case here) Otherwise, use contact_id. + * + * @param directoryId directory id of ContactsProvider query + * @param contactId contact id + * @return UserType indicates the user type of the contact. A directory id or contact id larger + * than a thredshold indicates that the contact is stored in Work Profile, but not in current + * user. It's a contract by ContactsProvider and check by Contacts.isEnterpriseDirectoryId and + * Contacts.isEnterpriseContactId. Currently, only 2 kinds of users can be detected from the + * directoryId and contactId as ContactsProvider can only access current and work user's + * contacts + */ + public static @UserType long determineUserType(Long directoryId, Long contactId) { + // First check directory id + if (directoryId != null) { + return DirectoryCompat.isEnterpriseDirectoryId(directoryId) + ? USER_TYPE_WORK + : USER_TYPE_CURRENT; + } + // Only check contact id if directory id is null + if (contactId != null && contactId != 0L && ContactsCompat.isEnterpriseContactId(contactId)) { + return USER_TYPE_WORK; + } else { + return USER_TYPE_CURRENT; + } + } + + // TODO find a proper place for the canonical version of these + public interface ProviderNames { + + String YAHOO = "Yahoo"; + String GTALK = "GTalk"; + String MSN = "MSN"; + String ICQ = "ICQ"; + String AIM = "AIM"; + String XMPP = "XMPP"; + String JABBER = "JABBER"; + String SKYPE = "SKYPE"; + String QQ = "QQ"; + } + + /** + * UserType indicates the user type of the contact. If the contact is from Work User (Work Profile + * in Android Multi-User System), it's {@link #USER_TYPE_WORK}, otherwise, {@link + * #USER_TYPE_CURRENT}. Please note that current user can be in work profile, where the dialer is + * running inside Work Profile. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({USER_TYPE_CURRENT, USER_TYPE_WORK}) + public @interface UserType {} +} diff --git a/java/com/android/contacts/common/GeoUtil.java b/java/com/android/contacts/common/GeoUtil.java new file mode 100644 index 000000000..50b0cd9e3 --- /dev/null +++ b/java/com/android/contacts/common/GeoUtil.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.contacts.common; + +import android.app.Application; +import android.content.Context; +import com.android.contacts.common.location.CountryDetector; +import com.google.i18n.phonenumbers.NumberParseException; +import com.google.i18n.phonenumbers.PhoneNumberUtil; +import com.google.i18n.phonenumbers.Phonenumber; +import com.google.i18n.phonenumbers.geocoding.PhoneNumberOfflineGeocoder; +import java.util.Locale; + +/** Static methods related to Geo. */ +public class GeoUtil { + + /** + * Returns the country code of the country the user is currently in. Before calling this method, + * make sure that {@link CountryDetector#initialize(Context)} has already been called in {@link + * Application#onCreate()}. + * + * @return The ISO 3166-1 two letters country code of the country the user is in. + */ + public static String getCurrentCountryIso(Context context) { + // The {@link CountryDetector} should never return null so this is safe to return as-is. + return CountryDetector.getInstance(context).getCurrentCountryIso(); + } + + public static String getGeocodedLocationFor(Context context, String phoneNumber) { + final PhoneNumberOfflineGeocoder geocoder = PhoneNumberOfflineGeocoder.getInstance(); + final PhoneNumberUtil phoneNumberUtil = PhoneNumberUtil.getInstance(); + try { + final Phonenumber.PhoneNumber structuredPhoneNumber = + phoneNumberUtil.parse(phoneNumber, getCurrentCountryIso(context)); + final Locale locale = context.getResources().getConfiguration().locale; + return geocoder.getDescriptionForNumber(structuredPhoneNumber, locale); + } catch (NumberParseException e) { + return null; + } + } +} diff --git a/java/com/android/contacts/common/GroupMetaData.java b/java/com/android/contacts/common/GroupMetaData.java new file mode 100644 index 000000000..b34f1d629 --- /dev/null +++ b/java/com/android/contacts/common/GroupMetaData.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.contacts.common; + +/** + * Meta-data for a contact group. We load all groups associated with the contact's constituent + * accounts. + */ +public final class GroupMetaData { + + private String mAccountName; + private String mAccountType; + private String mDataSet; + private long mGroupId; + private String mTitle; + private boolean mDefaultGroup; + private boolean mFavorites; + + public GroupMetaData( + String accountName, + String accountType, + String dataSet, + long groupId, + String title, + boolean defaultGroup, + boolean favorites) { + this.mAccountName = accountName; + this.mAccountType = accountType; + this.mDataSet = dataSet; + this.mGroupId = groupId; + this.mTitle = title; + this.mDefaultGroup = defaultGroup; + this.mFavorites = favorites; + } + + public String getAccountName() { + return mAccountName; + } + + public String getAccountType() { + return mAccountType; + } + + public String getDataSet() { + return mDataSet; + } + + public long getGroupId() { + return mGroupId; + } + + public String getTitle() { + return mTitle; + } + + public boolean isDefaultGroup() { + return mDefaultGroup; + } + + public boolean isFavorites() { + return mFavorites; + } +} diff --git a/java/com/android/contacts/common/MoreContactUtils.java b/java/com/android/contacts/common/MoreContactUtils.java new file mode 100644 index 000000000..028f89971 --- /dev/null +++ b/java/com/android/contacts/common/MoreContactUtils.java @@ -0,0 +1,251 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.contacts.common; + +import android.content.Context; +import android.content.Intent; +import android.graphics.Rect; +import android.net.Uri; +import android.provider.ContactsContract; +import android.telephony.PhoneNumberUtils; +import android.text.TextUtils; +import android.view.View; +import android.widget.TextView; +import com.android.contacts.common.model.account.AccountType; +import com.google.i18n.phonenumbers.NumberParseException; +import com.google.i18n.phonenumbers.PhoneNumberUtil; + +/** Shared static contact utility methods. */ +public class MoreContactUtils { + + private static final String WAIT_SYMBOL_AS_STRING = String.valueOf(PhoneNumberUtils.WAIT); + + /** + * Returns true if two data with mimetypes which represent values in contact entries are + * considered equal for collapsing in the GUI. For caller-id, use {@link + * android.telephony.PhoneNumberUtils#compare(android.content.Context, String, String)} instead + */ + public static boolean shouldCollapse( + CharSequence mimetype1, CharSequence data1, CharSequence mimetype2, CharSequence data2) { + // different mimetypes? don't collapse + if (!TextUtils.equals(mimetype1, mimetype2)) { + return false; + } + + // exact same string? good, bail out early + if (TextUtils.equals(data1, data2)) { + return true; + } + + // so if either is null, these two must be different + if (data1 == null || data2 == null) { + return false; + } + + // if this is not about phone numbers, we know this is not a match (of course, some + // mimetypes could have more sophisticated matching is the future, e.g. addresses) + if (!TextUtils.equals(ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE, mimetype1)) { + return false; + } + + return shouldCollapsePhoneNumbers(data1.toString(), data2.toString()); + } + + // TODO: Move this to PhoneDataItem.shouldCollapse override + private static boolean shouldCollapsePhoneNumbers(String number1, String number2) { + // Work around to address b/20724444. We want to distinguish between #555, *555 and 555. + // This makes no attempt to distinguish between 555 and 55*5, since 55*5 is an improbable + // number. PhoneNumberUtil already distinguishes between 555 and 55#5. + if (number1.contains("#") != number2.contains("#") + || number1.contains("*") != number2.contains("*")) { + return false; + } + + // Now do the full phone number thing. split into parts, separated by waiting symbol + // and compare them individually + final String[] dataParts1 = number1.split(WAIT_SYMBOL_AS_STRING); + final String[] dataParts2 = number2.split(WAIT_SYMBOL_AS_STRING); + if (dataParts1.length != dataParts2.length) { + return false; + } + final PhoneNumberUtil util = PhoneNumberUtil.getInstance(); + for (int i = 0; i < dataParts1.length; i++) { + // Match phone numbers represented by keypad letters, in which case prefer the + // phone number with letters. + final String dataPart1 = PhoneNumberUtils.convertKeypadLettersToDigits(dataParts1[i]); + final String dataPart2 = dataParts2[i]; + + // substrings equal? shortcut, don't parse + if (TextUtils.equals(dataPart1, dataPart2)) { + continue; + } + + // do a full parse of the numbers + final PhoneNumberUtil.MatchType result = util.isNumberMatch(dataPart1, dataPart2); + switch (result) { + case NOT_A_NUMBER: + // don't understand the numbers? let's play it safe + return false; + case NO_MATCH: + return false; + case EXACT_MATCH: + break; + case NSN_MATCH: + try { + // For NANP phone numbers, match when one has +1 and the other does not. + // In this case, prefer the +1 version. + if (util.parse(dataPart1, null).getCountryCode() == 1) { + // At this point, the numbers can be either case 1 or 2 below.... + // + // case 1) + // +14155551212 <--- country code 1 + // 14155551212 <--- 1 is trunk prefix, not country code + // + // and + // + // case 2) + // +14155551212 + // 4155551212 + // + // From b/7519057, case 2 needs to be equal. But also that bug, case 3 + // below should not be equal. + // + // case 3) + // 14155551212 + // 4155551212 + // + // So in order to make sure transitive equality is valid, case 1 cannot + // be equal. Otherwise, transitive equality breaks and the following + // would all be collapsed: + // 4155551212 | + // 14155551212 |----> +14155551212 + // +14155551212 | + // + // With transitive equality, the collapsed values should be: + // 4155551212 | 14155551212 + // 14155551212 |----> +14155551212 + // +14155551212 | + + // Distinguish between case 1 and 2 by checking for trunk prefix '1' + // at the start of number 2. + if (dataPart2.trim().charAt(0) == '1') { + // case 1 + return false; + } + break; + } + } catch (NumberParseException e) { + // This is the case where the first number does not have a country code. + // examples: + // (123) 456-7890 & 123-456-7890 (collapse) + // 0049 (8092) 1234 & +49/80921234 (unit test says do not collapse) + + // Check the second number. If it also does not have a country code, then + // we should collapse. If it has a country code, then it's a different + // number and we should not collapse (this conclusion is based on an + // existing unit test). + try { + util.parse(dataPart2, null); + } catch (NumberParseException e2) { + // Number 2 also does not have a country. Collapse. + break; + } + } + return false; + case SHORT_NSN_MATCH: + return false; + default: + throw new IllegalStateException("Unknown result value from phone number " + "library"); + } + } + return true; + } + + /** + * Returns the {@link android.graphics.Rect} with left, top, right, and bottom coordinates that + * are equivalent to the given {@link android.view.View}'s bounds. This is equivalent to how the + * target {@link android.graphics.Rect} is calculated in {@link + * android.provider.ContactsContract.QuickContact#showQuickContact}. + */ + public static Rect getTargetRectFromView(View view) { + final int[] pos = new int[2]; + view.getLocationOnScreen(pos); + + final Rect rect = new Rect(); + rect.left = pos[0]; + rect.top = pos[1]; + rect.right = pos[0] + view.getWidth(); + rect.bottom = pos[1] + view.getHeight(); + return rect; + } + + /** + * Returns a header view based on the R.layout.list_separator, where the containing {@link + * android.widget.TextView} is set using the given textResourceId. + */ + public static TextView createHeaderView(Context context, int textResourceId) { + final TextView textView = (TextView) View.inflate(context, R.layout.list_separator, null); + textView.setText(context.getString(textResourceId)); + return textView; + } + + /** + * Set the top padding on the header view dynamically, based on whether the header is in the first + * row or not. + */ + public static void setHeaderViewBottomPadding( + Context context, TextView textView, boolean isFirstRow) { + final int topPadding; + if (isFirstRow) { + topPadding = + (int) + context + .getResources() + .getDimension(R.dimen.frequently_contacted_title_top_margin_when_first_row); + } else { + topPadding = + (int) context.getResources().getDimension(R.dimen.frequently_contacted_title_top_margin); + } + textView.setPaddingRelative( + textView.getPaddingStart(), + topPadding, + textView.getPaddingEnd(), + textView.getPaddingBottom()); + } + + /** + * Returns the intent to launch for the given invitable account type and contact lookup URI. This + * will return null if the account type is not invitable (i.e. there is no {@link + * AccountType#getInviteContactActivityClassName()} or {@link + * AccountType#syncAdapterPackageName}). + */ + public static Intent getInvitableIntent(AccountType accountType, Uri lookupUri) { + String syncAdapterPackageName = accountType.syncAdapterPackageName; + String className = accountType.getInviteContactActivityClassName(); + if (TextUtils.isEmpty(syncAdapterPackageName) || TextUtils.isEmpty(className)) { + return null; + } + Intent intent = new Intent(); + intent.setClassName(syncAdapterPackageName, className); + + intent.setAction(ContactsContract.Intents.INVITE_CONTACT); + + // Data is the lookup URI. + intent.setData(lookupUri); + return intent; + } +} diff --git a/java/com/android/contacts/common/bindings/ContactsCommonBindings.java b/java/com/android/contacts/common/bindings/ContactsCommonBindings.java new file mode 100644 index 000000000..44be53b3f --- /dev/null +++ b/java/com/android/contacts/common/bindings/ContactsCommonBindings.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.android.contacts.common.bindings; + +import android.support.annotation.Nullable; + +/** Allows the container application to customize the contacts common library. */ +public interface ContactsCommonBindings { + + /** Builds a user agent string for the current application. */ + @Nullable + String getUserAgent(); +} diff --git a/java/com/android/contacts/common/bindings/ContactsCommonBindingsFactory.java b/java/com/android/contacts/common/bindings/ContactsCommonBindingsFactory.java new file mode 100644 index 000000000..8958ad997 --- /dev/null +++ b/java/com/android/contacts/common/bindings/ContactsCommonBindingsFactory.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.android.contacts.common.bindings; + +/** + * This interface should be implementated by the Application subclass. It allows the contacts common + * module to get references to the ContactsCommonBindings. + */ +public interface ContactsCommonBindingsFactory { + + ContactsCommonBindings newContactsCommonBindings(); +} diff --git a/java/com/android/contacts/common/bindings/ContactsCommonBindingsStub.java b/java/com/android/contacts/common/bindings/ContactsCommonBindingsStub.java new file mode 100644 index 000000000..f2e21b18e --- /dev/null +++ b/java/com/android/contacts/common/bindings/ContactsCommonBindingsStub.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.android.contacts.common.bindings; + +import android.support.annotation.Nullable; + +/** Default implementation for contacts common bindings. */ +public class ContactsCommonBindingsStub implements ContactsCommonBindings { + + @Override + @Nullable + public String getUserAgent() { + return null; + } +} diff --git a/java/com/android/contacts/common/compat/CallCompat.java b/java/com/android/contacts/common/compat/CallCompat.java new file mode 100644 index 000000000..641f7b1bd --- /dev/null +++ b/java/com/android/contacts/common/compat/CallCompat.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.compat; + +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.support.annotation.NonNull; +import android.telecom.Call; + +/** Compatibility utilities for android.telecom.Call */ +public class CallCompat { + + public static boolean canPullExternalCall(@NonNull android.telecom.Call call) { + return VERSION.SDK_INT >= VERSION_CODES.N_MR1 + && ((call.getDetails().getCallCapabilities() & Details.CAPABILITY_CAN_PULL_CALL) + == Details.CAPABILITY_CAN_PULL_CALL); + } + + /** android.telecom.Call.Details */ + public static class Details { + + public static final int PROPERTY_IS_EXTERNAL_CALL = Call.Details.PROPERTY_IS_EXTERNAL_CALL; + public static final int PROPERTY_ENTERPRISE_CALL = Call.Details.PROPERTY_ENTERPRISE_CALL; + public static final int CAPABILITY_CAN_PULL_CALL = Call.Details.CAPABILITY_CAN_PULL_CALL; + public static final int CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO = + Call.Details.CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO; + + public static final String EXTRA_ANSWERING_DROPS_FOREGROUND_CALL = + "android.telecom.extra.ANSWERING_DROPS_FG_CALL"; + } +} diff --git a/java/com/android/contacts/common/compat/CallableCompat.java b/java/com/android/contacts/common/compat/CallableCompat.java new file mode 100644 index 000000000..5e86f518e --- /dev/null +++ b/java/com/android/contacts/common/compat/CallableCompat.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.compat; + +import android.net.Uri; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.provider.ContactsContract.CommonDataKinds.Callable; + +public class CallableCompat { + + // TODO: Use N APIs + private static final Uri ENTERPRISE_CONTENT_FILTER_URI = + Uri.withAppendedPath(Callable.CONTENT_URI, "filter_enterprise"); + + public static Uri getContentFilterUri() { + if (VERSION.SDK_INT >= VERSION_CODES.N) { + return ENTERPRISE_CONTENT_FILTER_URI; + } + return Callable.CONTENT_FILTER_URI; + } +} diff --git a/java/com/android/contacts/common/compat/ContactsCompat.java b/java/com/android/contacts/common/compat/ContactsCompat.java new file mode 100644 index 000000000..39d0b55d3 --- /dev/null +++ b/java/com/android/contacts/common/compat/ContactsCompat.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.compat; + +import android.net.Uri; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.provider.ContactsContract; +import android.provider.ContactsContract.Contacts; +import com.android.dialer.compat.CompatUtils; + +/** Compatibility class for {@link ContactsContract.Contacts} */ +public class ContactsCompat { + + // TODO: Use N APIs + private static final Uri ENTERPRISE_CONTENT_FILTER_URI = + Uri.withAppendedPath(Contacts.CONTENT_URI, "filter_enterprise"); + // Copied from ContactsContract.Contacts#ENTERPRISE_CONTACT_ID_BASE, which is hidden. + private static final long ENTERPRISE_CONTACT_ID_BASE = 1000000000; + + /** Not instantiable. */ + private ContactsCompat() {} + + public static Uri getContentUri() { + if (VERSION.SDK_INT >= VERSION_CODES.N) { + return ENTERPRISE_CONTENT_FILTER_URI; + } + return Contacts.CONTENT_FILTER_URI; + } + + /** + * Return {@code true} if a contact ID is from the contacts provider on the enterprise profile. + */ + public static boolean isEnterpriseContactId(long contactId) { + if (CompatUtils.isLollipopCompatible()) { + return Contacts.isEnterpriseContactId(contactId); + } else { + // copied from ContactsContract.Contacts.isEnterpriseContactId + return (contactId >= ENTERPRISE_CONTACT_ID_BASE) + && (contactId < ContactsContract.Profile.MIN_ID); + } + } +} diff --git a/java/com/android/contacts/common/compat/DirectoryCompat.java b/java/com/android/contacts/common/compat/DirectoryCompat.java new file mode 100644 index 000000000..85f4a4202 --- /dev/null +++ b/java/com/android/contacts/common/compat/DirectoryCompat.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.compat; + +import android.net.Uri; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.provider.ContactsContract.Directory; + +public class DirectoryCompat { + + public static Uri getContentUri() { + if (VERSION.SDK_INT >= VERSION_CODES.N) { + return Directory.ENTERPRISE_CONTENT_URI; + } + return Directory.CONTENT_URI; + } + + public static boolean isInvisibleDirectory(long directoryId) { + if (VERSION.SDK_INT >= VERSION_CODES.N) { + return (directoryId == Directory.LOCAL_INVISIBLE + || directoryId == Directory.ENTERPRISE_LOCAL_INVISIBLE); + } + return directoryId == Directory.LOCAL_INVISIBLE; + } + + public static boolean isRemoteDirectoryId(long directoryId) { + if (VERSION.SDK_INT >= VERSION_CODES.N) { + return Directory.isRemoteDirectoryId(directoryId); + } + return !(directoryId == Directory.DEFAULT || directoryId == Directory.LOCAL_INVISIBLE); + } + + public static boolean isEnterpriseDirectoryId(long directoryId) { + return VERSION.SDK_INT >= VERSION_CODES.N && Directory.isEnterpriseDirectoryId(directoryId); + } +} diff --git a/java/com/android/contacts/common/compat/PhoneAccountCompat.java b/java/com/android/contacts/common/compat/PhoneAccountCompat.java new file mode 100644 index 000000000..6a24ec033 --- /dev/null +++ b/java/com/android/contacts/common/compat/PhoneAccountCompat.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.contacts.common.compat; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; +import android.support.annotation.Nullable; +import android.telecom.PhoneAccount; +import android.util.Log; +import com.android.dialer.compat.CompatUtils; + +/** Compatiblity class for {@link android.telecom.PhoneAccount} */ +public class PhoneAccountCompat { + + private static final String TAG = PhoneAccountCompat.class.getSimpleName(); + + /** + * Gets the {@link Icon} associated with the given {@link PhoneAccount} + * + * @param phoneAccount the PhoneAccount from which to retrieve the Icon + * @return the Icon, or null + */ + @Nullable + public static Icon getIcon(@Nullable PhoneAccount phoneAccount) { + if (phoneAccount == null) { + return null; + } + + if (CompatUtils.isMarshmallowCompatible()) { + return phoneAccount.getIcon(); + } + + return null; + } + + /** + * Builds and returns an icon {@code Drawable} to represent this {@code PhoneAccount} in a user + * interface. + * + * @param phoneAccount the PhoneAccount from which to build the icon. + * @param context A {@code Context} to use for loading Drawables. + * @return An icon for this PhoneAccount, or null + */ + @Nullable + public static Drawable createIconDrawable( + @Nullable PhoneAccount phoneAccount, @Nullable Context context) { + if (phoneAccount == null || context == null) { + return null; + } + + if (CompatUtils.isMarshmallowCompatible()) { + return createIconDrawableMarshmallow(phoneAccount, context); + } + + if (CompatUtils.isLollipopMr1Compatible()) { + return createIconDrawableLollipopMr1(phoneAccount, context); + } + return null; + } + + @Nullable + private static Drawable createIconDrawableMarshmallow( + PhoneAccount phoneAccount, Context context) { + Icon accountIcon = getIcon(phoneAccount); + if (accountIcon == null) { + return null; + } + return accountIcon.loadDrawable(context); + } + + @Nullable + private static Drawable createIconDrawableLollipopMr1( + PhoneAccount phoneAccount, Context context) { + try { + return (Drawable) + PhoneAccount.class + .getMethod("createIconDrawable", Context.class) + .invoke(phoneAccount, context); + } catch (ReflectiveOperationException e) { + return null; + } catch (Throwable t) { + Log.e( + TAG, + "Unexpected exception when attempting to call " + + "android.telecom.PhoneAccount#createIconDrawable", + t); + return null; + } + } +} diff --git a/java/com/android/contacts/common/compat/PhoneCompat.java b/java/com/android/contacts/common/compat/PhoneCompat.java new file mode 100644 index 000000000..31db7b537 --- /dev/null +++ b/java/com/android/contacts/common/compat/PhoneCompat.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.compat; + +import android.net.Uri; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.provider.ContactsContract.CommonDataKinds.Phone; + +public class PhoneCompat { + + // TODO: Use N APIs + private static final Uri ENTERPRISE_CONTENT_FILTER_URI = + Uri.withAppendedPath(Phone.CONTENT_URI, "filter_enterprise"); + + public static Uri getContentFilterUri() { + if (VERSION.SDK_INT >= VERSION_CODES.N) { + return ENTERPRISE_CONTENT_FILTER_URI; + } + return Phone.CONTENT_FILTER_URI; + } +} diff --git a/java/com/android/contacts/common/compat/PhoneNumberUtilsCompat.java b/java/com/android/contacts/common/compat/PhoneNumberUtilsCompat.java new file mode 100644 index 000000000..960b340d8 --- /dev/null +++ b/java/com/android/contacts/common/compat/PhoneNumberUtilsCompat.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.compat; + +import android.telephony.PhoneNumberUtils; +import android.text.Spannable; +import android.text.TextUtils; +import android.text.style.TtsSpan; +import com.android.dialer.compat.CompatUtils; +import com.google.i18n.phonenumbers.NumberParseException; +import com.google.i18n.phonenumbers.PhoneNumberUtil; +import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber; + +/** + * This class contains static utility methods extracted from PhoneNumberUtils, and the methods were + * added in API level 23. In this way, we could enable the corresponding functionality for pre-M + * devices. We need maintain this class and keep it synced with PhoneNumberUtils. Another thing to + * keep in mind is that we use com.google.i18n rather than com.android.i18n in here, so we need make + * sure the application behavior is preserved. + */ +public class PhoneNumberUtilsCompat { + + /** Not instantiable. */ + private PhoneNumberUtilsCompat() {} + + public static String normalizeNumber(String phoneNumber) { + if (CompatUtils.isLollipopCompatible()) { + return PhoneNumberUtils.normalizeNumber(phoneNumber); + } else { + return normalizeNumberInternal(phoneNumber); + } + } + + /** Implementation copied from {@link PhoneNumberUtils#normalizeNumber} */ + private static String normalizeNumberInternal(String phoneNumber) { + if (TextUtils.isEmpty(phoneNumber)) { + return ""; + } + StringBuilder sb = new StringBuilder(); + int len = phoneNumber.length(); + for (int i = 0; i < len; i++) { + char c = phoneNumber.charAt(i); + // Character.digit() supports ASCII and Unicode digits (fullwidth, Arabic-Indic, etc.) + int digit = Character.digit(c, 10); + if (digit != -1) { + sb.append(digit); + } else if (sb.length() == 0 && c == '+') { + sb.append(c); + } else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) { + return normalizeNumber(PhoneNumberUtils.convertKeypadLettersToDigits(phoneNumber)); + } + } + return sb.toString(); + } + + public static String formatNumber( + String phoneNumber, String phoneNumberE164, String defaultCountryIso) { + if (CompatUtils.isLollipopCompatible()) { + return PhoneNumberUtils.formatNumber(phoneNumber, phoneNumberE164, defaultCountryIso); + } else { + // This method was deprecated in API level 21, so it's only used on pre-L SDKs. + return PhoneNumberUtils.formatNumber(phoneNumber); + } + } + + public static CharSequence createTtsSpannable(CharSequence phoneNumber) { + if (CompatUtils.isMarshmallowCompatible()) { + return PhoneNumberUtils.createTtsSpannable(phoneNumber); + } else { + return createTtsSpannableInternal(phoneNumber); + } + } + + public static TtsSpan createTtsSpan(String phoneNumber) { + if (CompatUtils.isMarshmallowCompatible()) { + return PhoneNumberUtils.createTtsSpan(phoneNumber); + } else if (CompatUtils.isLollipopCompatible()) { + return createTtsSpanLollipop(phoneNumber); + } else { + return null; + } + } + + /** Copied from {@link PhoneNumberUtils#createTtsSpannable} */ + private static CharSequence createTtsSpannableInternal(CharSequence phoneNumber) { + if (phoneNumber == null) { + return null; + } + Spannable spannable = Spannable.Factory.getInstance().newSpannable(phoneNumber); + addTtsSpanInternal(spannable, 0, spannable.length()); + return spannable; + } + + /** Compat method for addTtsSpan, see {@link PhoneNumberUtils#addTtsSpan} */ + public static void addTtsSpan(Spannable s, int start, int endExclusive) { + if (CompatUtils.isMarshmallowCompatible()) { + PhoneNumberUtils.addTtsSpan(s, start, endExclusive); + } else { + addTtsSpanInternal(s, start, endExclusive); + } + } + + /** Copied from {@link PhoneNumberUtils#addTtsSpan} */ + private static void addTtsSpanInternal(Spannable s, int start, int endExclusive) { + s.setSpan( + createTtsSpan(s.subSequence(start, endExclusive).toString()), + start, + endExclusive, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + /** Copied from {@link PhoneNumberUtils#createTtsSpan} */ + private static TtsSpan createTtsSpanLollipop(String phoneNumberString) { + if (phoneNumberString == null) { + return null; + } + + // Parse the phone number + final PhoneNumberUtil phoneNumberUtil = PhoneNumberUtil.getInstance(); + PhoneNumber phoneNumber = null; + try { + // Don't supply a defaultRegion so this fails for non-international numbers because + // we don't want to TalkBalk to read a country code (e.g. +1) if it is not already + // present + phoneNumber = phoneNumberUtil.parse(phoneNumberString, /* defaultRegion */ null); + } catch (NumberParseException ignored) { + } + + // Build a telephone tts span + final TtsSpan.TelephoneBuilder builder = new TtsSpan.TelephoneBuilder(); + if (phoneNumber == null) { + // Strip separators otherwise TalkBack will be silent + // (this behavior was observed with TalkBalk 4.0.2 from their alpha channel) + builder.setNumberParts(splitAtNonNumerics(phoneNumberString)); + } else { + if (phoneNumber.hasCountryCode()) { + builder.setCountryCode(Integer.toString(phoneNumber.getCountryCode())); + } + builder.setNumberParts(Long.toString(phoneNumber.getNationalNumber())); + } + return builder.build(); + } + + /** + * Split a phone number using spaces, ignoring anything that is not a digit + * + * @param number A {@code CharSequence} before splitting, e.g., "+20(123)-456#" + * @return A {@code String} after splitting, e.g., "20 123 456". + */ + private static String splitAtNonNumerics(CharSequence number) { + StringBuilder sb = new StringBuilder(number.length()); + for (int i = 0; i < number.length(); i++) { + sb.append(PhoneNumberUtils.isISODigit(number.charAt(i)) ? number.charAt(i) : " "); + } + // It is very important to remove extra spaces. At time of writing, any leading or trailing + // spaces, or any sequence of more than one space, will confuse TalkBack and cause the TTS + // span to be non-functional! + return sb.toString().replaceAll(" +", " ").trim(); + } +} diff --git a/java/com/android/contacts/common/compat/TelephonyManagerCompat.java b/java/com/android/contacts/common/compat/TelephonyManagerCompat.java new file mode 100644 index 000000000..c8665af51 --- /dev/null +++ b/java/com/android/contacts/common/compat/TelephonyManagerCompat.java @@ -0,0 +1,213 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.compat; + +import android.net.Uri; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.support.annotation.Nullable; +import android.telecom.PhoneAccountHandle; +import android.telephony.TelephonyManager; +import com.android.dialer.common.Assert; +import com.android.dialer.common.LogUtil; +import com.android.dialer.compat.CompatUtils; +import java.lang.reflect.InvocationTargetException; + +public class TelephonyManagerCompat { + + // TODO: Use public API for these constants when available + public static final String EVENT_HANDOVER_VIDEO_FROM_WIFI_TO_LTE = + "android.telephony.event.EVENT_HANDOVER_VIDEO_FROM_WIFI_TO_LTE"; + public static final String EVENT_HANDOVER_TO_WIFI_FAILED = + "android.telephony.event.EVENT_HANDOVER_TO_WIFI_FAILED"; + public static final String EVENT_CALL_REMOTELY_HELD = "android.telecom.event.CALL_REMOTELY_HELD"; + public static final String EVENT_CALL_REMOTELY_UNHELD = + "android.telecom.event.CALL_REMOTELY_UNHELD"; + + public static final String TELEPHONY_MANAGER_CLASS = "android.telephony.TelephonyManager"; + + /** + * @param telephonyManager The telephony manager instance to use for method calls. + * @return true if the current device is "voice capable". + *

"Voice capable" means that this device supports circuit-switched (i.e. voice) phone + * calls over the telephony network, and is allowed to display the in-call UI while a cellular + * voice call is active. This will be false on "data only" devices which can't make voice + * calls and don't support any in-call UI. + *

Note: the meaning of this flag is subtly different from the + * PackageManager.FEATURE_TELEPHONY system feature, which is available on any device with a + * telephony radio, even if the device is data-only. + */ + public static boolean isVoiceCapable(@Nullable TelephonyManager telephonyManager) { + if (telephonyManager == null) { + return false; + } + if (CompatUtils.isLollipopMr1Compatible() + || CompatUtils.isMethodAvailable(TELEPHONY_MANAGER_CLASS, "isVoiceCapable")) { + // isVoiceCapable was unhidden in L-MR1 + return telephonyManager.isVoiceCapable(); + } + final int phoneType = telephonyManager.getPhoneType(); + return phoneType == TelephonyManager.PHONE_TYPE_CDMA + || phoneType == TelephonyManager.PHONE_TYPE_GSM; + } + + /** + * Returns the number of phones available. Returns 1 for Single standby mode (Single SIM + * functionality) Returns 2 for Dual standby mode.(Dual SIM functionality) + * + *

Returns 1 if the method or telephonyManager is not available. + * + * @param telephonyManager The telephony manager instance to use for method calls. + */ + public static int getPhoneCount(@Nullable TelephonyManager telephonyManager) { + if (telephonyManager == null) { + return 1; + } + if (CompatUtils.isMarshmallowCompatible() + || CompatUtils.isMethodAvailable(TELEPHONY_MANAGER_CLASS, "getPhoneCount")) { + return telephonyManager.getPhoneCount(); + } + return 1; + } + + /** + * Returns the unique device ID of a subscription, for example, the IMEI for GSM and the MEID for + * CDMA phones. Return null if device ID is not available. + * + *

Requires Permission: {@link android.Manifest.permission#READ_PHONE_STATE READ_PHONE_STATE} + * + * @param telephonyManager The telephony manager instance to use for method calls. + * @param slotId of which deviceID is returned + */ + public static String getDeviceId(@Nullable TelephonyManager telephonyManager, int slotId) { + if (telephonyManager == null) { + return null; + } + if (CompatUtils.isMarshmallowCompatible() + || CompatUtils.isMethodAvailable(TELEPHONY_MANAGER_CLASS, "getDeviceId", Integer.class)) { + return telephonyManager.getDeviceId(slotId); + } + return null; + } + + /** + * Whether the phone supports TTY mode. + * + * @param telephonyManager The telephony manager instance to use for method calls. + * @return {@code true} if the device supports TTY mode, and {@code false} otherwise. + */ + public static boolean isTtyModeSupported(@Nullable TelephonyManager telephonyManager) { + if (telephonyManager == null) { + return false; + } + if (CompatUtils.isMarshmallowCompatible() + || CompatUtils.isMethodAvailable(TELEPHONY_MANAGER_CLASS, "isTtyModeSupported")) { + return telephonyManager.isTtyModeSupported(); + } + return false; + } + + /** + * Whether the phone supports hearing aid compatibility. + * + * @param telephonyManager The telephony manager instance to use for method calls. + * @return {@code true} if the device supports hearing aid compatibility, and {@code false} + * otherwise. + */ + public static boolean isHearingAidCompatibilitySupported( + @Nullable TelephonyManager telephonyManager) { + if (telephonyManager == null) { + return false; + } + if (CompatUtils.isMarshmallowCompatible() + || CompatUtils.isMethodAvailable( + TELEPHONY_MANAGER_CLASS, "isHearingAidCompatibilitySupported")) { + return telephonyManager.isHearingAidCompatibilitySupported(); + } + return false; + } + + /** + * Returns the URI for the per-account voicemail ringtone set in Phone settings. + * + * @param telephonyManager The telephony manager instance to use for method calls. + * @param accountHandle The handle for the {@link android.telecom.PhoneAccount} for which to + * retrieve the voicemail ringtone. + * @return The URI for the ringtone to play when receiving a voicemail from a specific + * PhoneAccount. + */ + @Nullable + public static Uri getVoicemailRingtoneUri( + TelephonyManager telephonyManager, PhoneAccountHandle accountHandle) { + if (VERSION.SDK_INT < VERSION_CODES.N) { + return null; + } + return telephonyManager.getVoicemailRingtoneUri(accountHandle); + } + + /** + * Returns whether vibration is set for voicemail notification in Phone settings. + * + * @param telephonyManager The telephony manager instance to use for method calls. + * @param accountHandle The handle for the {@link android.telecom.PhoneAccount} for which to + * retrieve the voicemail vibration setting. + * @return {@code true} if the vibration is set for this PhoneAccount, {@code false} otherwise. + */ + public static boolean isVoicemailVibrationEnabled( + TelephonyManager telephonyManager, PhoneAccountHandle accountHandle) { + return VERSION.SDK_INT < VERSION_CODES.N + || telephonyManager.isVoicemailVibrationEnabled(accountHandle); + } + + /** + * This method uses a new system API to enable or disable visual voicemail. TODO: restrict + * to N MR1, not needed in future SDK. + */ + public static void setVisualVoicemailEnabled( + TelephonyManager telephonyManager, PhoneAccountHandle handle, boolean enabled) { + if (VERSION.SDK_INT < VERSION_CODES.N_MR1) { + Assert.fail("setVisualVoicemailEnabled called on pre-NMR1"); + } + try { + TelephonyManager.class + .getMethod("setVisualVoicemailEnabled", PhoneAccountHandle.class, boolean.class) + .invoke(telephonyManager, handle, enabled); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + LogUtil.e("TelephonyManagerCompat.setVisualVoicemailEnabled", "failed", e); + } + } + + /** + * This method uses a new system API to check if visual voicemail is enabled TODO: restrict + * to N MR1, not needed in future SDK. + */ + public static boolean isVisualVoicemailEnabled( + TelephonyManager telephonyManager, PhoneAccountHandle handle) { + if (VERSION.SDK_INT < VERSION_CODES.N_MR1) { + Assert.fail("isVisualVoicemailEnabled called on pre-NMR1"); + } + try { + return (boolean) + TelephonyManager.class + .getMethod("isVisualVoicemailEnabled", PhoneAccountHandle.class) + .invoke(telephonyManager, handle); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + LogUtil.e("TelephonyManagerCompat.setVisualVoicemailEnabled", "failed", e); + } + return false; + } +} diff --git a/java/com/android/contacts/common/compat/telecom/TelecomManagerCompat.java b/java/com/android/contacts/common/compat/telecom/TelecomManagerCompat.java new file mode 100644 index 000000000..5687f6fbf --- /dev/null +++ b/java/com/android/contacts/common/compat/telecom/TelecomManagerCompat.java @@ -0,0 +1,302 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.contacts.common.compat.telecom; + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.support.annotation.Nullable; +import android.telecom.PhoneAccount; +import android.telecom.PhoneAccountHandle; +import android.telecom.TelecomManager; +import android.telephony.PhoneNumberUtils; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import com.android.dialer.compat.CompatUtils; +import java.util.ArrayList; +import java.util.List; + +/** Compatibility class for {@link android.telecom.TelecomManager}. */ +public class TelecomManagerCompat { + + public static final String TELECOM_MANAGER_CLASS = "android.telecom.TelecomManager"; + + // TODO: remove once this is available in android.telecom.Call + // b/33779976 + public static final String EXTRA_LAST_EMERGENCY_CALLBACK_TIME_MILLIS = + "android.telecom.extra.LAST_EMERGENCY_CALLBACK_TIME_MILLIS"; + + /** + * Places a new outgoing call to the provided address using the system telecom service with the + * specified intent. + * + * @param activity {@link Activity} used to start another activity for the given intent + * @param telecomManager the {@link TelecomManager} used to place a call, if possible + * @param intent the intent for the call + */ + public static void placeCall( + @Nullable Activity activity, + @Nullable TelecomManager telecomManager, + @Nullable Intent intent) { + if (activity == null || telecomManager == null || intent == null) { + return; + } + if (CompatUtils.isMarshmallowCompatible()) { + telecomManager.placeCall(intent.getData(), intent.getExtras()); + return; + } + activity.startActivityForResult(intent, 0); + } + + /** + * Get the URI for running an adn query. + * + * @param telecomManager the {@link TelecomManager} used for method calls, if possible. + * @param accountHandle The handle for the account to derive an adn query URI for or {@code null} + * to return a URI which will use the default account. + * @return The URI (with the content:// scheme) specific to the specified {@link PhoneAccount} for + * the the content retrieve. + */ + public static Uri getAdnUriForPhoneAccount( + @Nullable TelecomManager telecomManager, PhoneAccountHandle accountHandle) { + if (telecomManager != null + && (CompatUtils.isMarshmallowCompatible() + || CompatUtils.isMethodAvailable( + TELECOM_MANAGER_CLASS, "getAdnUriForPhoneAccount", PhoneAccountHandle.class))) { + return telecomManager.getAdnUriForPhoneAccount(accountHandle); + } + return Uri.parse("content://icc/adn"); + } + + /** + * Returns a list of {@link PhoneAccountHandle}s which can be used to make and receive phone + * calls. The returned list includes only those accounts which have been explicitly enabled by the + * user. + * + * @param telecomManager the {@link TelecomManager} used for method calls, if possible. + * @return A list of PhoneAccountHandle objects. + */ + public static List getCallCapablePhoneAccounts( + @Nullable TelecomManager telecomManager) { + if (telecomManager != null + && (CompatUtils.isMarshmallowCompatible() + || CompatUtils.isMethodAvailable( + TELECOM_MANAGER_CLASS, "getCallCapablePhoneAccounts"))) { + return telecomManager.getCallCapablePhoneAccounts(); + } + return new ArrayList<>(); + } + + /** + * Used to determine the currently selected default dialer package. + * + * @param telecomManager the {@link TelecomManager} used for method calls, if possible. + * @return package name for the default dialer package or null if no package has been selected as + * the default dialer. + */ + @Nullable + public static String getDefaultDialerPackage(@Nullable TelecomManager telecomManager) { + if (telecomManager != null && CompatUtils.isDefaultDialerCompatible()) { + return telecomManager.getDefaultDialerPackage(); + } + return null; + } + + /** + * Return the {@link PhoneAccount} which will be used to place outgoing calls to addresses with + * the specified {@code uriScheme}. This PhoneAccount will always be a member of the list which is + * returned from invoking {@link TelecomManager#getCallCapablePhoneAccounts()}. The specific + * account returned depends on the following priorities: + * + *

1. If the user-selected default PhoneAccount supports the specified scheme, it will be + * returned. 2. If there exists only one PhoneAccount that supports the specified scheme, it will + * be returned. + * + *

If no PhoneAccount fits the criteria above, this method will return {@code null}. + * + * @param telecomManager the {@link TelecomManager} used for method calls, if possible. + * @param uriScheme The URI scheme. + * @return The {@link PhoneAccountHandle} corresponding to the account to be used. + */ + @Nullable + public static PhoneAccountHandle getDefaultOutgoingPhoneAccount( + @Nullable TelecomManager telecomManager, @Nullable String uriScheme) { + if (telecomManager != null + && (CompatUtils.isMarshmallowCompatible() + || CompatUtils.isMethodAvailable( + TELECOM_MANAGER_CLASS, "getDefaultOutgoingPhoneAccount", String.class))) { + return telecomManager.getDefaultOutgoingPhoneAccount(uriScheme); + } + return null; + } + + /** + * Return the line 1 phone number for given phone account. + * + * @param telecomManager the {@link TelecomManager} to use in the event that {@link + * TelecomManager#getLine1Number(PhoneAccountHandle)} is available + * @param telephonyManager the {@link TelephonyManager} to use if TelecomManager#getLine1Number is + * unavailable + * @param phoneAccountHandle the phoneAccountHandle upon which to check the line one number + * @return the line one number + */ + @Nullable + public static String getLine1Number( + @Nullable TelecomManager telecomManager, + @Nullable TelephonyManager telephonyManager, + @Nullable PhoneAccountHandle phoneAccountHandle) { + if (telecomManager != null && CompatUtils.isMarshmallowCompatible()) { + return telecomManager.getLine1Number(phoneAccountHandle); + } + if (telephonyManager != null) { + return telephonyManager.getLine1Number(); + } + return null; + } + + /** + * Return whether a given phone number is the configured voicemail number for a particular phone + * account. + * + * @param telecomManager the {@link TelecomManager} to use for checking the number. + * @param accountHandle The handle for the account to check the voicemail number against + * @param number The number to look up. + */ + public static boolean isVoiceMailNumber( + @Nullable TelecomManager telecomManager, + @Nullable PhoneAccountHandle accountHandle, + @Nullable String number) { + if (telecomManager != null + && (CompatUtils.isMarshmallowCompatible() + || CompatUtils.isMethodAvailable( + TELECOM_MANAGER_CLASS, + "isVoiceMailNumber", + PhoneAccountHandle.class, + String.class))) { + return telecomManager.isVoiceMailNumber(accountHandle, number); + } + return PhoneNumberUtils.isVoiceMailNumber(number); + } + + /** + * Return the {@link PhoneAccount} for a specified {@link PhoneAccountHandle}. Object includes + * resources which can be used in a user interface. + * + * @param telecomManager the {@link TelecomManager} used for method calls, if possible. + * @param account The {@link PhoneAccountHandle}. + * @return The {@link PhoneAccount} object or null if it doesn't exist. + */ + @Nullable + public static PhoneAccount getPhoneAccount( + @Nullable TelecomManager telecomManager, @Nullable PhoneAccountHandle accountHandle) { + if (telecomManager != null + && (CompatUtils.isMethodAvailable( + TELECOM_MANAGER_CLASS, "getPhoneAccount", PhoneAccountHandle.class))) { + return telecomManager.getPhoneAccount(accountHandle); + } + return null; + } + + /** + * Return the voicemail number for a given phone account. + * + * @param telecomManager The {@link TelecomManager} object to use for retrieving the voicemail + * number if accountHandle is specified. + * @param telephonyManager The {@link TelephonyManager} object to use for retrieving the voicemail + * number if accountHandle is null. + * @param accountHandle The handle for the phone account. + * @return The voicemail number for the phone account, and {@code null} if one has not been + * configured. + */ + @Nullable + public static String getVoiceMailNumber( + @Nullable TelecomManager telecomManager, + @Nullable TelephonyManager telephonyManager, + @Nullable PhoneAccountHandle accountHandle) { + if (telecomManager != null + && (CompatUtils.isMethodAvailable( + TELECOM_MANAGER_CLASS, "getVoiceMailNumber", PhoneAccountHandle.class))) { + return telecomManager.getVoiceMailNumber(accountHandle); + } else if (telephonyManager != null) { + return telephonyManager.getVoiceMailNumber(); + } + return null; + } + + /** + * Processes the specified dial string as an MMI code. MMI codes are any sequence of characters + * entered into the dialpad that contain a "*" or "#". Some of these sequences launch special + * behavior through handled by Telephony. + * + * @param telecomManager The {@link TelecomManager} object to use for handling MMI. + * @param dialString The digits to dial. + * @return {@code true} if the digits were processed as an MMI code, {@code false} otherwise. + */ + public static boolean handleMmi( + @Nullable TelecomManager telecomManager, + @Nullable String dialString, + @Nullable PhoneAccountHandle accountHandle) { + if (telecomManager == null || TextUtils.isEmpty(dialString)) { + return false; + } + if (CompatUtils.isMarshmallowCompatible()) { + return telecomManager.handleMmi(dialString, accountHandle); + } + + Object handleMmiResult = + CompatUtils.invokeMethod( + telecomManager, + "handleMmi", + new Class[] {PhoneAccountHandle.class, String.class}, + new Object[] {accountHandle, dialString}); + if (handleMmiResult != null) { + return (boolean) handleMmiResult; + } + + return telecomManager.handleMmi(dialString); + } + + /** + * Silences the ringer if a ringing call exists. Noop if {@link TelecomManager#silenceRinger()} is + * unavailable. + * + * @param telecomManager the TelecomManager to use to silence the ringer. + */ + public static void silenceRinger(@Nullable TelecomManager telecomManager) { + if (telecomManager != null + && (CompatUtils.isMarshmallowCompatible() + || CompatUtils.isMethodAvailable(TELECOM_MANAGER_CLASS, "silenceRinger"))) { + telecomManager.silenceRinger(); + } + } + + /** + * Returns the current SIM call manager. Apps must be prepared for this method to return null, + * indicating that there currently exists no registered SIM call manager. + * + * @param telecomManager the {@link TelecomManager} to use to fetch the SIM call manager. + * @return The phone account handle of the current sim call manager. + */ + @Nullable + public static PhoneAccountHandle getSimCallManager(TelecomManager telecomManager) { + if (telecomManager != null + && (CompatUtils.isMarshmallowCompatible() + || CompatUtils.isMethodAvailable(TELECOM_MANAGER_CLASS, "getSimCallManager"))) { + return telecomManager.getSimCallManager(); + } + return null; + } +} diff --git a/java/com/android/contacts/common/database/ContactUpdateUtils.java b/java/com/android/contacts/common/database/ContactUpdateUtils.java new file mode 100644 index 000000000..1a9febc07 --- /dev/null +++ b/java/com/android/contacts/common/database/ContactUpdateUtils.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.contacts.common.database; + +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.provider.ContactsContract; +import android.util.Log; + +/** Static methods to update contact information. */ +public class ContactUpdateUtils { + + private static final String TAG = ContactUpdateUtils.class.getSimpleName(); + + public static void setSuperPrimary(Context context, long dataId) { + if (dataId == -1) { + Log.e(TAG, "Invalid arguments for setSuperPrimary request"); + return; + } + + // Update the primary values in the data record. + ContentValues values = new ContentValues(2); + values.put(ContactsContract.Data.IS_SUPER_PRIMARY, 1); + values.put(ContactsContract.Data.IS_PRIMARY, 1); + + context + .getContentResolver() + .update( + ContentUris.withAppendedId(ContactsContract.Data.CONTENT_URI, dataId), + values, + null, + null); + } +} diff --git a/java/com/android/contacts/common/database/EmptyCursor.java b/java/com/android/contacts/common/database/EmptyCursor.java new file mode 100644 index 000000000..c2b24cdf7 --- /dev/null +++ b/java/com/android/contacts/common/database/EmptyCursor.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.contacts.common.database; + +import android.database.AbstractCursor; +import android.database.CursorIndexOutOfBoundsException; + +/** + * A cursor that is empty. + * + *

If you want an empty cursor, this class is better than a MatrixCursor because it has less + * overhead. + */ +public final class EmptyCursor extends AbstractCursor { + + private String[] mColumns; + + public EmptyCursor(String[] columns) { + this.mColumns = columns; + } + + @Override + public int getCount() { + return 0; + } + + @Override + public String[] getColumnNames() { + return mColumns; + } + + @Override + public String getString(int column) { + throw cursorException(); + } + + @Override + public short getShort(int column) { + throw cursorException(); + } + + @Override + public int getInt(int column) { + throw cursorException(); + } + + @Override + public long getLong(int column) { + throw cursorException(); + } + + @Override + public float getFloat(int column) { + throw cursorException(); + } + + @Override + public double getDouble(int column) { + throw cursorException(); + } + + @Override + public boolean isNull(int column) { + throw cursorException(); + } + + private CursorIndexOutOfBoundsException cursorException() { + return new CursorIndexOutOfBoundsException("Operation not permitted on an empty cursor."); + } +} diff --git a/java/com/android/contacts/common/database/NoNullCursorAsyncQueryHandler.java b/java/com/android/contacts/common/database/NoNullCursorAsyncQueryHandler.java new file mode 100644 index 000000000..d5e61354a --- /dev/null +++ b/java/com/android/contacts/common/database/NoNullCursorAsyncQueryHandler.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.contacts.common.database; + +import android.content.AsyncQueryHandler; +import android.content.ContentResolver; +import android.database.Cursor; +import android.net.Uri; + +/** + * An {@AsyncQueryHandler} that will never return a null cursor. + * + *

Instead, will return a {@link Cursor} with 0 records. + */ +public abstract class NoNullCursorAsyncQueryHandler extends AsyncQueryHandler { + + public NoNullCursorAsyncQueryHandler(ContentResolver cr) { + super(cr); + } + + @Override + public void startQuery( + int token, + Object cookie, + Uri uri, + String[] projection, + String selection, + String[] selectionArgs, + String orderBy) { + final CookieWithProjection projectionCookie = new CookieWithProjection(cookie, projection); + super.startQuery(token, projectionCookie, uri, projection, selection, selectionArgs, orderBy); + } + + @Override + protected final void onQueryComplete(int token, Object cookie, Cursor cursor) { + CookieWithProjection projectionCookie = (CookieWithProjection) cookie; + + super.onQueryComplete(token, projectionCookie.originalCookie, cursor); + + if (cursor == null) { + cursor = new EmptyCursor(projectionCookie.projection); + } + onNotNullableQueryComplete(token, projectionCookie.originalCookie, cursor); + } + + protected abstract void onNotNullableQueryComplete(int token, Object cookie, Cursor cursor); + + /** Class to add projection to an existing cookie. */ + private static class CookieWithProjection { + + public final Object originalCookie; + public final String[] projection; + + public CookieWithProjection(Object cookie, String[] projection) { + this.originalCookie = cookie; + this.projection = projection; + } + } +} diff --git a/java/com/android/contacts/common/dialog/CallSubjectDialog.java b/java/com/android/contacts/common/dialog/CallSubjectDialog.java new file mode 100644 index 000000000..d2e3a2357 --- /dev/null +++ b/java/com/android/contacts/common/dialog/CallSubjectDialog.java @@ -0,0 +1,607 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.contacts.common.dialog; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.telecom.PhoneAccount; +import android.telecom.PhoneAccountHandle; +import android.telecom.TelecomManager; +import android.text.Editable; +import android.text.InputFilter; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.InputMethodManager; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.QuickContactBadge; +import android.widget.TextView; +import com.android.contacts.common.ContactPhotoManager; +import com.android.contacts.common.R; +import com.android.contacts.common.compat.telecom.TelecomManagerCompat; +import com.android.contacts.common.util.UriUtils; +import com.android.dialer.animation.AnimUtils; +import com.android.dialer.callintent.CallIntentBuilder; +import com.android.dialer.callintent.nano.CallInitiationType; +import com.android.dialer.common.LogUtil; +import com.android.dialer.compat.CompatUtils; +import com.android.dialer.util.ViewUtil; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +/** + * Implements a dialog which prompts for a call subject for an outgoing call. The dialog includes a + * pop up list of historical call subjects. + */ +public class CallSubjectDialog extends Activity { + + public static final String PREF_KEY_SUBJECT_HISTORY_COUNT = "subject_history_count"; + public static final String PREF_KEY_SUBJECT_HISTORY_ITEM = "subject_history_item"; + /** Activity intent argument bundle keys: */ + public static final String ARG_PHOTO_ID = "PHOTO_ID"; + + public static final String ARG_PHOTO_URI = "PHOTO_URI"; + public static final String ARG_CONTACT_URI = "CONTACT_URI"; + public static final String ARG_NAME_OR_NUMBER = "NAME_OR_NUMBER"; + public static final String ARG_IS_BUSINESS = "IS_BUSINESS"; + public static final String ARG_NUMBER = "NUMBER"; + public static final String ARG_DISPLAY_NUMBER = "DISPLAY_NUMBER"; + public static final String ARG_NUMBER_LABEL = "NUMBER_LABEL"; + public static final String ARG_PHONE_ACCOUNT_HANDLE = "PHONE_ACCOUNT_HANDLE"; + private static final int CALL_SUBJECT_LIMIT = 16; + private static final int CALL_SUBJECT_HISTORY_SIZE = 5; + private int mAnimationDuration; + private Charset mMessageEncoding; + private View mBackgroundView; + private View mDialogView; + private QuickContactBadge mContactPhoto; + private TextView mNameView; + private TextView mNumberView; + private EditText mCallSubjectView; + private TextView mCharacterLimitView; + private View mHistoryButton; + private View mSendAndCallButton; + private ListView mSubjectList; + + private int mLimit = CALL_SUBJECT_LIMIT; + /** Handles changes to the text in the subject box. Ensures the character limit is updated. */ + private final TextWatcher mTextWatcher = + new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // no-op + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + updateCharacterLimit(); + } + + @Override + public void afterTextChanged(Editable s) { + // no-op + } + }; + + private int mPhotoSize; + private SharedPreferences mPrefs; + private List mSubjectHistory; + /** Handles displaying the list of past call subjects. */ + private final View.OnClickListener mHistoryOnClickListener = + new View.OnClickListener() { + @Override + public void onClick(View v) { + hideSoftKeyboard(CallSubjectDialog.this, mCallSubjectView); + showCallHistory(mSubjectList.getVisibility() == View.GONE); + } + }; + /** + * Handles auto-hiding the call history when user clicks in the call subject field to give it + * focus. + */ + private final View.OnClickListener mCallSubjectClickListener = + new View.OnClickListener() { + @Override + public void onClick(View v) { + if (mSubjectList.getVisibility() == View.VISIBLE) { + showCallHistory(false); + } + } + }; + + private long mPhotoID; + private Uri mPhotoUri; + private Uri mContactUri; + private String mNameOrNumber; + private boolean mIsBusiness; + private String mNumber; + private String mDisplayNumber; + private String mNumberLabel; + private PhoneAccountHandle mPhoneAccountHandle; + /** Handles starting a call with a call subject specified. */ + private final View.OnClickListener mSendAndCallOnClickListener = + new View.OnClickListener() { + @Override + public void onClick(View v) { + String subject = mCallSubjectView.getText().toString(); + Intent intent = + new CallIntentBuilder(mNumber, CallInitiationType.Type.CALL_SUBJECT_DIALOG) + .setPhoneAccountHandle(mPhoneAccountHandle) + .setCallSubject(subject) + .build(); + + TelecomManagerCompat.placeCall( + CallSubjectDialog.this, + (TelecomManager) getSystemService(Context.TELECOM_SERVICE), + intent); + + mSubjectHistory.add(subject); + saveSubjectHistory(mSubjectHistory); + finish(); + } + }; + /** Click listener which handles user clicks outside of the dialog. */ + private View.OnClickListener mBackgroundListener = + new View.OnClickListener() { + @Override + public void onClick(View v) { + finish(); + } + }; + /** + * Item click listener which handles user clicks on the items in the list view. Dismisses the + * activity, returning the subject to the caller and closing the activity with the {@link + * Activity#RESULT_OK} result code. + */ + private AdapterView.OnItemClickListener mItemClickListener = + new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView arg0, View view, int position, long arg3) { + mCallSubjectView.setText(mSubjectHistory.get(position)); + showCallHistory(false); + } + }; + + /** + * Show the call subject dialog given a phone number to dial (e.g. from the dialpad). + * + * @param activity The activity. + * @param number The number to dial. + */ + public static void start(Activity activity, String number) { + start( + activity, + -1 /* photoId */, + null /* photoUri */, + null /* contactUri */, + number /* nameOrNumber */, + false /* isBusiness */, + number /* number */, + null /* displayNumber */, + null /* numberLabel */, + null /* phoneAccountHandle */); + } + + /** + * Creates a call subject dialog. + * + * @param activity The current activity. + * @param photoId The photo ID (used to populate contact photo). + * @param photoUri The photo Uri (used to populate contact photo). + * @param contactUri The Contact URI (used so quick contact can be invoked from contact photo). + * @param nameOrNumber The name or number of the callee. + * @param isBusiness {@code true} if a business is being called (used for contact photo). + * @param number The raw number to dial. + * @param displayNumber The number to dial, formatted for display. + * @param numberLabel The label for the number (if from a contact). + * @param phoneAccountHandle The phone account handle. + */ + public static void start( + Activity activity, + long photoId, + Uri photoUri, + Uri contactUri, + String nameOrNumber, + boolean isBusiness, + String number, + String displayNumber, + String numberLabel, + PhoneAccountHandle phoneAccountHandle) { + Bundle arguments = new Bundle(); + arguments.putLong(ARG_PHOTO_ID, photoId); + arguments.putParcelable(ARG_PHOTO_URI, photoUri); + arguments.putParcelable(ARG_CONTACT_URI, contactUri); + arguments.putString(ARG_NAME_OR_NUMBER, nameOrNumber); + arguments.putBoolean(ARG_IS_BUSINESS, isBusiness); + arguments.putString(ARG_NUMBER, number); + arguments.putString(ARG_DISPLAY_NUMBER, displayNumber); + arguments.putString(ARG_NUMBER_LABEL, numberLabel); + arguments.putParcelable(ARG_PHONE_ACCOUNT_HANDLE, phoneAccountHandle); + start(activity, arguments); + } + + /** + * Shows the call subject dialog given a Bundle containing all the arguments required to display + * the dialog (e.g. from Quick Contacts). + * + * @param activity The activity. + * @param arguments The arguments bundle. + */ + public static void start(Activity activity, Bundle arguments) { + Intent intent = new Intent(activity, CallSubjectDialog.class); + intent.putExtras(arguments); + activity.startActivity(intent); + } + + /** + * Loads the subject history from shared preferences. + * + * @param prefs Shared preferences. + * @return List of subject history strings. + */ + public static List loadSubjectHistory(SharedPreferences prefs) { + int historySize = prefs.getInt(PREF_KEY_SUBJECT_HISTORY_COUNT, 0); + List subjects = new ArrayList(historySize); + + for (int ix = 0; ix < historySize; ix++) { + String historyItem = prefs.getString(PREF_KEY_SUBJECT_HISTORY_ITEM + ix, null); + if (!TextUtils.isEmpty(historyItem)) { + subjects.add(historyItem); + } + } + + return subjects; + } + + /** + * Creates the dialog, inflating the layout and populating it with the name and phone number. + * + * @param savedInstanceState The last saved instance state of the Fragment, or null if this is a + * freshly created Fragment. + * @return Dialog instance. + */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mAnimationDuration = getResources().getInteger(R.integer.call_subject_animation_duration); + mPrefs = PreferenceManager.getDefaultSharedPreferences(this); + mPhotoSize = + getResources().getDimensionPixelSize(R.dimen.call_subject_dialog_contact_photo_size); + readArguments(); + loadConfiguration(); + mSubjectHistory = loadSubjectHistory(mPrefs); + + setContentView(R.layout.dialog_call_subject); + getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); + mBackgroundView = findViewById(R.id.call_subject_dialog); + mBackgroundView.setOnClickListener(mBackgroundListener); + mDialogView = findViewById(R.id.dialog_view); + mContactPhoto = (QuickContactBadge) findViewById(R.id.contact_photo); + mNameView = (TextView) findViewById(R.id.name); + mNumberView = (TextView) findViewById(R.id.number); + mCallSubjectView = (EditText) findViewById(R.id.call_subject); + mCallSubjectView.addTextChangedListener(mTextWatcher); + mCallSubjectView.setOnClickListener(mCallSubjectClickListener); + InputFilter[] filters = new InputFilter[1]; + filters[0] = new InputFilter.LengthFilter(mLimit); + mCallSubjectView.setFilters(filters); + mCharacterLimitView = (TextView) findViewById(R.id.character_limit); + mHistoryButton = findViewById(R.id.history_button); + mHistoryButton.setOnClickListener(mHistoryOnClickListener); + mHistoryButton.setVisibility(mSubjectHistory.isEmpty() ? View.GONE : View.VISIBLE); + mSendAndCallButton = findViewById(R.id.send_and_call_button); + mSendAndCallButton.setOnClickListener(mSendAndCallOnClickListener); + mSubjectList = (ListView) findViewById(R.id.subject_list); + mSubjectList.setOnItemClickListener(mItemClickListener); + mSubjectList.setVisibility(View.GONE); + + updateContactInfo(); + updateCharacterLimit(); + } + + /** Populates the contact info fields based on the current contact information. */ + private void updateContactInfo() { + if (mContactUri != null) { + setPhoto(mPhotoID, mPhotoUri, mContactUri, mNameOrNumber, mIsBusiness); + } else { + mContactPhoto.setVisibility(View.GONE); + } + mNameView.setText(mNameOrNumber); + if (!TextUtils.isEmpty(mNumberLabel) && !TextUtils.isEmpty(mDisplayNumber)) { + mNumberView.setVisibility(View.VISIBLE); + mNumberView.setText( + getString(R.string.call_subject_type_and_number, mNumberLabel, mDisplayNumber)); + } else { + mNumberView.setVisibility(View.GONE); + mNumberView.setText(null); + } + } + + /** Reads arguments from the fragment arguments and populates the necessary instance variables. */ + private void readArguments() { + Bundle arguments = getIntent().getExtras(); + if (arguments == null) { + LogUtil.e("CallSubjectDialog.readArguments", "arguments cannot be null"); + return; + } + mPhotoID = arguments.getLong(ARG_PHOTO_ID); + mPhotoUri = arguments.getParcelable(ARG_PHOTO_URI); + mContactUri = arguments.getParcelable(ARG_CONTACT_URI); + mNameOrNumber = arguments.getString(ARG_NAME_OR_NUMBER); + mIsBusiness = arguments.getBoolean(ARG_IS_BUSINESS); + mNumber = arguments.getString(ARG_NUMBER); + mDisplayNumber = arguments.getString(ARG_DISPLAY_NUMBER); + mNumberLabel = arguments.getString(ARG_NUMBER_LABEL); + mPhoneAccountHandle = arguments.getParcelable(ARG_PHONE_ACCOUNT_HANDLE); + } + + /** + * Updates the character limit display, coloring the text RED when the limit is reached or + * exceeded. + */ + private void updateCharacterLimit() { + String subjectText = mCallSubjectView.getText().toString(); + final int length; + + // If a message encoding is specified, use that to count bytes in the message. + if (mMessageEncoding != null) { + length = subjectText.getBytes(mMessageEncoding).length; + } else { + // No message encoding specified, so just count characters entered. + length = subjectText.length(); + } + + mCharacterLimitView.setText(getString(R.string.call_subject_limit, length, mLimit)); + if (length >= mLimit) { + mCharacterLimitView.setTextColor( + getResources().getColor(R.color.call_subject_limit_exceeded)); + } else { + mCharacterLimitView.setTextColor( + getResources().getColor(R.color.dialer_secondary_text_color)); + } + } + + /** Sets the photo on the quick contact photo. */ + private void setPhoto( + long photoId, Uri photoUri, Uri contactUri, String displayName, boolean isBusiness) { + mContactPhoto.assignContactUri(contactUri); + if (CompatUtils.isLollipopCompatible()) { + mContactPhoto.setOverlay(null); + } + + int contactType; + if (isBusiness) { + contactType = ContactPhotoManager.TYPE_BUSINESS; + } else { + contactType = ContactPhotoManager.TYPE_DEFAULT; + } + + String lookupKey = null; + if (contactUri != null) { + lookupKey = UriUtils.getLookupKeyFromUri(contactUri); + } + + ContactPhotoManager.DefaultImageRequest request = + new ContactPhotoManager.DefaultImageRequest( + displayName, lookupKey, contactType, true /* isCircular */); + + if (photoId == 0 && photoUri != null) { + ContactPhotoManager.getInstance(this) + .loadPhoto( + mContactPhoto, + photoUri, + mPhotoSize, + false /* darkTheme */, + true /* isCircular */, + request); + } else { + ContactPhotoManager.getInstance(this) + .loadThumbnail( + mContactPhoto, photoId, false /* darkTheme */, true /* isCircular */, request); + } + } + + /** + * Saves the subject history list to shared prefs, removing older items so that there are only + * {@link #CALL_SUBJECT_HISTORY_SIZE} items at most. + * + * @param history The history. + */ + private void saveSubjectHistory(List history) { + // Remove oldest subject(s). + while (history.size() > CALL_SUBJECT_HISTORY_SIZE) { + history.remove(0); + } + + SharedPreferences.Editor editor = mPrefs.edit(); + int historyCount = 0; + for (String subject : history) { + if (!TextUtils.isEmpty(subject)) { + editor.putString(PREF_KEY_SUBJECT_HISTORY_ITEM + historyCount, subject); + historyCount++; + } + } + editor.putInt(PREF_KEY_SUBJECT_HISTORY_COUNT, historyCount); + editor.apply(); + } + + /** Hide software keyboard for the given {@link View}. */ + public void hideSoftKeyboard(Context context, View view) { + InputMethodManager imm = + (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) { + imm.hideSoftInputFromWindow(view.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); + } + } + + /** + * Hides or shows the call history list. + * + * @param show {@code true} if the call history should be shown, {@code false} otherwise. + */ + private void showCallHistory(final boolean show) { + // Bail early if the visibility has not changed. + if ((show && mSubjectList.getVisibility() == View.VISIBLE) + || (!show && mSubjectList.getVisibility() == View.GONE)) { + return; + } + + final int dialogStartingBottom = mDialogView.getBottom(); + if (show) { + // Showing the subject list; bind the list of history items to the list and show it. + ArrayAdapter adapter = + new ArrayAdapter( + CallSubjectDialog.this, R.layout.call_subject_history_list_item, mSubjectHistory); + mSubjectList.setAdapter(adapter); + mSubjectList.setVisibility(View.VISIBLE); + } else { + // Hiding the subject list. + mSubjectList.setVisibility(View.GONE); + } + + // Use a ViewTreeObserver so that we can animate between the pre-layout and post-layout + // states. + ViewUtil.doOnPreDraw( + mBackgroundView, + true, + new Runnable() { + @Override + public void run() { + // Determine the amount the dialog has shifted due to the relayout. + int shiftAmount = dialogStartingBottom - mDialogView.getBottom(); + + // If the dialog needs to be shifted, do that now. + if (shiftAmount != 0) { + // Start animation in translated state and animate to translationY 0. + mDialogView.setTranslationY(shiftAmount); + mDialogView + .animate() + .translationY(0) + .setInterpolator(AnimUtils.EASE_OUT_EASE_IN) + .setDuration(mAnimationDuration) + .start(); + } + + if (show) { + // Show the subject list. + mSubjectList.setTranslationY(mSubjectList.getHeight()); + + mSubjectList + .animate() + .translationY(0) + .setInterpolator(AnimUtils.EASE_OUT_EASE_IN) + .setDuration(mAnimationDuration) + .setListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + } + + @Override + public void onAnimationStart(Animator animation) { + super.onAnimationStart(animation); + mSubjectList.setVisibility(View.VISIBLE); + } + }) + .start(); + } else { + // Hide the subject list. + mSubjectList.setTranslationY(0); + + mSubjectList + .animate() + .translationY(mSubjectList.getHeight()) + .setInterpolator(AnimUtils.EASE_OUT_EASE_IN) + .setDuration(mAnimationDuration) + .setListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + mSubjectList.setVisibility(View.GONE); + } + + @Override + public void onAnimationStart(Animator animation) { + super.onAnimationStart(animation); + } + }) + .start(); + } + } + }); + } + + /** + * Loads the message encoding and maximum message length from the phone account extras for the + * current phone account. + */ + private void loadConfiguration() { + // Only attempt to load configuration from the phone account extras if the SDK is N or + // later. If we've got a prior SDK the default encoding and message length will suffice. + if (VERSION.SDK_INT < VERSION_CODES.N) { + return; + } + + if (mPhoneAccountHandle == null) { + return; + } + + TelecomManager telecomManager = (TelecomManager) getSystemService(Context.TELECOM_SERVICE); + final PhoneAccount account = telecomManager.getPhoneAccount(mPhoneAccountHandle); + + Bundle phoneAccountExtras = account.getExtras(); + if (phoneAccountExtras == null) { + return; + } + + // Get limit, if provided; otherwise default to existing value. + mLimit = phoneAccountExtras.getInt(PhoneAccount.EXTRA_CALL_SUBJECT_MAX_LENGTH, mLimit); + + // Get charset; default to none (e.g. count characters 1:1). + String charsetName = + phoneAccountExtras.getString(PhoneAccount.EXTRA_CALL_SUBJECT_CHARACTER_ENCODING); + + if (!TextUtils.isEmpty(charsetName)) { + try { + mMessageEncoding = Charset.forName(charsetName); + } catch (java.nio.charset.UnsupportedCharsetException uce) { + // Character set was invalid; log warning and fallback to none. + LogUtil.e("CallSubjectDialog.loadConfiguration", "invalid charset: " + charsetName); + mMessageEncoding = null; + } + } else { + // No character set specified, so count characters 1:1. + mMessageEncoding = null; + } + } +} diff --git a/java/com/android/contacts/common/dialog/ClearFrequentsDialog.java b/java/com/android/contacts/common/dialog/ClearFrequentsDialog.java new file mode 100644 index 000000000..e96496cda --- /dev/null +++ b/java/com/android/contacts/common/dialog/ClearFrequentsDialog.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.contacts.common.dialog; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.app.FragmentManager; +import android.app.ProgressDialog; +import android.content.ContentResolver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.os.AsyncTask; +import android.os.Bundle; +import android.provider.ContactsContract; +import com.android.contacts.common.R; +import com.android.dialer.util.PermissionsUtil; + +/** Dialog that clears the frequently contacted list after confirming with the user. */ +public class ClearFrequentsDialog extends DialogFragment { + + /** Preferred way to show this dialog */ + public static void show(FragmentManager fragmentManager) { + ClearFrequentsDialog dialog = new ClearFrequentsDialog(); + dialog.show(fragmentManager, "clearFrequents"); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Context context = getActivity().getApplicationContext(); + final ContentResolver resolver = getActivity().getContentResolver(); + final OnClickListener okListener = + new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + if (!PermissionsUtil.hasContactsPermissions(context)) { + return; + } + + final ProgressDialog progressDialog = + ProgressDialog.show( + getContext(), + getString(R.string.clearFrequentsProgress_title), + null, + true, + true); + + final AsyncTask task = + new AsyncTask() { + @Override + protected Void doInBackground(Void... params) { + resolver.delete( + ContactsContract.DataUsageFeedback.DELETE_USAGE_URI, null, null); + return null; + } + + @Override + protected void onPostExecute(Void result) { + progressDialog.dismiss(); + } + }; + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + }; + return new AlertDialog.Builder(getActivity()) + .setTitle(R.string.clearFrequentsConfirmation_title) + .setMessage(R.string.clearFrequentsConfirmation) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(android.R.string.ok, okListener) + .setCancelable(true) + .create(); + } +} diff --git a/java/com/android/contacts/common/extensions/PhoneDirectoryExtender.java b/java/com/android/contacts/common/extensions/PhoneDirectoryExtender.java new file mode 100644 index 000000000..2607ad19a --- /dev/null +++ b/java/com/android/contacts/common/extensions/PhoneDirectoryExtender.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.android.contacts.common.extensions; + +import android.content.Context; +import com.android.contacts.common.list.DirectoryPartition; +import java.util.List; + +/** An interface for adding extended phone directories. */ +public interface PhoneDirectoryExtender { + /** + * Return a list of extended directories to add. May return null if no directories are to be + * added. + */ + List getExtendedDirectories(Context context); +} diff --git a/java/com/android/contacts/common/extensions/PhoneDirectoryExtenderAccessor.java b/java/com/android/contacts/common/extensions/PhoneDirectoryExtenderAccessor.java new file mode 100644 index 000000000..84649f1ed --- /dev/null +++ b/java/com/android/contacts/common/extensions/PhoneDirectoryExtenderAccessor.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.android.contacts.common.extensions; + +import android.content.Context; +import android.support.annotation.NonNull; +import com.android.dialer.common.Assert; + +/** Accessor for the phone directory extender singleton. */ +public final class PhoneDirectoryExtenderAccessor { + + private static PhoneDirectoryExtender instance; + + private PhoneDirectoryExtenderAccessor() {} + + @NonNull + public static PhoneDirectoryExtender get(@NonNull Context context) { + Assert.isNotNull(context); + if (instance != null) { + return instance; + } + + Context application = context.getApplicationContext(); + if (application instanceof PhoneDirectoryExtenderFactory) { + instance = ((PhoneDirectoryExtenderFactory) application).newPhoneDirectoryExtender(); + } + + if (instance == null) { + instance = new PhoneDirectoryExtenderStub(); + } + return instance; + } +} diff --git a/java/com/android/contacts/common/extensions/PhoneDirectoryExtenderFactory.java b/java/com/android/contacts/common/extensions/PhoneDirectoryExtenderFactory.java new file mode 100644 index 000000000..9750ee300 --- /dev/null +++ b/java/com/android/contacts/common/extensions/PhoneDirectoryExtenderFactory.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.android.contacts.common.extensions; + +import android.support.annotation.NonNull; + +/** + * This interface should be implemented by the Application subclass. It allows the contacts module + * to get references to the PhoneDirectoryExtender. + */ +public interface PhoneDirectoryExtenderFactory { + + @NonNull + PhoneDirectoryExtender newPhoneDirectoryExtender(); +} diff --git a/java/com/android/contacts/common/extensions/PhoneDirectoryExtenderStub.java b/java/com/android/contacts/common/extensions/PhoneDirectoryExtenderStub.java new file mode 100644 index 000000000..95f971533 --- /dev/null +++ b/java/com/android/contacts/common/extensions/PhoneDirectoryExtenderStub.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.android.contacts.common.extensions; + +import android.content.Context; +import com.android.contacts.common.list.DirectoryPartition; +import java.util.Collections; +import java.util.List; + +/** No-op implementation for phone directory extender. */ +class PhoneDirectoryExtenderStub implements PhoneDirectoryExtender { + + @Override + public List getExtendedDirectories(Context context) { + return Collections.emptyList(); + } +} diff --git a/java/com/android/contacts/common/format/FormatUtils.java b/java/com/android/contacts/common/format/FormatUtils.java new file mode 100644 index 000000000..727c15b83 --- /dev/null +++ b/java/com/android/contacts/common/format/FormatUtils.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.contacts.common.format; + +import android.database.CharArrayBuffer; +import android.graphics.Typeface; +import android.support.annotation.VisibleForTesting; +import android.text.SpannableString; +import android.text.style.StyleSpan; +import java.util.Arrays; + +/** Assorted utility methods related to text formatting in Contacts. */ +public class FormatUtils { + + /** + * Finds the earliest point in buffer1 at which the first part of buffer2 matches. For example, + * overlapPoint("abcd", "cdef") == 2. + */ + public static int overlapPoint(CharArrayBuffer buffer1, CharArrayBuffer buffer2) { + if (buffer1 == null || buffer2 == null) { + return -1; + } + return overlapPoint( + Arrays.copyOfRange(buffer1.data, 0, buffer1.sizeCopied), + Arrays.copyOfRange(buffer2.data, 0, buffer2.sizeCopied)); + } + + /** + * Finds the earliest point in string1 at which the first part of string2 matches. For example, + * overlapPoint("abcd", "cdef") == 2. + */ + @VisibleForTesting + public static int overlapPoint(String string1, String string2) { + if (string1 == null || string2 == null) { + return -1; + } + return overlapPoint(string1.toCharArray(), string2.toCharArray()); + } + + /** + * Finds the earliest point in array1 at which the first part of array2 matches. For example, + * overlapPoint("abcd", "cdef") == 2. + */ + public static int overlapPoint(char[] array1, char[] array2) { + if (array1 == null || array2 == null) { + return -1; + } + int count1 = array1.length; + int count2 = array2.length; + + // Ignore matching tails of the two arrays. + while (count1 > 0 && count2 > 0 && array1[count1 - 1] == array2[count2 - 1]) { + count1--; + count2--; + } + + int size = count2; + for (int i = 0; i < count1; i++) { + if (i + size > count1) { + size = count1 - i; + } + int j; + for (j = 0; j < size; j++) { + if (array1[i + j] != array2[j]) { + break; + } + } + if (j == size) { + return i; + } + } + + return -1; + } + + /** + * Applies the given style to a range of the input CharSequence. + * + * @param style The style to apply (see the style constants in {@link Typeface}). + * @param input The CharSequence to style. + * @param start Starting index of the range to style (will be clamped to be a minimum of 0). + * @param end Ending index of the range to style (will be clamped to a maximum of the input + * length). + * @param flags Bitmask for configuring behavior of the span. See {@link android.text.Spanned}. + * @return The styled CharSequence. + */ + public static CharSequence applyStyleToSpan( + int style, CharSequence input, int start, int end, int flags) { + // Enforce bounds of the char sequence. + start = Math.max(0, start); + end = Math.min(input.length(), end); + SpannableString text = new SpannableString(input); + text.setSpan(new StyleSpan(style), start, end, flags); + return text; + } + + @VisibleForTesting + public static void copyToCharArrayBuffer(String text, CharArrayBuffer buffer) { + if (text != null) { + char[] data = buffer.data; + if (data == null || data.length < text.length()) { + buffer.data = text.toCharArray(); + } else { + text.getChars(0, text.length(), data, 0); + } + buffer.sizeCopied = text.length(); + } else { + buffer.sizeCopied = 0; + } + } + + /** Returns a String that represents the content of the given {@link CharArrayBuffer}. */ + @VisibleForTesting + public static String charArrayBufferToString(CharArrayBuffer buffer) { + return new String(buffer.data, 0, buffer.sizeCopied); + } + + /** + * Finds the index of the first word that starts with the given prefix. + * + *

If not found, returns -1. + * + * @param text the text in which to search for the prefix + * @param prefix the text to find, in upper case letters + */ + public static int indexOfWordPrefix(CharSequence text, String prefix) { + if (prefix == null || text == null) { + return -1; + } + + int textLength = text.length(); + int prefixLength = prefix.length(); + + if (prefixLength == 0 || textLength < prefixLength) { + return -1; + } + + int i = 0; + while (i < textLength) { + // Skip non-word characters + while (i < textLength && !Character.isLetterOrDigit(text.charAt(i))) { + i++; + } + + if (i + prefixLength > textLength) { + return -1; + } + + // Compare the prefixes + int j; + for (j = 0; j < prefixLength; j++) { + if (Character.toUpperCase(text.charAt(i + j)) != prefix.charAt(j)) { + break; + } + } + if (j == prefixLength) { + return i; + } + + // Skip this word + while (i < textLength && Character.isLetterOrDigit(text.charAt(i))) { + i++; + } + } + + return -1; + } +} diff --git a/java/com/android/contacts/common/format/TextHighlighter.java b/java/com/android/contacts/common/format/TextHighlighter.java new file mode 100644 index 000000000..30c03fdf3 --- /dev/null +++ b/java/com/android/contacts/common/format/TextHighlighter.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.format; + +import android.text.SpannableString; +import android.text.style.CharacterStyle; +import android.text.style.StyleSpan; +import android.widget.TextView; + +/** Highlights the text in a text field. */ +public class TextHighlighter { + + private static final boolean DEBUG = false; + private final String TAG = TextHighlighter.class.getSimpleName(); + private int mTextStyle; + + private CharacterStyle mTextStyleSpan; + + public TextHighlighter(int textStyle) { + mTextStyle = textStyle; + mTextStyleSpan = getStyleSpan(); + } + + /** + * Sets the text on the given text view, highlighting the word that matches the given prefix. + * + * @param view the view on which to set the text + * @param text the string to use as the text + * @param prefix the prefix to look for + */ + public void setPrefixText(TextView view, String text, String prefix) { + view.setText(applyPrefixHighlight(text, prefix)); + } + + private CharacterStyle getStyleSpan() { + return new StyleSpan(mTextStyle); + } + + /** + * Applies highlight span to the text. + * + * @param text Text sequence to be highlighted. + * @param start Start position of the highlight sequence. + * @param end End position of the highlight sequence. + */ + public void applyMaskingHighlight(SpannableString text, int start, int end) { + /** Sets text color of the masked locations to be highlighted. */ + text.setSpan(getStyleSpan(), start, end, 0); + } + + /** + * Returns a CharSequence which highlights the given prefix if found in the given text. + * + * @param text the text to which to apply the highlight + * @param prefix the prefix to look for + */ + public CharSequence applyPrefixHighlight(CharSequence text, String prefix) { + if (prefix == null) { + return text; + } + + // Skip non-word characters at the beginning of prefix. + int prefixStart = 0; + while (prefixStart < prefix.length() + && !Character.isLetterOrDigit(prefix.charAt(prefixStart))) { + prefixStart++; + } + final String trimmedPrefix = prefix.substring(prefixStart); + + int index = FormatUtils.indexOfWordPrefix(text, trimmedPrefix); + if (index != -1) { + final SpannableString result = new SpannableString(text); + result.setSpan(mTextStyleSpan, index, index + trimmedPrefix.length(), 0 /* flags */); + return result; + } else { + return text; + } + } +} diff --git a/java/com/android/contacts/common/format/testing/SpannedTestUtils.java b/java/com/android/contacts/common/format/testing/SpannedTestUtils.java new file mode 100644 index 000000000..293d9d5ad --- /dev/null +++ b/java/com/android/contacts/common/format/testing/SpannedTestUtils.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.format.testing; + +import android.test.suitebuilder.annotation.SmallTest; +import android.text.Html; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.TextUtils; +import android.text.style.StyleSpan; +import android.widget.TextView; +import junit.framework.Assert; + +/** Utility class to check the value of spanned text in text views. */ +@SmallTest +public class SpannedTestUtils { + + /** + * Checks that the text contained in the text view matches the given HTML text. + * + * @param expectedHtmlText the expected text to be in the text view + * @param textView the text view from which to get the text + */ + public static void checkHtmlText(String expectedHtmlText, TextView textView) { + String actualHtmlText = Html.toHtml((Spanned) textView.getText()); + if (TextUtils.isEmpty(expectedHtmlText)) { + // If the text is empty, it does not add the

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

" + expectedHtmlText + "

\n", actualHtmlText); + } + } + + /** + * Assert span exists in the correct location. + * + * @param seq The spannable string to check. + * @param start The starting index. + * @param end The ending index. + */ + public static void assertPrefixSpan(CharSequence seq, int start, int end) { + Assert.assertTrue(seq instanceof Spanned); + Spanned spannable = (Spanned) seq; + + if (start > 0) { + Assert.assertEquals(0, getNumForegroundColorSpansBetween(spannable, 0, start - 1)); + } + Assert.assertEquals(1, getNumForegroundColorSpansBetween(spannable, start, end)); + Assert.assertEquals( + 0, getNumForegroundColorSpansBetween(spannable, end + 1, spannable.length() - 1)); + } + + private static int getNumForegroundColorSpansBetween(Spanned value, int start, int end) { + return value.getSpans(start, end, StyleSpan.class).length; + } + + /** + * Asserts that the given character sequence is not a Spanned object and text is correct. + * + * @param seq The sequence to check. + * @param expected The expected text. + */ + public static void assertNotSpanned(CharSequence seq, String expected) { + Assert.assertFalse(seq instanceof Spanned); + Assert.assertEquals(expected, seq); + } + + public static int getNextTransition(SpannableString seq, int start) { + return seq.nextSpanTransition(start, seq.length(), StyleSpan.class); + } +} diff --git a/java/com/android/contacts/common/lettertiles/LetterTileDrawable.java b/java/com/android/contacts/common/lettertiles/LetterTileDrawable.java new file mode 100644 index 000000000..7e1839c1e --- /dev/null +++ b/java/com/android/contacts/common/lettertiles/LetterTileDrawable.java @@ -0,0 +1,382 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.lettertiles; + +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Outline; +import android.graphics.Paint; +import android.graphics.Paint.Align; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.support.annotation.IntDef; +import android.support.annotation.Nullable; +import android.text.TextUtils; +import com.android.contacts.common.R; +import com.android.dialer.common.Assert; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * A drawable that encapsulates all the functionality needed to display a letter tile to represent a + * contact image. + */ +public class LetterTileDrawable extends Drawable { + + /** + * ContactType indicates the avatar type of the contact. For a person or for the default when no + * name is provided, it is {@link #TYPE_DEFAULT}, otherwise, for a business it is {@link + * #TYPE_BUSINESS}, and voicemail contacts should use {@link #TYPE_VOICEMAIL}. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({TYPE_PERSON, TYPE_BUSINESS, TYPE_VOICEMAIL}) + public @interface ContactType {} + + /** Contact type constants */ + public static final int TYPE_PERSON = 1; + public static final int TYPE_BUSINESS = 2; + public static final int TYPE_VOICEMAIL = 3; + @ContactType public static final int TYPE_DEFAULT = TYPE_PERSON; + + /** + * Shape indicates the letter tile shape. It can be either a {@link #SHAPE_CIRCLE}, otherwise, it + * is a {@link #SHAPE_RECTANGLE}. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({SHAPE_CIRCLE, SHAPE_RECTANGLE}) + public @interface Shape {} + + /** Shape constants */ + public static final int SHAPE_CIRCLE = 1; + + public static final int SHAPE_RECTANGLE = 2; + + /** 54% opacity */ + private static final int ALPHA = 138; + + /** Reusable components to avoid new allocations */ + private static final Paint sPaint = new Paint(); + + private static final Rect sRect = new Rect(); + private static final char[] sFirstChar = new char[1]; + /** Letter tile */ + private static TypedArray sColors; + + private static int sDefaultColor; + private static int sTileFontColor; + private static float sLetterToTileRatio; + private static Bitmap sDefaultPersonAvatar; + private static Bitmap sDefaultBusinessAvatar; + private static Bitmap sDefaultVoicemailAvatar; + private static final String TAG = LetterTileDrawable.class.getSimpleName(); + private final Paint mPaint; + private int mContactType = TYPE_DEFAULT; + private float mScale = 1.0f; + private float mOffset = 0.0f; + private boolean mIsCircle = false; + + private int mColor; + private Character mLetter = null; + + private boolean mAvatarWasVoicemailOrBusiness = false; + private String mDisplayName; + + public LetterTileDrawable(final Resources res) { + if (sColors == null) { + sColors = res.obtainTypedArray(R.array.letter_tile_colors); + sDefaultColor = res.getColor(R.color.letter_tile_default_color); + sTileFontColor = res.getColor(R.color.letter_tile_font_color); + sLetterToTileRatio = res.getFraction(R.dimen.letter_to_tile_ratio, 1, 1); + sDefaultPersonAvatar = + BitmapFactory.decodeResource( + res, R.drawable.product_logo_avatar_anonymous_white_color_120); + sDefaultBusinessAvatar = + BitmapFactory.decodeResource(res, R.drawable.ic_business_white_120dp); + sDefaultVoicemailAvatar = BitmapFactory.decodeResource(res, R.drawable.ic_voicemail_avatar); + sPaint.setTypeface( + Typeface.create(res.getString(R.string.letter_tile_letter_font_family), Typeface.NORMAL)); + sPaint.setTextAlign(Align.CENTER); + sPaint.setAntiAlias(true); + } + mPaint = new Paint(); + mPaint.setFilterBitmap(true); + mPaint.setDither(true); + mColor = sDefaultColor; + } + + private static Bitmap getBitmapForContactType(int contactType) { + switch (contactType) { + case TYPE_BUSINESS: + return sDefaultBusinessAvatar; + case TYPE_VOICEMAIL: + return sDefaultVoicemailAvatar; + case TYPE_PERSON: + default: + return sDefaultPersonAvatar; + } + } + + private static boolean isEnglishLetter(final char c) { + return ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z'); + } + + @Override + public void draw(final Canvas canvas) { + final Rect bounds = getBounds(); + if (!isVisible() || bounds.isEmpty()) { + return; + } + // Draw letter tile. + drawLetterTile(canvas); + } + + /** + * Draw the bitmap onto the canvas at the current bounds taking into account the current scale. + */ + private void drawBitmap( + final Bitmap bitmap, final int width, final int height, final Canvas canvas) { + // The bitmap should be drawn in the middle of the canvas without changing its width to + // height ratio. + final Rect destRect = copyBounds(); + + // Crop the destination bounds into a square, scaled and offset as appropriate + final int halfLength = (int) (mScale * Math.min(destRect.width(), destRect.height()) / 2); + + destRect.set( + destRect.centerX() - halfLength, + (int) (destRect.centerY() - halfLength + mOffset * destRect.height()), + destRect.centerX() + halfLength, + (int) (destRect.centerY() + halfLength + mOffset * destRect.height())); + + // Source rectangle remains the entire bounds of the source bitmap. + sRect.set(0, 0, width, height); + + sPaint.setTextAlign(Align.CENTER); + sPaint.setAntiAlias(true); + sPaint.setAlpha(ALPHA); + + canvas.drawBitmap(bitmap, sRect, destRect, sPaint); + } + + private void drawLetterTile(final Canvas canvas) { + // Draw background color. + sPaint.setColor(mColor); + + sPaint.setAlpha(mPaint.getAlpha()); + final Rect bounds = getBounds(); + final int minDimension = Math.min(bounds.width(), bounds.height()); + + if (mIsCircle) { + canvas.drawCircle(bounds.centerX(), bounds.centerY(), minDimension / 2, sPaint); + } else { + canvas.drawRect(bounds, sPaint); + } + + // Draw letter/digit only if the first character is an english letter or there's a override + + if (mLetter != null) { + // Draw letter or digit. + sFirstChar[0] = mLetter; + + // Scale text by canvas bounds and user selected scaling factor + sPaint.setTextSize(mScale * sLetterToTileRatio * minDimension); + sPaint.getTextBounds(sFirstChar, 0, 1, sRect); + sPaint.setTypeface(Typeface.create("sans-serif", Typeface.NORMAL)); + sPaint.setColor(sTileFontColor); + sPaint.setAlpha(ALPHA); + + // Draw the letter in the canvas, vertically shifted up or down by the user-defined + // offset + canvas.drawText( + sFirstChar, + 0, + 1, + bounds.centerX(), + bounds.centerY() + mOffset * bounds.height() - sRect.exactCenterY(), + sPaint); + } else { + // Draw the default image if there is no letter/digit to be drawn + final Bitmap bitmap = getBitmapForContactType(mContactType); + drawBitmap(bitmap, bitmap.getWidth(), bitmap.getHeight(), canvas); + } + } + + public int getColor() { + return mColor; + } + + public LetterTileDrawable setColor(int color) { + mColor = color; + return this; + } + + /** Returns a deterministic color based on the provided contact identifier string. */ + private int pickColor(final String identifier) { + if (TextUtils.isEmpty(identifier) || mContactType == TYPE_VOICEMAIL) { + return sDefaultColor; + } + // String.hashCode() implementation is not supposed to change across java versions, so + // this should guarantee the same email address always maps to the same color. + // The email should already have been normalized by the ContactRequest. + final int color = Math.abs(identifier.hashCode()) % sColors.length(); + return sColors.getColor(color, sDefaultColor); + } + + @Override + public void setAlpha(final int alpha) { + mPaint.setAlpha(alpha); + } + + @Override + public void setColorFilter(final ColorFilter cf) { + mPaint.setColorFilter(cf); + } + + @Override + public int getOpacity() { + return android.graphics.PixelFormat.OPAQUE; + } + + @Override + public void getOutline(Outline outline) { + if (mIsCircle) { + outline.setOval(getBounds()); + } else { + outline.setRect(getBounds()); + } + + outline.setAlpha(1); + } + + /** + * Scale the drawn letter tile to a ratio of its default size + * + * @param scale The ratio the letter tile should be scaled to as a percentage of its default size, + * from a scale of 0 to 2.0f. The default is 1.0f. + */ + public LetterTileDrawable setScale(float scale) { + mScale = scale; + return this; + } + + /** + * Assigns the vertical offset of the position of the letter tile to the ContactDrawable + * + * @param offset The provided offset must be within the range of -0.5f to 0.5f. If set to -0.5f, + * the letter will be shifted upwards by 0.5 times the height of the canvas it is being drawn + * on, which means it will be drawn with the center of the letter starting at the top edge of + * the canvas. If set to 0.5f, the letter will be shifted downwards by 0.5 times the height of + * the canvas it is being drawn on, which means it will be drawn with the center of the letter + * starting at the bottom edge of the canvas. The default is 0.0f. + */ + public LetterTileDrawable setOffset(float offset) { + Assert.checkArgument(offset >= -0.5f && offset <= 0.5f); + mOffset = offset; + return this; + } + + public LetterTileDrawable setLetter(Character letter) { + mLetter = letter; + return this; + } + + public Character getLetter() { + return this.mLetter; + } + + private LetterTileDrawable setLetterAndColorFromContactDetails( + final String displayName, final String identifier) { + if (displayName != null && displayName.length() > 0 && isEnglishLetter(displayName.charAt(0))) { + mLetter = Character.toUpperCase(displayName.charAt(0)); + } else { + mLetter = null; + } + mColor = pickColor(identifier); + return this; + } + + public LetterTileDrawable setContactType(@ContactType int contactType) { + mContactType = contactType; + return this; + } + + @ContactType + public int getContactType() { + return this.mContactType; + } + + public LetterTileDrawable setIsCircular(boolean isCircle) { + mIsCircle = isCircle; + return this; + } + + /** + * Creates a canonical letter tile for use across dialer fragments. + * + * @param displayName The display name to produce the letter in the tile. Null values or numbers + * yield no letter. + * @param identifierForTileColor The string used to produce the tile color. + * @param shape The shape of the tile. + * @param contactType The type of contact, e.g. TYPE_VOICEMAIL. + * @return this + */ + public LetterTileDrawable setCanonicalDialerLetterTileDetails( + @Nullable final String displayName, + @Nullable final String identifierForTileColor, + @Shape final int shape, + final int contactType) { + setContactType(contactType); + /** + * During hangup, we lose the call state for special types of contacts, like voicemail. To help + * callers avoid extraneous LetterTileDrawable allocations, we keep track of the special case + * until we encounter a new display name. + */ + if (contactType == TYPE_VOICEMAIL || contactType == TYPE_BUSINESS) { + this.mAvatarWasVoicemailOrBusiness = true; + } else if (displayName != null && !displayName.equals(mDisplayName)) { + this.mAvatarWasVoicemailOrBusiness = false; + } + this.mDisplayName = displayName; + if (shape == SHAPE_CIRCLE) { + this.setIsCircular(true); + } else { + this.setIsCircular(false); + } + + /** + * To preserve style, we don't use contactType to set the tile icon. In the future, when all + * callers surface this detail, we can use this to better style the tile icon. + */ + if (mAvatarWasVoicemailOrBusiness) { + this.setLetterAndColorFromContactDetails(null, displayName); + return this; + } else { + if (identifierForTileColor != null) { + this.setLetterAndColorFromContactDetails(displayName, identifierForTileColor); + return this; + } else { + this.setLetterAndColorFromContactDetails(displayName, displayName); + return this; + } + } + } +} diff --git a/java/com/android/contacts/common/list/AutoScrollListView.java b/java/com/android/contacts/common/list/AutoScrollListView.java new file mode 100644 index 000000000..601abf528 --- /dev/null +++ b/java/com/android/contacts/common/list/AutoScrollListView.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.list; + +import android.content.Context; +import android.os.Build; +import android.util.AttributeSet; +import android.widget.ListView; + +/** + * A ListView that can be asked to scroll (smoothly or otherwise) to a specific position. This class + * takes advantage of similar functionality that exists in {@link ListView} and enhances it. + */ +public class AutoScrollListView extends ListView { + + /** Position the element at about 1/3 of the list height */ + private static final float PREFERRED_SELECTION_OFFSET_FROM_TOP = 0.33f; + + private int mRequestedScrollPosition = -1; + private boolean mSmoothScrollRequested; + + public AutoScrollListView(Context context) { + super(context); + } + + public AutoScrollListView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public AutoScrollListView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + /** + * Brings the specified position to view by optionally performing a jump-scroll maneuver: first it + * jumps to some position near the one requested and then does a smooth scroll to the requested + * position. This creates an impression of full smooth scrolling without actually traversing the + * entire list. If smooth scrolling is not requested, instantly positions the requested item at a + * preferred offset. + */ + public void requestPositionToScreen(int position, boolean smoothScroll) { + mRequestedScrollPosition = position; + mSmoothScrollRequested = smoothScroll; + requestLayout(); + } + + @Override + protected void layoutChildren() { + super.layoutChildren(); + if (mRequestedScrollPosition == -1) { + return; + } + + final int position = mRequestedScrollPosition; + mRequestedScrollPosition = -1; + + int firstPosition = getFirstVisiblePosition() + 1; + int lastPosition = getLastVisiblePosition(); + if (position >= firstPosition && position <= lastPosition) { + return; // Already on screen + } + + final int offset = (int) (getHeight() * PREFERRED_SELECTION_OFFSET_FROM_TOP); + if (!mSmoothScrollRequested) { + setSelectionFromTop(position, offset); + + // Since we have changed the scrolling position, we need to redo child layout + // Calling "requestLayout" in the middle of a layout pass has no effect, + // so we call layoutChildren explicitly + super.layoutChildren(); + + } else { + // We will first position the list a couple of screens before or after + // the new selection and then scroll smoothly to it. + int twoScreens = (lastPosition - firstPosition) * 2; + int preliminaryPosition; + if (position < firstPosition) { + preliminaryPosition = position + twoScreens; + if (preliminaryPosition >= getCount()) { + preliminaryPosition = getCount() - 1; + } + if (preliminaryPosition < firstPosition) { + setSelection(preliminaryPosition); + super.layoutChildren(); + } + } else { + preliminaryPosition = position - twoScreens; + if (preliminaryPosition < 0) { + preliminaryPosition = 0; + } + if (preliminaryPosition > lastPosition) { + setSelection(preliminaryPosition); + super.layoutChildren(); + } + } + + smoothScrollToPositionFromTop(position, offset); + } + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + + // Workaround for b/31160338 and b/32778636. + if (android.os.Build.VERSION.SDK_INT == Build.VERSION_CODES.N + || android.os.Build.VERSION.SDK_INT == Build.VERSION_CODES.N_MR1) { + layoutChildren(); + } + } +} diff --git a/java/com/android/contacts/common/list/ContactEntry.java b/java/com/android/contacts/common/list/ContactEntry.java new file mode 100644 index 000000000..e33165e45 --- /dev/null +++ b/java/com/android/contacts/common/list/ContactEntry.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.list; + +import android.net.Uri; +import android.provider.ContactsContract.PinnedPositions; +import android.text.TextUtils; +import com.android.contacts.common.preference.ContactsPreferences; + +/** Class to hold contact information */ +public class ContactEntry { + + public static final ContactEntry BLANK_ENTRY = new ContactEntry(); + private static final int UNSET_DISPLAY_ORDER_PREFERENCE = -1; + /** Primary name for a Contact */ + public String namePrimary; + /** Alternative name for a Contact, e.g. last name first */ + public String nameAlternative; + /** + * The user's preference on name display order, last name first or first time first. {@see + * ContactsPreferences} + */ + public int nameDisplayOrder = UNSET_DISPLAY_ORDER_PREFERENCE; + + public String phoneLabel; + public String phoneNumber; + public Uri photoUri; + public Uri lookupUri; + public String lookupKey; + public long id; + public int pinned = PinnedPositions.UNPINNED; + public boolean isFavorite = false; + public boolean isDefaultNumber = false; + + public String getPreferredDisplayName() { + if (nameDisplayOrder == UNSET_DISPLAY_ORDER_PREFERENCE + || nameDisplayOrder == ContactsPreferences.DISPLAY_ORDER_PRIMARY + || TextUtils.isEmpty(nameAlternative)) { + return namePrimary; + } + return nameAlternative; + } +} diff --git a/java/com/android/contacts/common/list/ContactEntryListAdapter.java b/java/com/android/contacts/common/list/ContactEntryListAdapter.java new file mode 100644 index 000000000..18bbae382 --- /dev/null +++ b/java/com/android/contacts/common/list/ContactEntryListAdapter.java @@ -0,0 +1,742 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.contacts.common.list; + +import android.content.Context; +import android.content.CursorLoader; +import android.content.res.Resources; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.provider.ContactsContract; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.Directory; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.QuickContactBadge; +import android.widget.SectionIndexer; +import android.widget.TextView; +import com.android.contacts.common.ContactPhotoManager; +import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest; +import com.android.contacts.common.ContactsUtils; +import com.android.contacts.common.R; +import com.android.contacts.common.compat.DirectoryCompat; +import com.android.contacts.common.util.SearchUtil; +import com.android.dialer.compat.CompatUtils; +import java.util.HashSet; + +/** + * Common base class for various contact-related lists, e.g. contact list, phone number list etc. + */ +public abstract class ContactEntryListAdapter extends IndexerListAdapter { + + /** + * Indicates whether the {@link Directory#LOCAL_INVISIBLE} directory should be included in the + * search. + */ + public static final boolean LOCAL_INVISIBLE_DIRECTORY_ENABLED = false; + + private static final String TAG = "ContactEntryListAdapter"; + private int mDisplayOrder; + private int mSortOrder; + + private boolean mDisplayPhotos; + private boolean mCircularPhotos = true; + private boolean mQuickContactEnabled; + private boolean mAdjustSelectionBoundsEnabled; + + /** The root view of the fragment that this adapter is associated with. */ + private View mFragmentRootView; + + private ContactPhotoManager mPhotoLoader; + + private String mQueryString; + private String mUpperCaseQueryString; + private boolean mSearchMode; + private int mDirectorySearchMode; + private int mDirectoryResultLimit = Integer.MAX_VALUE; + + private boolean mEmptyListEnabled = true; + + private boolean mSelectionVisible; + + private ContactListFilter mFilter; + private boolean mDarkTheme = false; + + /** Resource used to provide header-text for default filter. */ + private CharSequence mDefaultFilterHeaderText; + + public ContactEntryListAdapter(Context context) { + super(context); + setDefaultFilterHeaderText(R.string.local_search_label); + addPartitions(); + } + + /** + * @param fragmentRootView Root view of the fragment. This is used to restrict the scope of image + * loading requests that get cancelled on cursor changes. + */ + protected void setFragmentRootView(View fragmentRootView) { + mFragmentRootView = fragmentRootView; + } + + protected void setDefaultFilterHeaderText(int resourceId) { + mDefaultFilterHeaderText = getContext().getResources().getText(resourceId); + } + + @Override + protected ContactListItemView newView( + Context context, int partition, Cursor cursor, int position, ViewGroup parent) { + final ContactListItemView view = new ContactListItemView(context, null); + view.setIsSectionHeaderEnabled(isSectionHeaderDisplayEnabled()); + view.setAdjustSelectionBoundsEnabled(isAdjustSelectionBoundsEnabled()); + return view; + } + + @Override + protected void bindView(View itemView, int partition, Cursor cursor, int position) { + final ContactListItemView view = (ContactListItemView) itemView; + view.setIsSectionHeaderEnabled(isSectionHeaderDisplayEnabled()); + bindWorkProfileIcon(view, partition); + } + + @Override + protected View createPinnedSectionHeaderView(Context context, ViewGroup parent) { + return new ContactListPinnedHeaderView(context, null, parent); + } + + @Override + protected void setPinnedSectionTitle(View pinnedHeaderView, String title) { + ((ContactListPinnedHeaderView) pinnedHeaderView).setSectionHeaderTitle(title); + } + + protected void addPartitions() { + addPartition(createDefaultDirectoryPartition()); + } + + protected DirectoryPartition createDefaultDirectoryPartition() { + DirectoryPartition partition = new DirectoryPartition(true, true); + partition.setDirectoryId(Directory.DEFAULT); + partition.setDirectoryType(getContext().getString(R.string.contactsList)); + partition.setPriorityDirectory(true); + partition.setPhotoSupported(true); + partition.setLabel(mDefaultFilterHeaderText.toString()); + return partition; + } + + /** + * Remove all directories after the default directory. This is typically used when contacts list + * screens are asked to exit the search mode and thus need to remove all remote directory results + * for the search. + * + *

This code assumes that the default directory and directories before that should not be + * deleted (e.g. Join screen has "suggested contacts" directory before the default director, and + * we should not remove the directory). + */ + public void removeDirectoriesAfterDefault() { + final int partitionCount = getPartitionCount(); + for (int i = partitionCount - 1; i >= 0; i--) { + final Partition partition = getPartition(i); + if ((partition instanceof DirectoryPartition) + && ((DirectoryPartition) partition).getDirectoryId() == Directory.DEFAULT) { + break; + } else { + removePartition(i); + } + } + } + + protected int getPartitionByDirectoryId(long id) { + int count = getPartitionCount(); + for (int i = 0; i < count; i++) { + Partition partition = getPartition(i); + if (partition instanceof DirectoryPartition) { + if (((DirectoryPartition) partition).getDirectoryId() == id) { + return i; + } + } + } + return -1; + } + + protected DirectoryPartition getDirectoryById(long id) { + int count = getPartitionCount(); + for (int i = 0; i < count; i++) { + Partition partition = getPartition(i); + if (partition instanceof DirectoryPartition) { + final DirectoryPartition directoryPartition = (DirectoryPartition) partition; + if (directoryPartition.getDirectoryId() == id) { + return directoryPartition; + } + } + } + return null; + } + + public abstract void configureLoader(CursorLoader loader, long directoryId); + + /** Marks all partitions as "loading" */ + public void onDataReload() { + boolean notify = false; + int count = getPartitionCount(); + for (int i = 0; i < count; i++) { + Partition partition = getPartition(i); + if (partition instanceof DirectoryPartition) { + DirectoryPartition directoryPartition = (DirectoryPartition) partition; + if (!directoryPartition.isLoading()) { + notify = true; + } + directoryPartition.setStatus(DirectoryPartition.STATUS_NOT_LOADED); + } + } + if (notify) { + notifyDataSetChanged(); + } + } + + @Override + public void clearPartitions() { + int count = getPartitionCount(); + for (int i = 0; i < count; i++) { + Partition partition = getPartition(i); + if (partition instanceof DirectoryPartition) { + DirectoryPartition directoryPartition = (DirectoryPartition) partition; + directoryPartition.setStatus(DirectoryPartition.STATUS_NOT_LOADED); + } + } + super.clearPartitions(); + } + + public boolean isSearchMode() { + return mSearchMode; + } + + public void setSearchMode(boolean flag) { + mSearchMode = flag; + } + + public String getQueryString() { + return mQueryString; + } + + public void setQueryString(String queryString) { + mQueryString = queryString; + if (TextUtils.isEmpty(queryString)) { + mUpperCaseQueryString = null; + } else { + mUpperCaseQueryString = SearchUtil.cleanStartAndEndOfSearchQuery(queryString.toUpperCase()); + } + } + + public String getUpperCaseQueryString() { + return mUpperCaseQueryString; + } + + public int getDirectorySearchMode() { + return mDirectorySearchMode; + } + + public void setDirectorySearchMode(int mode) { + mDirectorySearchMode = mode; + } + + public int getDirectoryResultLimit() { + return mDirectoryResultLimit; + } + + public void setDirectoryResultLimit(int limit) { + this.mDirectoryResultLimit = limit; + } + + public int getDirectoryResultLimit(DirectoryPartition directoryPartition) { + final int limit = directoryPartition.getResultLimit(); + return limit == DirectoryPartition.RESULT_LIMIT_DEFAULT ? mDirectoryResultLimit : limit; + } + + public int getContactNameDisplayOrder() { + return mDisplayOrder; + } + + public void setContactNameDisplayOrder(int displayOrder) { + mDisplayOrder = displayOrder; + } + + public int getSortOrder() { + return mSortOrder; + } + + public void setSortOrder(int sortOrder) { + mSortOrder = sortOrder; + } + + protected ContactPhotoManager getPhotoLoader() { + return mPhotoLoader; + } + + public void setPhotoLoader(ContactPhotoManager photoLoader) { + mPhotoLoader = photoLoader; + } + + public boolean getDisplayPhotos() { + return mDisplayPhotos; + } + + public void setDisplayPhotos(boolean displayPhotos) { + mDisplayPhotos = displayPhotos; + } + + public boolean getCircularPhotos() { + return mCircularPhotos; + } + + public boolean isSelectionVisible() { + return mSelectionVisible; + } + + public void setSelectionVisible(boolean flag) { + this.mSelectionVisible = flag; + } + + public boolean isQuickContactEnabled() { + return mQuickContactEnabled; + } + + public void setQuickContactEnabled(boolean quickContactEnabled) { + mQuickContactEnabled = quickContactEnabled; + } + + public boolean isAdjustSelectionBoundsEnabled() { + return mAdjustSelectionBoundsEnabled; + } + + public void setAdjustSelectionBoundsEnabled(boolean enabled) { + mAdjustSelectionBoundsEnabled = enabled; + } + + public void setProfileExists(boolean exists) { + // Stick the "ME" header for the profile + if (exists) { + setSectionHeader(R.string.user_profile_contacts_list_header, /* # of ME */ 1); + } + } + + private void setSectionHeader(int resId, int numberOfItems) { + SectionIndexer indexer = getIndexer(); + if (indexer != null) { + ((ContactsSectionIndexer) indexer) + .setProfileAndFavoritesHeader(getContext().getString(resId), numberOfItems); + } + } + + public void setDarkTheme(boolean value) { + mDarkTheme = value; + } + + /** Updates partitions according to the directory meta-data contained in the supplied cursor. */ + public void changeDirectories(Cursor cursor) { + if (cursor.getCount() == 0) { + // Directory table must have at least local directory, without which this adapter will + // enter very weird state. + Log.e( + TAG, + "Directory search loader returned an empty cursor, which implies we have " + + "no directory entries.", + new RuntimeException()); + return; + } + HashSet directoryIds = new HashSet(); + + int idColumnIndex = cursor.getColumnIndex(Directory._ID); + int directoryTypeColumnIndex = cursor.getColumnIndex(DirectoryListLoader.DIRECTORY_TYPE); + int displayNameColumnIndex = cursor.getColumnIndex(Directory.DISPLAY_NAME); + int photoSupportColumnIndex = cursor.getColumnIndex(Directory.PHOTO_SUPPORT); + + // TODO preserve the order of partition to match those of the cursor + // Phase I: add new directories + cursor.moveToPosition(-1); + while (cursor.moveToNext()) { + long id = cursor.getLong(idColumnIndex); + directoryIds.add(id); + if (getPartitionByDirectoryId(id) == -1) { + DirectoryPartition partition = new DirectoryPartition(false, true); + partition.setDirectoryId(id); + if (DirectoryCompat.isRemoteDirectoryId(id)) { + if (DirectoryCompat.isEnterpriseDirectoryId(id)) { + partition.setLabel(mContext.getString(R.string.directory_search_label_work)); + } else { + partition.setLabel(mContext.getString(R.string.directory_search_label)); + } + } else { + if (DirectoryCompat.isEnterpriseDirectoryId(id)) { + partition.setLabel(mContext.getString(R.string.list_filter_phones_work)); + } else { + partition.setLabel(mDefaultFilterHeaderText.toString()); + } + } + partition.setDirectoryType(cursor.getString(directoryTypeColumnIndex)); + partition.setDisplayName(cursor.getString(displayNameColumnIndex)); + int photoSupport = cursor.getInt(photoSupportColumnIndex); + partition.setPhotoSupported( + photoSupport == Directory.PHOTO_SUPPORT_THUMBNAIL_ONLY + || photoSupport == Directory.PHOTO_SUPPORT_FULL); + addPartition(partition); + } + } + + // Phase II: remove deleted directories + int count = getPartitionCount(); + for (int i = count; --i >= 0; ) { + Partition partition = getPartition(i); + if (partition instanceof DirectoryPartition) { + long id = ((DirectoryPartition) partition).getDirectoryId(); + if (!directoryIds.contains(id)) { + removePartition(i); + } + } + } + + invalidate(); + notifyDataSetChanged(); + } + + @Override + public void changeCursor(int partitionIndex, Cursor cursor) { + if (partitionIndex >= getPartitionCount()) { + // There is no partition for this data + return; + } + + Partition partition = getPartition(partitionIndex); + if (partition instanceof DirectoryPartition) { + ((DirectoryPartition) partition).setStatus(DirectoryPartition.STATUS_LOADED); + } + + if (mDisplayPhotos && mPhotoLoader != null && isPhotoSupported(partitionIndex)) { + mPhotoLoader.refreshCache(); + } + + super.changeCursor(partitionIndex, cursor); + + if (isSectionHeaderDisplayEnabled() && partitionIndex == getIndexedPartition()) { + updateIndexer(cursor); + } + + // When the cursor changes, cancel any pending asynchronous photo loads. + mPhotoLoader.cancelPendingRequests(mFragmentRootView); + } + + public void changeCursor(Cursor cursor) { + changeCursor(0, cursor); + } + + /** Updates the indexer, which is used to produce section headers. */ + private void updateIndexer(Cursor cursor) { + if (cursor == null || cursor.isClosed()) { + setIndexer(null); + return; + } + + Bundle bundle = cursor.getExtras(); + if (bundle.containsKey(Contacts.EXTRA_ADDRESS_BOOK_INDEX_TITLES) + && bundle.containsKey(Contacts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS)) { + String[] sections = bundle.getStringArray(Contacts.EXTRA_ADDRESS_BOOK_INDEX_TITLES); + int[] counts = bundle.getIntArray(Contacts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS); + + if (getExtraStartingSection()) { + // Insert an additional unnamed section at the top of the list. + String[] allSections = new String[sections.length + 1]; + int[] allCounts = new int[counts.length + 1]; + for (int i = 0; i < sections.length; i++) { + allSections[i + 1] = sections[i]; + allCounts[i + 1] = counts[i]; + } + allCounts[0] = 1; + allSections[0] = ""; + setIndexer(new ContactsSectionIndexer(allSections, allCounts)); + } else { + setIndexer(new ContactsSectionIndexer(sections, counts)); + } + } else { + setIndexer(null); + } + } + + protected boolean getExtraStartingSection() { + return false; + } + + @Override + public int getViewTypeCount() { + // We need a separate view type for each item type, plus another one for + // each type with header, plus one for "other". + return getItemViewTypeCount() * 2 + 1; + } + + @Override + public int getItemViewType(int partitionIndex, int position) { + int type = super.getItemViewType(partitionIndex, position); + if (!isUserProfile(position) + && isSectionHeaderDisplayEnabled() + && partitionIndex == getIndexedPartition()) { + Placement placement = getItemPlacementInSection(position); + return placement.firstInSection ? type : getItemViewTypeCount() + type; + } else { + return type; + } + } + + @Override + public boolean isEmpty() { + // TODO + // if (contactsListActivity.mProviderStatus != ProviderStatus.STATUS_NORMAL) { + // return true; + // } + + if (!mEmptyListEnabled) { + return false; + } else if (isSearchMode()) { + return TextUtils.isEmpty(getQueryString()); + } else { + return super.isEmpty(); + } + } + + public boolean isLoading() { + int count = getPartitionCount(); + for (int i = 0; i < count; i++) { + Partition partition = getPartition(i); + if (partition instanceof DirectoryPartition && ((DirectoryPartition) partition).isLoading()) { + return true; + } + } + return false; + } + + /** Changes visibility parameters for the default directory partition. */ + public void configureDefaultPartition(boolean showIfEmpty, boolean hasHeader) { + int defaultPartitionIndex = -1; + int count = getPartitionCount(); + for (int i = 0; i < count; i++) { + Partition partition = getPartition(i); + if (partition instanceof DirectoryPartition + && ((DirectoryPartition) partition).getDirectoryId() == Directory.DEFAULT) { + defaultPartitionIndex = i; + break; + } + } + if (defaultPartitionIndex != -1) { + setShowIfEmpty(defaultPartitionIndex, showIfEmpty); + setHasHeader(defaultPartitionIndex, hasHeader); + } + } + + @Override + protected View newHeaderView(Context context, int partition, Cursor cursor, ViewGroup parent) { + LayoutInflater inflater = LayoutInflater.from(context); + View view = inflater.inflate(R.layout.directory_header, parent, false); + if (!getPinnedPartitionHeadersEnabled()) { + // If the headers are unpinned, there is no need for their background + // color to be non-transparent. Setting this transparent reduces maintenance for + // non-pinned headers. We don't need to bother synchronizing the activity's + // background color with the header background color. + view.setBackground(null); + } + return view; + } + + protected void bindWorkProfileIcon(final ContactListItemView view, int partitionId) { + final Partition partition = getPartition(partitionId); + if (partition instanceof DirectoryPartition) { + final DirectoryPartition directoryPartition = (DirectoryPartition) partition; + final long directoryId = directoryPartition.getDirectoryId(); + final long userType = ContactsUtils.determineUserType(directoryId, null); + view.setWorkProfileIconEnabled(userType == ContactsUtils.USER_TYPE_WORK); + } + } + + @Override + protected void bindHeaderView(View view, int partitionIndex, Cursor cursor) { + Partition partition = getPartition(partitionIndex); + if (!(partition instanceof DirectoryPartition)) { + return; + } + + DirectoryPartition directoryPartition = (DirectoryPartition) partition; + long directoryId = directoryPartition.getDirectoryId(); + TextView labelTextView = (TextView) view.findViewById(R.id.label); + TextView displayNameTextView = (TextView) view.findViewById(R.id.display_name); + labelTextView.setText(directoryPartition.getLabel()); + if (!DirectoryCompat.isRemoteDirectoryId(directoryId)) { + displayNameTextView.setText(null); + } else { + String directoryName = directoryPartition.getDisplayName(); + String displayName = + !TextUtils.isEmpty(directoryName) ? directoryName : directoryPartition.getDirectoryType(); + displayNameTextView.setText(displayName); + } + + final Resources res = getContext().getResources(); + final int headerPaddingTop = + partitionIndex == 1 && getPartition(0).isEmpty() + ? 0 + : res.getDimensionPixelOffset(R.dimen.directory_header_extra_top_padding); + // There should be no extra padding at the top of the first directory header + view.setPaddingRelative( + view.getPaddingStart(), headerPaddingTop, view.getPaddingEnd(), view.getPaddingBottom()); + } + + /** Checks whether the contact entry at the given position represents the user's profile. */ + protected boolean isUserProfile(int position) { + // The profile only ever appears in the first position if it is present. So if the position + // is anything beyond 0, it can't be the profile. + boolean isUserProfile = false; + if (position == 0) { + int partition = getPartitionForPosition(position); + if (partition >= 0) { + // Save the old cursor position - the call to getItem() may modify the cursor + // position. + int offset = getCursor(partition).getPosition(); + Cursor cursor = (Cursor) getItem(position); + if (cursor != null) { + int profileColumnIndex = cursor.getColumnIndex(Contacts.IS_USER_PROFILE); + if (profileColumnIndex != -1) { + isUserProfile = cursor.getInt(profileColumnIndex) == 1; + } + // Restore the old cursor position. + cursor.moveToPosition(offset); + } + } + } + return isUserProfile; + } + + public boolean isPhotoSupported(int partitionIndex) { + Partition partition = getPartition(partitionIndex); + if (partition instanceof DirectoryPartition) { + return ((DirectoryPartition) partition).isPhotoSupported(); + } + return true; + } + + /** Returns the currently selected filter. */ + public ContactListFilter getFilter() { + return mFilter; + } + + public void setFilter(ContactListFilter filter) { + mFilter = filter; + } + + // TODO: move sharable logic (bindXX() methods) to here with extra arguments + + /** + * Loads the photo for the quick contact view and assigns the contact uri. + * + * @param photoIdColumn Index of the photo id column + * @param photoUriColumn Index of the photo uri column. Optional: Can be -1 + * @param contactIdColumn Index of the contact id column + * @param lookUpKeyColumn Index of the lookup key column + * @param displayNameColumn Index of the display name column + */ + protected void bindQuickContact( + final ContactListItemView view, + int partitionIndex, + Cursor cursor, + int photoIdColumn, + int photoUriColumn, + int contactIdColumn, + int lookUpKeyColumn, + int displayNameColumn) { + long photoId = 0; + if (!cursor.isNull(photoIdColumn)) { + photoId = cursor.getLong(photoIdColumn); + } + + QuickContactBadge quickContact = view.getQuickContact(); + quickContact.assignContactUri( + getContactUri(partitionIndex, cursor, contactIdColumn, lookUpKeyColumn)); + if (CompatUtils.hasPrioritizedMimeType()) { + // The Contacts app never uses the QuickContactBadge. Therefore, it is safe to assume + // that only Dialer will use this QuickContact badge. This means prioritizing the phone + // mimetype here is reasonable. + quickContact.setPrioritizedMimeType(Phone.CONTENT_ITEM_TYPE); + } + + if (photoId != 0 || photoUriColumn == -1) { + getPhotoLoader().loadThumbnail(quickContact, photoId, mDarkTheme, mCircularPhotos, null); + } else { + final String photoUriString = cursor.getString(photoUriColumn); + final Uri photoUri = photoUriString == null ? null : Uri.parse(photoUriString); + DefaultImageRequest request = null; + if (photoUri == null) { + request = getDefaultImageRequestFromCursor(cursor, displayNameColumn, lookUpKeyColumn); + } + getPhotoLoader().loadPhoto(quickContact, photoUri, -1, mDarkTheme, mCircularPhotos, request); + } + } + + @Override + public boolean hasStableIds() { + // Whenever bindViewId() is called, the values passed into setId() are stable or + // stable-ish. For example, when one contact is modified we don't expect a second + // contact's Contact._ID values to change. + return true; + } + + protected void bindViewId(final ContactListItemView view, Cursor cursor, int idColumn) { + // Set a semi-stable id, so that talkback won't get confused when the list gets + // refreshed. There is little harm in inserting the same ID twice. + long contactId = cursor.getLong(idColumn); + view.setId((int) (contactId % Integer.MAX_VALUE)); + } + + protected Uri getContactUri( + int partitionIndex, Cursor cursor, int contactIdColumn, int lookUpKeyColumn) { + long contactId = cursor.getLong(contactIdColumn); + String lookupKey = cursor.getString(lookUpKeyColumn); + long directoryId = ((DirectoryPartition) getPartition(partitionIndex)).getDirectoryId(); + Uri uri = Contacts.getLookupUri(contactId, lookupKey); + if (uri != null && directoryId != Directory.DEFAULT) { + uri = + uri.buildUpon() + .appendQueryParameter( + ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId)) + .build(); + } + return uri; + } + + /** + * Retrieves the lookup key and display name from a cursor, and returns a {@link + * DefaultImageRequest} containing these contact details + * + * @param cursor Contacts cursor positioned at the current row to retrieve contact details for + * @param displayNameColumn Column index of the display name + * @param lookupKeyColumn Column index of the lookup key + * @return {@link DefaultImageRequest} with the displayName and identifier fields set to the + * display name and lookup key of the contact. + */ + public DefaultImageRequest getDefaultImageRequestFromCursor( + Cursor cursor, int displayNameColumn, int lookupKeyColumn) { + final String displayName = cursor.getString(displayNameColumn); + final String lookupKey = cursor.getString(lookupKeyColumn); + return new DefaultImageRequest(displayName, lookupKey, mCircularPhotos); + } +} diff --git a/java/com/android/contacts/common/list/ContactEntryListFragment.java b/java/com/android/contacts/common/list/ContactEntryListFragment.java new file mode 100644 index 000000000..a8d9b55ba --- /dev/null +++ b/java/com/android/contacts/common/list/ContactEntryListFragment.java @@ -0,0 +1,862 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.list; + +import android.app.Activity; +import android.app.Fragment; +import android.app.LoaderManager; +import android.app.LoaderManager.LoaderCallbacks; +import android.content.Context; +import android.content.CursorLoader; +import android.content.Loader; +import android.database.Cursor; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.Parcelable; +import android.provider.ContactsContract.Directory; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnFocusChangeListener; +import android.view.View.OnTouchListener; +import android.view.ViewGroup; +import android.view.inputmethod.InputMethodManager; +import android.widget.AbsListView; +import android.widget.AbsListView.OnScrollListener; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.AdapterView.OnItemLongClickListener; +import android.widget.ListView; +import com.android.common.widget.CompositeCursorAdapter.Partition; +import com.android.contacts.common.ContactPhotoManager; +import com.android.contacts.common.preference.ContactsPreferences; +import com.android.contacts.common.util.ContactListViewUtils; +import java.util.Locale; + +/** Common base class for various contact-related list fragments. */ +public abstract class ContactEntryListFragment extends Fragment + implements OnItemClickListener, + OnScrollListener, + OnFocusChangeListener, + OnTouchListener, + OnItemLongClickListener, + LoaderCallbacks { + private static final String TAG = "ContactEntryListFragment"; + private static final String KEY_LIST_STATE = "liststate"; + private static final String KEY_SECTION_HEADER_DISPLAY_ENABLED = "sectionHeaderDisplayEnabled"; + private static final String KEY_PHOTO_LOADER_ENABLED = "photoLoaderEnabled"; + private static final String KEY_QUICK_CONTACT_ENABLED = "quickContactEnabled"; + private static final String KEY_ADJUST_SELECTION_BOUNDS_ENABLED = "adjustSelectionBoundsEnabled"; + private static final String KEY_INCLUDE_PROFILE = "includeProfile"; + private static final String KEY_SEARCH_MODE = "searchMode"; + private static final String KEY_VISIBLE_SCROLLBAR_ENABLED = "visibleScrollbarEnabled"; + private static final String KEY_SCROLLBAR_POSITION = "scrollbarPosition"; + private static final String KEY_QUERY_STRING = "queryString"; + private static final String KEY_DIRECTORY_SEARCH_MODE = "directorySearchMode"; + private static final String KEY_SELECTION_VISIBLE = "selectionVisible"; + private static final String KEY_DARK_THEME = "darkTheme"; + private static final String KEY_LEGACY_COMPATIBILITY = "legacyCompatibility"; + private static final String KEY_DIRECTORY_RESULT_LIMIT = "directoryResultLimit"; + + private static final String DIRECTORY_ID_ARG_KEY = "directoryId"; + + private static final int DIRECTORY_LOADER_ID = -1; + + private static final int DIRECTORY_SEARCH_DELAY_MILLIS = 300; + private static final int DIRECTORY_SEARCH_MESSAGE = 1; + + private static final int DEFAULT_DIRECTORY_RESULT_LIMIT = 20; + private static final int STATUS_NOT_LOADED = 0; + private static final int STATUS_LOADING = 1; + private static final int STATUS_LOADED = 2; + protected boolean mUserProfileExists; + private boolean mSectionHeaderDisplayEnabled; + private boolean mPhotoLoaderEnabled; + private boolean mQuickContactEnabled = true; + private boolean mAdjustSelectionBoundsEnabled = true; + private boolean mIncludeProfile; + private boolean mSearchMode; + private boolean mVisibleScrollbarEnabled; + private boolean mShowEmptyListForEmptyQuery; + private int mVerticalScrollbarPosition = getDefaultVerticalScrollbarPosition(); + private String mQueryString; + private int mDirectorySearchMode = DirectoryListLoader.SEARCH_MODE_NONE; + private boolean mSelectionVisible; + private boolean mLegacyCompatibility; + private boolean mEnabled = true; + private T mAdapter; + private View mView; + private ListView mListView; + /** Used to save the scrolling state of the list when the fragment is not recreated. */ + private int mListViewTopIndex; + + private int mListViewTopOffset; + /** Used for keeping track of the scroll state of the list. */ + private Parcelable mListState; + + private int mDisplayOrder; + private int mSortOrder; + private int mDirectoryResultLimit = DEFAULT_DIRECTORY_RESULT_LIMIT; + private ContactPhotoManager mPhotoManager; + private ContactsPreferences mContactsPrefs; + private boolean mForceLoad; + private boolean mDarkTheme; + private int mDirectoryListStatus = STATUS_NOT_LOADED; + + /** + * Indicates whether we are doing the initial complete load of data (false) or a refresh caused by + * a change notification (true) + */ + private boolean mLoadPriorityDirectoriesOnly; + + private Context mContext; + + private LoaderManager mLoaderManager; + + private Handler mDelayedDirectorySearchHandler = + new Handler() { + @Override + public void handleMessage(Message msg) { + if (msg.what == DIRECTORY_SEARCH_MESSAGE) { + loadDirectoryPartition(msg.arg1, (DirectoryPartition) msg.obj); + } + } + }; + private ContactsPreferences.ChangeListener mPreferencesChangeListener = + new ContactsPreferences.ChangeListener() { + @Override + public void onChange() { + loadPreferences(); + reloadData(); + } + }; + + protected abstract View inflateView(LayoutInflater inflater, ViewGroup container); + + protected abstract T createListAdapter(); + + /** + * @param position Please note that the position is already adjusted for header views, so "0" + * means the first list item below header views. + */ + protected abstract void onItemClick(int position, long id); + + /** + * @param position Please note that the position is already adjusted for header views, so "0" + * means the first list item below header views. + */ + protected boolean onItemLongClick(int position, long id) { + return false; + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + setContext(activity); + setLoaderManager(super.getLoaderManager()); + } + + @Override + public Context getContext() { + return mContext; + } + + /** Sets a context for the fragment in the unit test environment. */ + public void setContext(Context context) { + mContext = context; + configurePhotoLoader(); + } + + public void setEnabled(boolean enabled) { + if (mEnabled != enabled) { + mEnabled = enabled; + if (mAdapter != null) { + if (mEnabled) { + reloadData(); + } else { + mAdapter.clearPartitions(); + } + } + } + } + + @Override + public LoaderManager getLoaderManager() { + return mLoaderManager; + } + + /** Overrides a loader manager for use in unit tests. */ + public void setLoaderManager(LoaderManager loaderManager) { + mLoaderManager = loaderManager; + } + + public T getAdapter() { + return mAdapter; + } + + @Override + public View getView() { + return mView; + } + + public ListView getListView() { + return mListView; + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putBoolean(KEY_SECTION_HEADER_DISPLAY_ENABLED, mSectionHeaderDisplayEnabled); + outState.putBoolean(KEY_PHOTO_LOADER_ENABLED, mPhotoLoaderEnabled); + outState.putBoolean(KEY_QUICK_CONTACT_ENABLED, mQuickContactEnabled); + outState.putBoolean(KEY_ADJUST_SELECTION_BOUNDS_ENABLED, mAdjustSelectionBoundsEnabled); + outState.putBoolean(KEY_INCLUDE_PROFILE, mIncludeProfile); + outState.putBoolean(KEY_SEARCH_MODE, mSearchMode); + outState.putBoolean(KEY_VISIBLE_SCROLLBAR_ENABLED, mVisibleScrollbarEnabled); + outState.putInt(KEY_SCROLLBAR_POSITION, mVerticalScrollbarPosition); + outState.putInt(KEY_DIRECTORY_SEARCH_MODE, mDirectorySearchMode); + outState.putBoolean(KEY_SELECTION_VISIBLE, mSelectionVisible); + outState.putBoolean(KEY_LEGACY_COMPATIBILITY, mLegacyCompatibility); + outState.putString(KEY_QUERY_STRING, mQueryString); + outState.putInt(KEY_DIRECTORY_RESULT_LIMIT, mDirectoryResultLimit); + outState.putBoolean(KEY_DARK_THEME, mDarkTheme); + + if (mListView != null) { + outState.putParcelable(KEY_LIST_STATE, mListView.onSaveInstanceState()); + } + } + + @Override + public void onCreate(Bundle savedState) { + super.onCreate(savedState); + restoreSavedState(savedState); + mAdapter = createListAdapter(); + mContactsPrefs = new ContactsPreferences(mContext); + } + + public void restoreSavedState(Bundle savedState) { + if (savedState == null) { + return; + } + + mSectionHeaderDisplayEnabled = savedState.getBoolean(KEY_SECTION_HEADER_DISPLAY_ENABLED); + mPhotoLoaderEnabled = savedState.getBoolean(KEY_PHOTO_LOADER_ENABLED); + mQuickContactEnabled = savedState.getBoolean(KEY_QUICK_CONTACT_ENABLED); + mAdjustSelectionBoundsEnabled = savedState.getBoolean(KEY_ADJUST_SELECTION_BOUNDS_ENABLED); + mIncludeProfile = savedState.getBoolean(KEY_INCLUDE_PROFILE); + mSearchMode = savedState.getBoolean(KEY_SEARCH_MODE); + mVisibleScrollbarEnabled = savedState.getBoolean(KEY_VISIBLE_SCROLLBAR_ENABLED); + mVerticalScrollbarPosition = savedState.getInt(KEY_SCROLLBAR_POSITION); + mDirectorySearchMode = savedState.getInt(KEY_DIRECTORY_SEARCH_MODE); + mSelectionVisible = savedState.getBoolean(KEY_SELECTION_VISIBLE); + mLegacyCompatibility = savedState.getBoolean(KEY_LEGACY_COMPATIBILITY); + mQueryString = savedState.getString(KEY_QUERY_STRING); + mDirectoryResultLimit = savedState.getInt(KEY_DIRECTORY_RESULT_LIMIT); + mDarkTheme = savedState.getBoolean(KEY_DARK_THEME); + + // Retrieve list state. This will be applied in onLoadFinished + mListState = savedState.getParcelable(KEY_LIST_STATE); + } + + @Override + public void onStart() { + super.onStart(); + + mContactsPrefs.registerChangeListener(mPreferencesChangeListener); + + mForceLoad = loadPreferences(); + + mDirectoryListStatus = STATUS_NOT_LOADED; + mLoadPriorityDirectoriesOnly = true; + + startLoading(); + } + + protected void startLoading() { + if (mAdapter == null) { + // The method was called before the fragment was started + return; + } + + configureAdapter(); + int partitionCount = mAdapter.getPartitionCount(); + for (int i = 0; i < partitionCount; i++) { + Partition partition = mAdapter.getPartition(i); + if (partition instanceof DirectoryPartition) { + DirectoryPartition directoryPartition = (DirectoryPartition) partition; + if (directoryPartition.getStatus() == DirectoryPartition.STATUS_NOT_LOADED) { + if (directoryPartition.isPriorityDirectory() || !mLoadPriorityDirectoriesOnly) { + startLoadingDirectoryPartition(i); + } + } + } else { + getLoaderManager().initLoader(i, null, this); + } + } + + // Next time this method is called, we should start loading non-priority directories + mLoadPriorityDirectoriesOnly = false; + } + + @Override + public Loader onCreateLoader(int id, Bundle args) { + if (id == DIRECTORY_LOADER_ID) { + DirectoryListLoader loader = new DirectoryListLoader(mContext); + loader.setDirectorySearchMode(mAdapter.getDirectorySearchMode()); + loader.setLocalInvisibleDirectoryEnabled( + ContactEntryListAdapter.LOCAL_INVISIBLE_DIRECTORY_ENABLED); + return loader; + } else { + CursorLoader loader = createCursorLoader(mContext); + long directoryId = + args != null && args.containsKey(DIRECTORY_ID_ARG_KEY) + ? args.getLong(DIRECTORY_ID_ARG_KEY) + : Directory.DEFAULT; + mAdapter.configureLoader(loader, directoryId); + return loader; + } + } + + public CursorLoader createCursorLoader(Context context) { + return new CursorLoader(context, null, null, null, null, null) { + @Override + protected Cursor onLoadInBackground() { + try { + return super.onLoadInBackground(); + } catch (RuntimeException e) { + // We don't even know what the projection should be, so no point trying to + // return an empty MatrixCursor with the correct projection here. + Log.w(TAG, "RuntimeException while trying to query ContactsProvider."); + return null; + } + } + }; + } + + private void startLoadingDirectoryPartition(int partitionIndex) { + DirectoryPartition partition = (DirectoryPartition) mAdapter.getPartition(partitionIndex); + partition.setStatus(DirectoryPartition.STATUS_LOADING); + long directoryId = partition.getDirectoryId(); + if (mForceLoad) { + if (directoryId == Directory.DEFAULT) { + loadDirectoryPartition(partitionIndex, partition); + } else { + loadDirectoryPartitionDelayed(partitionIndex, partition); + } + } else { + Bundle args = new Bundle(); + args.putLong(DIRECTORY_ID_ARG_KEY, directoryId); + getLoaderManager().initLoader(partitionIndex, args, this); + } + } + + /** + * Queues up a delayed request to search the specified directory. Since directory search will + * likely introduce a lot of network traffic, we want to wait for a pause in the user's typing + * before sending a directory request. + */ + private void loadDirectoryPartitionDelayed(int partitionIndex, DirectoryPartition partition) { + mDelayedDirectorySearchHandler.removeMessages(DIRECTORY_SEARCH_MESSAGE, partition); + Message msg = + mDelayedDirectorySearchHandler.obtainMessage( + DIRECTORY_SEARCH_MESSAGE, partitionIndex, 0, partition); + mDelayedDirectorySearchHandler.sendMessageDelayed(msg, DIRECTORY_SEARCH_DELAY_MILLIS); + } + + /** Loads the directory partition. */ + protected void loadDirectoryPartition(int partitionIndex, DirectoryPartition partition) { + Bundle args = new Bundle(); + args.putLong(DIRECTORY_ID_ARG_KEY, partition.getDirectoryId()); + getLoaderManager().restartLoader(partitionIndex, args, this); + } + + /** Cancels all queued directory loading requests. */ + private void removePendingDirectorySearchRequests() { + mDelayedDirectorySearchHandler.removeMessages(DIRECTORY_SEARCH_MESSAGE); + } + + @Override + public void onLoadFinished(Loader loader, Cursor data) { + if (!mEnabled) { + return; + } + + int loaderId = loader.getId(); + if (loaderId == DIRECTORY_LOADER_ID) { + mDirectoryListStatus = STATUS_LOADED; + mAdapter.changeDirectories(data); + startLoading(); + } else { + onPartitionLoaded(loaderId, data); + if (isSearchMode()) { + int directorySearchMode = getDirectorySearchMode(); + if (directorySearchMode != DirectoryListLoader.SEARCH_MODE_NONE) { + if (mDirectoryListStatus == STATUS_NOT_LOADED) { + mDirectoryListStatus = STATUS_LOADING; + getLoaderManager().initLoader(DIRECTORY_LOADER_ID, null, this); + } else { + startLoading(); + } + } + } else { + mDirectoryListStatus = STATUS_NOT_LOADED; + getLoaderManager().destroyLoader(DIRECTORY_LOADER_ID); + } + } + } + + @Override + public void onLoaderReset(Loader loader) {} + + protected void onPartitionLoaded(int partitionIndex, Cursor data) { + if (partitionIndex >= mAdapter.getPartitionCount()) { + // When we get unsolicited data, ignore it. This could happen + // when we are switching from search mode to the default mode. + return; + } + + mAdapter.changeCursor(partitionIndex, data); + setProfileHeader(); + + if (!isLoading()) { + completeRestoreInstanceState(); + } + } + + public boolean isLoading() { + if (mAdapter != null && mAdapter.isLoading()) { + return true; + } + + return isLoadingDirectoryList(); + + } + + public boolean isLoadingDirectoryList() { + return isSearchMode() + && getDirectorySearchMode() != DirectoryListLoader.SEARCH_MODE_NONE + && (mDirectoryListStatus == STATUS_NOT_LOADED || mDirectoryListStatus == STATUS_LOADING); + } + + @Override + public void onStop() { + super.onStop(); + mContactsPrefs.unregisterChangeListener(); + mAdapter.clearPartitions(); + } + + protected void reloadData() { + removePendingDirectorySearchRequests(); + mAdapter.onDataReload(); + mLoadPriorityDirectoriesOnly = true; + mForceLoad = true; + startLoading(); + } + + /** + * Shows a view at the top of the list with a pseudo local profile prompting the user to add a + * local profile. Default implementation does nothing. + */ + protected void setProfileHeader() { + mUserProfileExists = false; + } + + /** Provides logic that dismisses this fragment. The default implementation does nothing. */ + protected void finish() {} + + public boolean isSectionHeaderDisplayEnabled() { + return mSectionHeaderDisplayEnabled; + } + + public void setSectionHeaderDisplayEnabled(boolean flag) { + if (mSectionHeaderDisplayEnabled != flag) { + mSectionHeaderDisplayEnabled = flag; + if (mAdapter != null) { + mAdapter.setSectionHeaderDisplayEnabled(flag); + } + configureVerticalScrollbar(); + } + } + + public boolean isVisibleScrollbarEnabled() { + return mVisibleScrollbarEnabled; + } + + public void setVisibleScrollbarEnabled(boolean flag) { + if (mVisibleScrollbarEnabled != flag) { + mVisibleScrollbarEnabled = flag; + configureVerticalScrollbar(); + } + } + + private void configureVerticalScrollbar() { + boolean hasScrollbar = isVisibleScrollbarEnabled() && isSectionHeaderDisplayEnabled(); + + if (mListView != null) { + mListView.setFastScrollEnabled(hasScrollbar); + mListView.setFastScrollAlwaysVisible(hasScrollbar); + mListView.setVerticalScrollbarPosition(mVerticalScrollbarPosition); + mListView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY); + } + } + + public boolean isPhotoLoaderEnabled() { + return mPhotoLoaderEnabled; + } + + public void setPhotoLoaderEnabled(boolean flag) { + mPhotoLoaderEnabled = flag; + configurePhotoLoader(); + } + + public void setQuickContactEnabled(boolean flag) { + this.mQuickContactEnabled = flag; + } + + public void setAdjustSelectionBoundsEnabled(boolean flag) { + mAdjustSelectionBoundsEnabled = flag; + } + + public final boolean isSearchMode() { + return mSearchMode; + } + + /** + * Enter/exit search mode. This is method is tightly related to the current query, and should only + * be called by {@link #setQueryString}. + * + *

Also note this method doesn't call {@link #reloadData()}; {@link #setQueryString} does it. + */ + protected void setSearchMode(boolean flag) { + if (mSearchMode != flag) { + mSearchMode = flag; + setSectionHeaderDisplayEnabled(!mSearchMode); + + if (!flag) { + mDirectoryListStatus = STATUS_NOT_LOADED; + getLoaderManager().destroyLoader(DIRECTORY_LOADER_ID); + } + + if (mAdapter != null) { + mAdapter.setSearchMode(flag); + + mAdapter.clearPartitions(); + if (!flag) { + // If we are switching from search to regular display, remove all directory + // partitions after default one, assuming they are remote directories which + // should be cleaned up on exiting the search mode. + mAdapter.removeDirectoriesAfterDefault(); + } + mAdapter.configureDefaultPartition(false, flag); + } + + if (mListView != null) { + mListView.setFastScrollEnabled(!flag); + } + } + } + + public final String getQueryString() { + return mQueryString; + } + + public void setQueryString(String queryString) { + if (!TextUtils.equals(mQueryString, queryString)) { + if (mShowEmptyListForEmptyQuery && mAdapter != null && mListView != null) { + if (TextUtils.isEmpty(mQueryString)) { + // Restore the adapter if the query used to be empty. + mListView.setAdapter(mAdapter); + } else if (TextUtils.isEmpty(queryString)) { + // Instantly clear the list view if the new query is empty. + mListView.setAdapter(null); + } + } + + mQueryString = queryString; + setSearchMode(!TextUtils.isEmpty(mQueryString) || mShowEmptyListForEmptyQuery); + + if (mAdapter != null) { + mAdapter.setQueryString(queryString); + reloadData(); + } + } + } + + public void setShowEmptyListForNullQuery(boolean show) { + mShowEmptyListForEmptyQuery = show; + } + + public boolean getShowEmptyListForNullQuery() { + return mShowEmptyListForEmptyQuery; + } + + public int getDirectoryLoaderId() { + return DIRECTORY_LOADER_ID; + } + + public int getDirectorySearchMode() { + return mDirectorySearchMode; + } + + public void setDirectorySearchMode(int mode) { + mDirectorySearchMode = mode; + } + + protected int getContactNameDisplayOrder() { + return mDisplayOrder; + } + + protected void setContactNameDisplayOrder(int displayOrder) { + mDisplayOrder = displayOrder; + if (mAdapter != null) { + mAdapter.setContactNameDisplayOrder(displayOrder); + } + } + + public int getSortOrder() { + return mSortOrder; + } + + public void setSortOrder(int sortOrder) { + mSortOrder = sortOrder; + if (mAdapter != null) { + mAdapter.setSortOrder(sortOrder); + } + } + + public void setDirectoryResultLimit(int limit) { + mDirectoryResultLimit = limit; + } + + protected boolean loadPreferences() { + boolean changed = false; + if (getContactNameDisplayOrder() != mContactsPrefs.getDisplayOrder()) { + setContactNameDisplayOrder(mContactsPrefs.getDisplayOrder()); + changed = true; + } + + if (getSortOrder() != mContactsPrefs.getSortOrder()) { + setSortOrder(mContactsPrefs.getSortOrder()); + changed = true; + } + + return changed; + } + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + onCreateView(inflater, container); + + boolean searchMode = isSearchMode(); + mAdapter.setSearchMode(searchMode); + mAdapter.configureDefaultPartition(false, searchMode); + mAdapter.setPhotoLoader(mPhotoManager); + mListView.setAdapter(mAdapter); + + if (!isSearchMode()) { + mListView.setFocusableInTouchMode(true); + mListView.requestFocus(); + } + + return mView; + } + + protected void onCreateView(LayoutInflater inflater, ViewGroup container) { + mView = inflateView(inflater, container); + + mListView = (ListView) mView.findViewById(android.R.id.list); + if (mListView == null) { + throw new RuntimeException( + "Your content must have a ListView whose id attribute is " + "'android.R.id.list'"); + } + + View emptyView = mView.findViewById(android.R.id.empty); + if (emptyView != null) { + mListView.setEmptyView(emptyView); + } + + mListView.setOnItemClickListener(this); + mListView.setOnItemLongClickListener(this); + mListView.setOnFocusChangeListener(this); + mListView.setOnTouchListener(this); + mListView.setFastScrollEnabled(!isSearchMode()); + + // Tell list view to not show dividers. We'll do it ourself so that we can *not* show + // them when an A-Z headers is visible. + mListView.setDividerHeight(0); + + // We manually save/restore the listview state + mListView.setSaveEnabled(false); + + configureVerticalScrollbar(); + configurePhotoLoader(); + + getAdapter().setFragmentRootView(getView()); + + ContactListViewUtils.applyCardPaddingToView(getResources(), mListView, mView); + } + + @Override + public void onHiddenChanged(boolean hidden) { + super.onHiddenChanged(hidden); + if (getActivity() != null && getView() != null && !hidden) { + // If the padding was last applied when in a hidden state, it may have been applied + // incorrectly. Therefore we need to reapply it. + ContactListViewUtils.applyCardPaddingToView(getResources(), mListView, getView()); + } + } + + protected void configurePhotoLoader() { + if (isPhotoLoaderEnabled() && mContext != null) { + if (mPhotoManager == null) { + mPhotoManager = ContactPhotoManager.getInstance(mContext); + } + if (mListView != null) { + mListView.setOnScrollListener(this); + } + if (mAdapter != null) { + mAdapter.setPhotoLoader(mPhotoManager); + } + } + } + + protected void configureAdapter() { + if (mAdapter == null) { + return; + } + + mAdapter.setQuickContactEnabled(mQuickContactEnabled); + mAdapter.setAdjustSelectionBoundsEnabled(mAdjustSelectionBoundsEnabled); + mAdapter.setQueryString(mQueryString); + mAdapter.setDirectorySearchMode(mDirectorySearchMode); + mAdapter.setPinnedPartitionHeadersEnabled(false); + mAdapter.setContactNameDisplayOrder(mDisplayOrder); + mAdapter.setSortOrder(mSortOrder); + mAdapter.setSectionHeaderDisplayEnabled(mSectionHeaderDisplayEnabled); + mAdapter.setSelectionVisible(mSelectionVisible); + mAdapter.setDirectoryResultLimit(mDirectoryResultLimit); + mAdapter.setDarkTheme(mDarkTheme); + } + + @Override + public void onScroll( + AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {} + + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + if (scrollState == OnScrollListener.SCROLL_STATE_FLING) { + mPhotoManager.pause(); + } else if (isPhotoLoaderEnabled()) { + mPhotoManager.resume(); + } + } + + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + hideSoftKeyboard(); + + int adjPosition = position - mListView.getHeaderViewsCount(); + if (adjPosition >= 0) { + onItemClick(adjPosition, id); + } + } + + @Override + public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { + int adjPosition = position - mListView.getHeaderViewsCount(); + + if (adjPosition >= 0) { + return onItemLongClick(adjPosition, id); + } + return false; + } + + private void hideSoftKeyboard() { + // Hide soft keyboard, if visible + InputMethodManager inputMethodManager = + (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE); + inputMethodManager.hideSoftInputFromWindow(mListView.getWindowToken(), 0); + } + + /** Dismisses the soft keyboard when the list takes focus. */ + @Override + public void onFocusChange(View view, boolean hasFocus) { + if (view == mListView && hasFocus) { + hideSoftKeyboard(); + } + } + + /** Dismisses the soft keyboard when the list is touched. */ + @Override + public boolean onTouch(View view, MotionEvent event) { + if (view == mListView) { + hideSoftKeyboard(); + } + return false; + } + + @Override + public void onPause() { + // Save the scrolling state of the list view + mListViewTopIndex = mListView.getFirstVisiblePosition(); + View v = mListView.getChildAt(0); + mListViewTopOffset = (v == null) ? 0 : (v.getTop() - mListView.getPaddingTop()); + + super.onPause(); + removePendingDirectorySearchRequests(); + } + + @Override + public void onResume() { + super.onResume(); + // Restore the selection of the list view. See b/19982820. + // This has to be done manually because if the list view has its emptyView set, + // the scrolling state will be reset when clearPartitions() is called on the adapter. + mListView.setSelectionFromTop(mListViewTopIndex, mListViewTopOffset); + } + + /** Restore the list state after the adapter is populated. */ + protected void completeRestoreInstanceState() { + if (mListState != null) { + mListView.onRestoreInstanceState(mListState); + mListState = null; + } + } + + public void setDarkTheme(boolean value) { + mDarkTheme = value; + if (mAdapter != null) { + mAdapter.setDarkTheme(value); + } + } + + private int getDefaultVerticalScrollbarPosition() { + final Locale locale = Locale.getDefault(); + final int layoutDirection = TextUtils.getLayoutDirectionFromLocale(locale); + switch (layoutDirection) { + case View.LAYOUT_DIRECTION_RTL: + return View.SCROLLBAR_POSITION_LEFT; + case View.LAYOUT_DIRECTION_LTR: + default: + return View.SCROLLBAR_POSITION_RIGHT; + } + } +} diff --git a/java/com/android/contacts/common/list/ContactListAdapter.java b/java/com/android/contacts/common/list/ContactListAdapter.java new file mode 100644 index 000000000..6cd311811 --- /dev/null +++ b/java/com/android/contacts/common/list/ContactListAdapter.java @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.contacts.common.list; + +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.provider.ContactsContract; +import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.Directory; +import android.provider.ContactsContract.SearchSnippets; +import android.view.ViewGroup; +import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest; +import com.android.contacts.common.R; +import com.android.contacts.common.preference.ContactsPreferences; + +/** + * A cursor adapter for the {@link ContactsContract.Contacts#CONTENT_TYPE} content type. Also + * includes support for including the {@link ContactsContract.Profile} record in the list. + */ +public abstract class ContactListAdapter extends ContactEntryListAdapter { + + private CharSequence mUnknownNameText; + + public ContactListAdapter(Context context) { + super(context); + + mUnknownNameText = context.getText(R.string.missing_name); + } + + protected static Uri buildSectionIndexerUri(Uri uri) { + return uri.buildUpon().appendQueryParameter(Contacts.EXTRA_ADDRESS_BOOK_INDEX, "true").build(); + } + + public Uri getContactUri(int partitionIndex, Cursor cursor) { + long contactId = cursor.getLong(ContactQuery.CONTACT_ID); + String lookupKey = cursor.getString(ContactQuery.CONTACT_LOOKUP_KEY); + Uri uri = Contacts.getLookupUri(contactId, lookupKey); + long directoryId = ((DirectoryPartition) getPartition(partitionIndex)).getDirectoryId(); + if (uri != null && directoryId != Directory.DEFAULT) { + uri = + uri.buildUpon() + .appendQueryParameter( + ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId)) + .build(); + } + return uri; + } + + @Override + protected ContactListItemView newView( + Context context, int partition, Cursor cursor, int position, ViewGroup parent) { + ContactListItemView view = super.newView(context, partition, cursor, position, parent); + view.setUnknownNameText(mUnknownNameText); + view.setQuickContactEnabled(isQuickContactEnabled()); + view.setAdjustSelectionBoundsEnabled(isAdjustSelectionBoundsEnabled()); + view.setActivatedStateSupported(isSelectionVisible()); + return view; + } + + protected void bindSectionHeaderAndDivider( + ContactListItemView view, int position, Cursor cursor) { + view.setIsSectionHeaderEnabled(isSectionHeaderDisplayEnabled()); + if (isSectionHeaderDisplayEnabled()) { + Placement placement = getItemPlacementInSection(position); + view.setSectionHeader(placement.sectionHeader); + } else { + view.setSectionHeader(null); + } + } + + protected void bindPhoto(final ContactListItemView view, int partitionIndex, Cursor cursor) { + if (!isPhotoSupported(partitionIndex)) { + view.removePhotoView(); + return; + } + + // Set the photo, if available + long photoId = 0; + if (!cursor.isNull(ContactQuery.CONTACT_PHOTO_ID)) { + photoId = cursor.getLong(ContactQuery.CONTACT_PHOTO_ID); + } + + if (photoId != 0) { + getPhotoLoader() + .loadThumbnail(view.getPhotoView(), photoId, false, getCircularPhotos(), null); + } else { + final String photoUriString = cursor.getString(ContactQuery.CONTACT_PHOTO_URI); + final Uri photoUri = photoUriString == null ? null : Uri.parse(photoUriString); + DefaultImageRequest request = null; + if (photoUri == null) { + request = + getDefaultImageRequestFromCursor( + cursor, ContactQuery.CONTACT_DISPLAY_NAME, ContactQuery.CONTACT_LOOKUP_KEY); + } + getPhotoLoader() + .loadDirectoryPhoto(view.getPhotoView(), photoUri, false, getCircularPhotos(), request); + } + } + + protected void bindNameAndViewId(final ContactListItemView view, Cursor cursor) { + view.showDisplayName(cursor, ContactQuery.CONTACT_DISPLAY_NAME); + // Note: we don't show phonetic any more (See issue 5265330) + + bindViewId(view, cursor, ContactQuery.CONTACT_ID); + } + + protected void bindPresenceAndStatusMessage(final ContactListItemView view, Cursor cursor) { + view.showPresenceAndStatusMessage( + cursor, ContactQuery.CONTACT_PRESENCE_STATUS, ContactQuery.CONTACT_CONTACT_STATUS); + } + + protected void bindSearchSnippet(final ContactListItemView view, Cursor cursor) { + view.showSnippet(cursor, ContactQuery.CONTACT_SNIPPET); + } + + @Override + public void changeCursor(int partitionIndex, Cursor cursor) { + super.changeCursor(partitionIndex, cursor); + + if (cursor == null || !cursor.moveToFirst()) { + return; + } + + // hasProfile tells whether the first row is a profile + final boolean hasProfile = cursor.getInt(ContactQuery.CONTACT_IS_USER_PROFILE) == 1; + + // Add ME profile on top of favorites + cursor.moveToFirst(); + setProfileExists(hasProfile); + } + + /** @return Projection useful for children. */ + protected final String[] getProjection(boolean forSearch) { + final int sortOrder = getContactNameDisplayOrder(); + if (forSearch) { + if (sortOrder == ContactsPreferences.DISPLAY_ORDER_PRIMARY) { + return ContactQuery.FILTER_PROJECTION_PRIMARY; + } else { + return ContactQuery.FILTER_PROJECTION_ALTERNATIVE; + } + } else { + if (sortOrder == ContactsPreferences.DISPLAY_ORDER_PRIMARY) { + return ContactQuery.CONTACT_PROJECTION_PRIMARY; + } else { + return ContactQuery.CONTACT_PROJECTION_ALTERNATIVE; + } + } + } + + protected static class ContactQuery { + + public static final int CONTACT_ID = 0; + public static final int CONTACT_DISPLAY_NAME = 1; + public static final int CONTACT_PRESENCE_STATUS = 2; + public static final int CONTACT_CONTACT_STATUS = 3; + public static final int CONTACT_PHOTO_ID = 4; + public static final int CONTACT_PHOTO_URI = 5; + public static final int CONTACT_LOOKUP_KEY = 6; + public static final int CONTACT_IS_USER_PROFILE = 7; + public static final int CONTACT_PHONETIC_NAME = 8; + public static final int CONTACT_STARRED = 9; + public static final int CONTACT_SNIPPET = 10; + private static final String[] CONTACT_PROJECTION_PRIMARY = + new String[] { + Contacts._ID, // 0 + Contacts.DISPLAY_NAME_PRIMARY, // 1 + Contacts.CONTACT_PRESENCE, // 2 + Contacts.CONTACT_STATUS, // 3 + Contacts.PHOTO_ID, // 4 + Contacts.PHOTO_THUMBNAIL_URI, // 5 + Contacts.LOOKUP_KEY, // 6 + Contacts.IS_USER_PROFILE, // 7 + Contacts.PHONETIC_NAME, // 8 + Contacts.STARRED, // 9 + }; + private static final String[] CONTACT_PROJECTION_ALTERNATIVE = + new String[] { + Contacts._ID, // 0 + Contacts.DISPLAY_NAME_ALTERNATIVE, // 1 + Contacts.CONTACT_PRESENCE, // 2 + Contacts.CONTACT_STATUS, // 3 + Contacts.PHOTO_ID, // 4 + Contacts.PHOTO_THUMBNAIL_URI, // 5 + Contacts.LOOKUP_KEY, // 6 + Contacts.IS_USER_PROFILE, // 7 + Contacts.PHONETIC_NAME, // 8 + Contacts.STARRED, // 9 + }; + private static final String[] FILTER_PROJECTION_PRIMARY = + new String[] { + Contacts._ID, // 0 + Contacts.DISPLAY_NAME_PRIMARY, // 1 + Contacts.CONTACT_PRESENCE, // 2 + Contacts.CONTACT_STATUS, // 3 + Contacts.PHOTO_ID, // 4 + Contacts.PHOTO_THUMBNAIL_URI, // 5 + Contacts.LOOKUP_KEY, // 6 + Contacts.IS_USER_PROFILE, // 7 + Contacts.PHONETIC_NAME, // 8 + Contacts.STARRED, // 9 + SearchSnippets.SNIPPET, // 10 + }; + private static final String[] FILTER_PROJECTION_ALTERNATIVE = + new String[] { + Contacts._ID, // 0 + Contacts.DISPLAY_NAME_ALTERNATIVE, // 1 + Contacts.CONTACT_PRESENCE, // 2 + Contacts.CONTACT_STATUS, // 3 + Contacts.PHOTO_ID, // 4 + Contacts.PHOTO_THUMBNAIL_URI, // 5 + Contacts.LOOKUP_KEY, // 6 + Contacts.IS_USER_PROFILE, // 7 + Contacts.PHONETIC_NAME, // 8 + Contacts.STARRED, // 9 + SearchSnippets.SNIPPET, // 10 + }; + } +} diff --git a/java/com/android/contacts/common/list/ContactListFilter.java b/java/com/android/contacts/common/list/ContactListFilter.java new file mode 100644 index 000000000..1a03bb64c --- /dev/null +++ b/java/com/android/contacts/common/list/ContactListFilter.java @@ -0,0 +1,297 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.list; + +import android.content.SharedPreferences; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; +import android.provider.ContactsContract.RawContacts; +import android.text.TextUtils; + +/** Contact list filter parameters. */ +public final class ContactListFilter implements Comparable, Parcelable { + + public static final int FILTER_TYPE_DEFAULT = -1; + public static final int FILTER_TYPE_ALL_ACCOUNTS = -2; + public static final int FILTER_TYPE_CUSTOM = -3; + public static final int FILTER_TYPE_STARRED = -4; + public static final int FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY = -5; + public static final int FILTER_TYPE_SINGLE_CONTACT = -6; + + public static final int FILTER_TYPE_ACCOUNT = 0; + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public ContactListFilter createFromParcel(Parcel source) { + int filterType = source.readInt(); + String accountName = source.readString(); + String accountType = source.readString(); + String dataSet = source.readString(); + return new ContactListFilter(filterType, accountType, accountName, dataSet, null); + } + + @Override + public ContactListFilter[] newArray(int size) { + return new ContactListFilter[size]; + } + }; + /** + * Obsolete filter which had been used in Honeycomb. This may be stored in {@link + * SharedPreferences}, but should be replaced with ALL filter when it is found. + * + *

TODO: "group" filter and relevant variables are all obsolete. Remove them. + */ + private static final int FILTER_TYPE_GROUP = 1; + + private static final String KEY_FILTER_TYPE = "filter.type"; + private static final String KEY_ACCOUNT_NAME = "filter.accountName"; + private static final String KEY_ACCOUNT_TYPE = "filter.accountType"; + private static final String KEY_DATA_SET = "filter.dataSet"; + public final int filterType; + public final String accountType; + public final String accountName; + public final String dataSet; + public final Drawable icon; + private String mId; + + public ContactListFilter( + int filterType, String accountType, String accountName, String dataSet, Drawable icon) { + this.filterType = filterType; + this.accountType = accountType; + this.accountName = accountName; + this.dataSet = dataSet; + this.icon = icon; + } + + public static ContactListFilter createFilterWithType(int filterType) { + return new ContactListFilter(filterType, null, null, null, null); + } + + public static ContactListFilter createAccountFilter( + String accountType, String accountName, String dataSet, Drawable icon) { + return new ContactListFilter( + ContactListFilter.FILTER_TYPE_ACCOUNT, accountType, accountName, dataSet, icon); + } + + /** + * Store the given {@link ContactListFilter} to preferences. If the requested filter is of type + * {@link #FILTER_TYPE_SINGLE_CONTACT} then do not save it to preferences because it is a + * temporary state. + */ + public static void storeToPreferences(SharedPreferences prefs, ContactListFilter filter) { + if (filter != null && filter.filterType == FILTER_TYPE_SINGLE_CONTACT) { + return; + } + prefs + .edit() + .putInt(KEY_FILTER_TYPE, filter == null ? FILTER_TYPE_DEFAULT : filter.filterType) + .putString(KEY_ACCOUNT_NAME, filter == null ? null : filter.accountName) + .putString(KEY_ACCOUNT_TYPE, filter == null ? null : filter.accountType) + .putString(KEY_DATA_SET, filter == null ? null : filter.dataSet) + .apply(); + } + + /** + * Try to obtain ContactListFilter object saved in SharedPreference. If there's no info there, + * return ALL filter instead. + */ + public static ContactListFilter restoreDefaultPreferences(SharedPreferences prefs) { + ContactListFilter filter = restoreFromPreferences(prefs); + if (filter == null) { + filter = ContactListFilter.createFilterWithType(FILTER_TYPE_ALL_ACCOUNTS); + } + // "Group" filter is obsolete and thus is not exposed anymore. The "single contact mode" + // should also not be stored in preferences anymore since it is a temporary state. + if (filter.filterType == FILTER_TYPE_GROUP || filter.filterType == FILTER_TYPE_SINGLE_CONTACT) { + filter = ContactListFilter.createFilterWithType(FILTER_TYPE_ALL_ACCOUNTS); + } + return filter; + } + + private static ContactListFilter restoreFromPreferences(SharedPreferences prefs) { + int filterType = prefs.getInt(KEY_FILTER_TYPE, FILTER_TYPE_DEFAULT); + if (filterType == FILTER_TYPE_DEFAULT) { + return null; + } + + String accountName = prefs.getString(KEY_ACCOUNT_NAME, null); + String accountType = prefs.getString(KEY_ACCOUNT_TYPE, null); + String dataSet = prefs.getString(KEY_DATA_SET, null); + return new ContactListFilter(filterType, accountType, accountName, dataSet, null); + } + + public static final String filterTypeToString(int filterType) { + switch (filterType) { + case FILTER_TYPE_DEFAULT: + return "FILTER_TYPE_DEFAULT"; + case FILTER_TYPE_ALL_ACCOUNTS: + return "FILTER_TYPE_ALL_ACCOUNTS"; + case FILTER_TYPE_CUSTOM: + return "FILTER_TYPE_CUSTOM"; + case FILTER_TYPE_STARRED: + return "FILTER_TYPE_STARRED"; + case FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY: + return "FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY"; + case FILTER_TYPE_SINGLE_CONTACT: + return "FILTER_TYPE_SINGLE_CONTACT"; + case FILTER_TYPE_ACCOUNT: + return "FILTER_TYPE_ACCOUNT"; + default: + return "(unknown)"; + } + } + + /** Returns true if this filter is based on data and may become invalid over time. */ + public boolean isValidationRequired() { + return filterType == FILTER_TYPE_ACCOUNT; + } + + @Override + public String toString() { + switch (filterType) { + case FILTER_TYPE_DEFAULT: + return "default"; + case FILTER_TYPE_ALL_ACCOUNTS: + return "all_accounts"; + case FILTER_TYPE_CUSTOM: + return "custom"; + case FILTER_TYPE_STARRED: + return "starred"; + case FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY: + return "with_phones"; + case FILTER_TYPE_SINGLE_CONTACT: + return "single"; + case FILTER_TYPE_ACCOUNT: + return "account: " + + accountType + + (dataSet != null ? "/" + dataSet : "") + + " " + + accountName; + } + return super.toString(); + } + + @Override + public int compareTo(ContactListFilter another) { + int res = accountName.compareTo(another.accountName); + if (res != 0) { + return res; + } + + res = accountType.compareTo(another.accountType); + if (res != 0) { + return res; + } + + return filterType - another.filterType; + } + + @Override + public int hashCode() { + int code = filterType; + if (accountType != null) { + code = code * 31 + accountType.hashCode(); + code = code * 31 + accountName.hashCode(); + } + if (dataSet != null) { + code = code * 31 + dataSet.hashCode(); + } + return code; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + + if (!(other instanceof ContactListFilter)) { + return false; + } + + ContactListFilter otherFilter = (ContactListFilter) other; + return filterType == otherFilter.filterType + && TextUtils.equals(accountName, otherFilter.accountName) + && TextUtils.equals(accountType, otherFilter.accountType) + && TextUtils.equals(dataSet, otherFilter.dataSet); + + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(filterType); + dest.writeString(accountName); + dest.writeString(accountType); + dest.writeString(dataSet); + } + + @Override + public int describeContents() { + return 0; + } + + /** Returns a string that can be used as a stable persistent identifier for this filter. */ + public String getId() { + if (mId == null) { + StringBuilder sb = new StringBuilder(); + sb.append(filterType); + if (accountType != null) { + sb.append('-').append(accountType); + } + if (dataSet != null) { + sb.append('/').append(dataSet); + } + if (accountName != null) { + sb.append('-').append(accountName.replace('-', '_')); + } + mId = sb.toString(); + } + return mId; + } + + /** + * Adds the account query parameters to the given {@code uriBuilder}. + * + * @throws IllegalStateException if the filter type is not {@link #FILTER_TYPE_ACCOUNT}. + */ + public Uri.Builder addAccountQueryParameterToUrl(Uri.Builder uriBuilder) { + if (filterType != FILTER_TYPE_ACCOUNT) { + throw new IllegalStateException("filterType must be FILTER_TYPE_ACCOUNT"); + } + uriBuilder.appendQueryParameter(RawContacts.ACCOUNT_NAME, accountName); + uriBuilder.appendQueryParameter(RawContacts.ACCOUNT_TYPE, accountType); + if (!TextUtils.isEmpty(dataSet)) { + uriBuilder.appendQueryParameter(RawContacts.DATA_SET, dataSet); + } + return uriBuilder; + } + + public String toDebugString() { + final StringBuilder builder = new StringBuilder(); + builder.append("[filter type: " + filterType + " (" + filterTypeToString(filterType) + ")"); + if (filterType == FILTER_TYPE_ACCOUNT) { + builder + .append(", accountType: " + accountType) + .append(", accountName: " + accountName) + .append(", dataSet: " + dataSet); + } + builder.append(", icon: " + icon + "]"); + return builder.toString(); + } +} diff --git a/java/com/android/contacts/common/list/ContactListFilterController.java b/java/com/android/contacts/common/list/ContactListFilterController.java new file mode 100644 index 000000000..d2168f3f2 --- /dev/null +++ b/java/com/android/contacts/common/list/ContactListFilterController.java @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.contacts.common.list; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import com.android.contacts.common.model.AccountTypeManager; +import com.android.contacts.common.model.account.AccountWithDataSet; +import java.util.ArrayList; +import java.util.List; + +/** Manages {@link ContactListFilter}. All methods must be called from UI thread. */ +public abstract class ContactListFilterController { + + // singleton to cache the filter controller + private static ContactListFilterControllerImpl sFilterController = null; + + public static ContactListFilterController getInstance(Context context) { + // We may need to synchronize this in the future if background task will call this. + if (sFilterController == null) { + sFilterController = new ContactListFilterControllerImpl(context); + } + return sFilterController; + } + + public abstract void addListener(ContactListFilterListener listener); + + public abstract void removeListener(ContactListFilterListener listener); + + /** Return the currently-active filter. */ + public abstract ContactListFilter getFilter(); + + /** + * @param filter the filter + * @param persistent True when the given filter should be saved soon. False when the filter should + * not be saved. The latter case may happen when some Intent requires a certain type of UI + * (e.g. single contact) temporarily. + */ + public abstract void setContactListFilter(ContactListFilter filter, boolean persistent); + + public abstract void selectCustomFilter(); + + /** + * Checks if the current filter is valid and reset the filter if not. It may happen when an + * account is removed while the filter points to the account with {@link + * ContactListFilter#FILTER_TYPE_ACCOUNT} type, for example. It may also happen if the current + * filter is {@link ContactListFilter#FILTER_TYPE_SINGLE_CONTACT}, in which case, we should switch + * to the last saved filter in {@link SharedPreferences}. + */ + public abstract void checkFilterValidity(boolean notifyListeners); + + public interface ContactListFilterListener { + + void onContactListFilterChanged(); + } +} + +/** + * Stores the {@link ContactListFilter} selected by the user and saves it to {@link + * SharedPreferences} if necessary. + */ +class ContactListFilterControllerImpl extends ContactListFilterController { + + private final Context mContext; + private final List mListeners = + new ArrayList(); + private ContactListFilter mFilter; + + public ContactListFilterControllerImpl(Context context) { + mContext = context; + mFilter = ContactListFilter.restoreDefaultPreferences(getSharedPreferences()); + checkFilterValidity(true /* notify listeners */); + } + + @Override + public void addListener(ContactListFilterListener listener) { + mListeners.add(listener); + } + + @Override + public void removeListener(ContactListFilterListener listener) { + mListeners.remove(listener); + } + + @Override + public ContactListFilter getFilter() { + return mFilter; + } + + private SharedPreferences getSharedPreferences() { + return PreferenceManager.getDefaultSharedPreferences(mContext); + } + + @Override + public void setContactListFilter(ContactListFilter filter, boolean persistent) { + setContactListFilter(filter, persistent, true); + } + + private void setContactListFilter( + ContactListFilter filter, boolean persistent, boolean notifyListeners) { + if (!filter.equals(mFilter)) { + mFilter = filter; + if (persistent) { + ContactListFilter.storeToPreferences(getSharedPreferences(), mFilter); + } + if (notifyListeners && !mListeners.isEmpty()) { + notifyContactListFilterChanged(); + } + } + } + + @Override + public void selectCustomFilter() { + setContactListFilter( + ContactListFilter.createFilterWithType(ContactListFilter.FILTER_TYPE_CUSTOM), true); + } + + private void notifyContactListFilterChanged() { + for (ContactListFilterListener listener : mListeners) { + listener.onContactListFilterChanged(); + } + } + + @Override + public void checkFilterValidity(boolean notifyListeners) { + if (mFilter == null) { + return; + } + + switch (mFilter.filterType) { + case ContactListFilter.FILTER_TYPE_SINGLE_CONTACT: + setContactListFilter( + ContactListFilter.restoreDefaultPreferences(getSharedPreferences()), + false, + notifyListeners); + break; + case ContactListFilter.FILTER_TYPE_ACCOUNT: + if (!filterAccountExists()) { + // The current account filter points to invalid account. Use "all" filter + // instead. + setContactListFilter( + ContactListFilter.createFilterWithType(ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS), + true, + notifyListeners); + } + } + } + + /** @return true if the Account for the current filter exists. */ + private boolean filterAccountExists() { + final AccountTypeManager accountTypeManager = AccountTypeManager.getInstance(mContext); + final AccountWithDataSet filterAccount = + new AccountWithDataSet(mFilter.accountName, mFilter.accountType, mFilter.dataSet); + return accountTypeManager.contains(filterAccount, false); + } +} diff --git a/java/com/android/contacts/common/list/ContactListItemView.java b/java/com/android/contacts/common/list/ContactListItemView.java new file mode 100644 index 000000000..76842483a --- /dev/null +++ b/java/com/android/contacts/common/list/ContactListItemView.java @@ -0,0 +1,1513 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.list; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.TypedArray; +import android.database.Cursor; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.provider.ContactsContract; +import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.SearchSnippets; +import android.support.v4.content.ContextCompat; +import android.support.v4.graphics.drawable.DrawableCompat; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.TextUtils; +import android.text.TextUtils.TruncateAt; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView.SelectionBoundsAdjuster; +import android.widget.ImageView; +import android.widget.ImageView.ScaleType; +import android.widget.QuickContactBadge; +import android.widget.TextView; +import com.android.contacts.common.ContactPresenceIconUtil; +import com.android.contacts.common.ContactStatusUtil; +import com.android.contacts.common.R; +import com.android.contacts.common.compat.PhoneNumberUtilsCompat; +import com.android.contacts.common.format.TextHighlighter; +import com.android.contacts.common.util.ContactDisplayUtils; +import com.android.contacts.common.util.SearchUtil; +import com.android.dialer.compat.CompatUtils; +import com.android.dialer.util.ViewUtil; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A custom view for an item in the contact list. The view contains the contact's photo, a set of + * text views (for name, status, etc...) and icons for presence and call. The view uses no XML file + * for layout and all the measurements and layouts are done in the onMeasure and onLayout methods. + * + *

The layout puts the contact's photo on the right side of the view, the call icon (if present) + * to the left of the photo, the text lines are aligned to the left and the presence icon (if + * present) is set to the left of the status line. + * + *

The layout also supports a header (used as a header of a group of contacts) that is above the + * contact's data and a divider between contact view. + */ +public class ContactListItemView extends ViewGroup implements SelectionBoundsAdjuster { + private static final Pattern SPLIT_PATTERN = + Pattern.compile("([\\w-\\.]+)@((?:[\\w]+\\.)+)([a-zA-Z]{2,4})|[\\w]+"); + static final char SNIPPET_START_MATCH = '['; + static final char SNIPPET_END_MATCH = ']'; + /** A helper used to highlight a prefix in a text field. */ + private final TextHighlighter mTextHighlighter; + // Style values for layout and appearance + // The initialized values are defaults if none is provided through xml. + private int mPreferredHeight = 0; + private int mGapBetweenImageAndText = 0; + private int mGapBetweenLabelAndData = 0; + private int mPresenceIconMargin = 4; + private int mPresenceIconSize = 16; + private int mTextIndent = 0; + private int mTextOffsetTop; + private int mNameTextViewTextSize; + private int mHeaderWidth; + private Drawable mActivatedBackgroundDrawable; + private int mVideoCallIconSize = 32; + private int mVideoCallIconMargin = 16; + // Set in onLayout. Represent left and right position of the View on the screen. + private int mLeftOffset; + private int mRightOffset; + /** Used with {@link #mLabelView}, specifying the width ratio between label and data. */ + private int mLabelViewWidthWeight = 3; + /** Used with {@link #mDataView}, specifying the width ratio between label and data. */ + private int mDataViewWidthWeight = 5; + + private ArrayList mNameHighlightSequence; + private ArrayList mNumberHighlightSequence; + // Highlighting prefix for names. + private String mHighlightedPrefix; + /** Used to notify listeners when a video call icon is clicked. */ + private PhoneNumberListAdapter.Listener mPhoneNumberListAdapterListener; + /** Indicates whether to show the "video call" icon, used to initiate a video call. */ + private boolean mShowVideoCallIcon = false; + /** Indicates whether the view should leave room for the "video call" icon. */ + private boolean mSupportVideoCallIcon = false; + + private PhotoPosition mPhotoPosition = getDefaultPhotoPosition(false /* normal/non opposite */); + // Header layout data + private TextView mHeaderTextView; + private boolean mIsSectionHeaderEnabled; + // The views inside the contact view + private boolean mQuickContactEnabled = true; + private QuickContactBadge mQuickContact; + private ImageView mPhotoView; + private TextView mNameTextView; + private TextView mLabelView; + private TextView mDataView; + private TextView mSnippetView; + private TextView mStatusView; + private ImageView mPresenceIcon; + private ImageView mVideoCallIcon; + private ImageView mWorkProfileIcon; + private ColorStateList mSecondaryTextColor; + private int mDefaultPhotoViewSize = 0; + /** + * Can be effective even when {@link #mPhotoView} is null, as we want to have horizontal padding + * to align other data in this View. + */ + private int mPhotoViewWidth; + /** + * Can be effective even when {@link #mPhotoView} is null, as we want to have vertical padding. + */ + private int mPhotoViewHeight; + /** + * Only effective when {@link #mPhotoView} is null. When true all the Views on the right side of + * the photo should have horizontal padding on those left assuming there is a photo. + */ + private boolean mKeepHorizontalPaddingForPhotoView; + /** Only effective when {@link #mPhotoView} is null. */ + private boolean mKeepVerticalPaddingForPhotoView; + /** + * True when {@link #mPhotoViewWidth} and {@link #mPhotoViewHeight} are ready for being used. + * False indicates those values should be updated before being used in position calculation. + */ + private boolean mPhotoViewWidthAndHeightAreReady = false; + + private int mNameTextViewHeight; + private int mNameTextViewTextColor = Color.BLACK; + private int mPhoneticNameTextViewHeight; + private int mLabelViewHeight; + private int mDataViewHeight; + private int mSnippetTextViewHeight; + private int mStatusTextViewHeight; + private int mCheckBoxWidth; + // Holds Math.max(mLabelTextViewHeight, mDataViewHeight), assuming Label and Data share the + // same row. + private int mLabelAndDataViewMaxHeight; + private boolean mActivatedStateSupported; + private boolean mAdjustSelectionBoundsEnabled = true; + private Rect mBoundsWithoutHeader = new Rect(); + private CharSequence mUnknownNameText; + private int mPosition; + + public ContactListItemView(Context context) { + super(context); + + mTextHighlighter = new TextHighlighter(Typeface.BOLD); + mNameHighlightSequence = new ArrayList(); + mNumberHighlightSequence = new ArrayList(); + } + + public ContactListItemView(Context context, AttributeSet attrs, boolean supportVideoCallIcon) { + this(context, attrs); + + mSupportVideoCallIcon = supportVideoCallIcon; + } + + public ContactListItemView(Context context, AttributeSet attrs) { + super(context, attrs); + + TypedArray a; + + if (R.styleable.ContactListItemView != null) { + // Read all style values + a = getContext().obtainStyledAttributes(attrs, R.styleable.ContactListItemView); + mPreferredHeight = + a.getDimensionPixelSize( + R.styleable.ContactListItemView_list_item_height, mPreferredHeight); + mActivatedBackgroundDrawable = + a.getDrawable(R.styleable.ContactListItemView_activated_background); + + mGapBetweenImageAndText = + a.getDimensionPixelOffset( + R.styleable.ContactListItemView_list_item_gap_between_image_and_text, + mGapBetweenImageAndText); + mGapBetweenLabelAndData = + a.getDimensionPixelOffset( + R.styleable.ContactListItemView_list_item_gap_between_label_and_data, + mGapBetweenLabelAndData); + mPresenceIconMargin = + a.getDimensionPixelOffset( + R.styleable.ContactListItemView_list_item_presence_icon_margin, mPresenceIconMargin); + mPresenceIconSize = + a.getDimensionPixelOffset( + R.styleable.ContactListItemView_list_item_presence_icon_size, mPresenceIconSize); + mDefaultPhotoViewSize = + a.getDimensionPixelOffset( + R.styleable.ContactListItemView_list_item_photo_size, mDefaultPhotoViewSize); + mTextIndent = + a.getDimensionPixelOffset( + R.styleable.ContactListItemView_list_item_text_indent, mTextIndent); + mTextOffsetTop = + a.getDimensionPixelOffset( + R.styleable.ContactListItemView_list_item_text_offset_top, mTextOffsetTop); + mDataViewWidthWeight = + a.getInteger( + R.styleable.ContactListItemView_list_item_data_width_weight, mDataViewWidthWeight); + mLabelViewWidthWeight = + a.getInteger( + R.styleable.ContactListItemView_list_item_label_width_weight, mLabelViewWidthWeight); + mNameTextViewTextColor = + a.getColor( + R.styleable.ContactListItemView_list_item_name_text_color, mNameTextViewTextColor); + mNameTextViewTextSize = + (int) + a.getDimension( + R.styleable.ContactListItemView_list_item_name_text_size, + (int) getResources().getDimension(R.dimen.contact_browser_list_item_text_size)); + mVideoCallIconSize = + a.getDimensionPixelOffset( + R.styleable.ContactListItemView_list_item_video_call_icon_size, mVideoCallIconSize); + mVideoCallIconMargin = + a.getDimensionPixelOffset( + R.styleable.ContactListItemView_list_item_video_call_icon_margin, + mVideoCallIconMargin); + + setPaddingRelative( + a.getDimensionPixelOffset(R.styleable.ContactListItemView_list_item_padding_left, 0), + a.getDimensionPixelOffset(R.styleable.ContactListItemView_list_item_padding_top, 0), + a.getDimensionPixelOffset(R.styleable.ContactListItemView_list_item_padding_right, 0), + a.getDimensionPixelOffset(R.styleable.ContactListItemView_list_item_padding_bottom, 0)); + + a.recycle(); + } + + mTextHighlighter = new TextHighlighter(Typeface.BOLD); + + if (R.styleable.Theme != null) { + a = getContext().obtainStyledAttributes(R.styleable.Theme); + mSecondaryTextColor = a.getColorStateList(R.styleable.Theme_android_textColorSecondary); + a.recycle(); + } + + mHeaderWidth = getResources().getDimensionPixelSize(R.dimen.contact_list_section_header_width); + + if (mActivatedBackgroundDrawable != null) { + mActivatedBackgroundDrawable.setCallback(this); + } + + mNameHighlightSequence = new ArrayList(); + mNumberHighlightSequence = new ArrayList(); + + setLayoutDirection(View.LAYOUT_DIRECTION_LOCALE); + } + + public static final PhotoPosition getDefaultPhotoPosition(boolean opposite) { + final Locale locale = Locale.getDefault(); + final int layoutDirection = TextUtils.getLayoutDirectionFromLocale(locale); + switch (layoutDirection) { + case View.LAYOUT_DIRECTION_RTL: + return (opposite ? PhotoPosition.LEFT : PhotoPosition.RIGHT); + case View.LAYOUT_DIRECTION_LTR: + default: + return (opposite ? PhotoPosition.RIGHT : PhotoPosition.LEFT); + } + } + + /** + * Helper method for splitting a string into tokens. The lists passed in are populated with the + * tokens and offsets into the content of each token. The tokenization function parses e-mail + * addresses as a single token; otherwise it splits on any non-alphanumeric character. + * + * @param content Content to split. + * @return List of token strings. + */ + private static List split(String content) { + final Matcher matcher = SPLIT_PATTERN.matcher(content); + final ArrayList tokens = new ArrayList<>(); + while (matcher.find()) { + tokens.add(matcher.group()); + } + return tokens; + } + + public void setUnknownNameText(CharSequence unknownNameText) { + mUnknownNameText = unknownNameText; + } + + public void setQuickContactEnabled(boolean flag) { + mQuickContactEnabled = flag; + } + + /** + * Sets whether the video calling icon is shown. For the video calling icon to be shown, {@link + * #mSupportVideoCallIcon} must be {@code true}. + * + * @param showVideoCallIcon {@code true} if the video calling icon is shown, {@code false} + * otherwise. + * @param listener Listener to notify when the video calling icon is clicked. + * @param position The position in the adapater of the video calling icon. + */ + public void setShowVideoCallIcon( + boolean showVideoCallIcon, PhoneNumberListAdapter.Listener listener, int position) { + mShowVideoCallIcon = showVideoCallIcon; + mPhoneNumberListAdapterListener = listener; + mPosition = position; + + if (mShowVideoCallIcon) { + if (mVideoCallIcon == null) { + mVideoCallIcon = new ImageView(getContext()); + addView(mVideoCallIcon); + } + mVideoCallIcon.setContentDescription( + getContext().getString(R.string.description_search_video_call)); + mVideoCallIcon.setImageResource(R.drawable.ic_search_video_call); + mVideoCallIcon.setScaleType(ScaleType.CENTER); + mVideoCallIcon.setVisibility(View.VISIBLE); + mVideoCallIcon.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View v) { + // Inform the adapter that the video calling icon was clicked. + if (mPhoneNumberListAdapterListener != null) { + mPhoneNumberListAdapterListener.onVideoCallIconClicked(mPosition); + } + } + }); + } else { + if (mVideoCallIcon != null) { + mVideoCallIcon.setVisibility(View.GONE); + } + } + } + + /** + * Sets whether the view supports a video calling icon. This is independent of whether the view is + * actually showing an icon. Support for the video calling icon ensures that the layout leaves + * space for the video icon, should it be shown. + * + * @param supportVideoCallIcon {@code true} if the video call icon is supported, {@code false} + * otherwise. + */ + public void setSupportVideoCallIcon(boolean supportVideoCallIcon) { + mSupportVideoCallIcon = supportVideoCallIcon; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // We will match parent's width and wrap content vertically, but make sure + // height is no less than listPreferredItemHeight. + final int specWidth = resolveSize(0, widthMeasureSpec); + final int preferredHeight = mPreferredHeight; + + mNameTextViewHeight = 0; + mPhoneticNameTextViewHeight = 0; + mLabelViewHeight = 0; + mDataViewHeight = 0; + mLabelAndDataViewMaxHeight = 0; + mSnippetTextViewHeight = 0; + mStatusTextViewHeight = 0; + mCheckBoxWidth = 0; + + ensurePhotoViewSize(); + + // Width each TextView is able to use. + int effectiveWidth; + // All the other Views will honor the photo, so available width for them may be shrunk. + if (mPhotoViewWidth > 0 || mKeepHorizontalPaddingForPhotoView) { + effectiveWidth = + specWidth + - getPaddingLeft() + - getPaddingRight() + - (mPhotoViewWidth + mGapBetweenImageAndText); + } else { + effectiveWidth = specWidth - getPaddingLeft() - getPaddingRight(); + } + + if (mIsSectionHeaderEnabled) { + effectiveWidth -= mHeaderWidth + mGapBetweenImageAndText; + } + + if (mSupportVideoCallIcon) { + effectiveWidth -= (mVideoCallIconSize + mVideoCallIconMargin); + } + + // Go over all visible text views and measure actual width of each of them. + // Also calculate their heights to get the total height for this entire view. + + if (isVisible(mNameTextView)) { + // Calculate width for name text - this parallels similar measurement in onLayout. + int nameTextWidth = effectiveWidth; + if (mPhotoPosition != PhotoPosition.LEFT) { + nameTextWidth -= mTextIndent; + } + mNameTextView.measure( + MeasureSpec.makeMeasureSpec(nameTextWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + mNameTextViewHeight = mNameTextView.getMeasuredHeight(); + } + + // If both data (phone number/email address) and label (type like "MOBILE") are quite long, + // we should ellipsize both using appropriate ratio. + final int dataWidth; + final int labelWidth; + if (isVisible(mDataView)) { + if (isVisible(mLabelView)) { + final int totalWidth = effectiveWidth - mGapBetweenLabelAndData; + dataWidth = + ((totalWidth * mDataViewWidthWeight) / (mDataViewWidthWeight + mLabelViewWidthWeight)); + labelWidth = + ((totalWidth * mLabelViewWidthWeight) / (mDataViewWidthWeight + mLabelViewWidthWeight)); + } else { + dataWidth = effectiveWidth; + labelWidth = 0; + } + } else { + dataWidth = 0; + if (isVisible(mLabelView)) { + labelWidth = effectiveWidth; + } else { + labelWidth = 0; + } + } + + if (isVisible(mDataView)) { + mDataView.measure( + MeasureSpec.makeMeasureSpec(dataWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + mDataViewHeight = mDataView.getMeasuredHeight(); + } + + if (isVisible(mLabelView)) { + mLabelView.measure( + MeasureSpec.makeMeasureSpec(labelWidth, MeasureSpec.AT_MOST), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + mLabelViewHeight = mLabelView.getMeasuredHeight(); + } + mLabelAndDataViewMaxHeight = Math.max(mLabelViewHeight, mDataViewHeight); + + if (isVisible(mSnippetView)) { + mSnippetView.measure( + MeasureSpec.makeMeasureSpec(effectiveWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + mSnippetTextViewHeight = mSnippetView.getMeasuredHeight(); + } + + // Status view height is the biggest of the text view and the presence icon + if (isVisible(mPresenceIcon)) { + mPresenceIcon.measure( + MeasureSpec.makeMeasureSpec(mPresenceIconSize, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(mPresenceIconSize, MeasureSpec.EXACTLY)); + mStatusTextViewHeight = mPresenceIcon.getMeasuredHeight(); + } + + if (mSupportVideoCallIcon && isVisible(mVideoCallIcon)) { + mVideoCallIcon.measure( + MeasureSpec.makeMeasureSpec(mVideoCallIconSize, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(mVideoCallIconSize, MeasureSpec.EXACTLY)); + } + + if (isVisible(mWorkProfileIcon)) { + mWorkProfileIcon.measure( + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + mNameTextViewHeight = Math.max(mNameTextViewHeight, mWorkProfileIcon.getMeasuredHeight()); + } + + if (isVisible(mStatusView)) { + // Presence and status are in a same row, so status will be affected by icon size. + final int statusWidth; + if (isVisible(mPresenceIcon)) { + statusWidth = (effectiveWidth - mPresenceIcon.getMeasuredWidth() - mPresenceIconMargin); + } else { + statusWidth = effectiveWidth; + } + mStatusView.measure( + MeasureSpec.makeMeasureSpec(statusWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + mStatusTextViewHeight = Math.max(mStatusTextViewHeight, mStatusView.getMeasuredHeight()); + } + + // Calculate height including padding. + int height = + (mNameTextViewHeight + + mPhoneticNameTextViewHeight + + mLabelAndDataViewMaxHeight + + mSnippetTextViewHeight + + mStatusTextViewHeight); + + // Make sure the height is at least as high as the photo + height = Math.max(height, mPhotoViewHeight + getPaddingBottom() + getPaddingTop()); + + // Make sure height is at least the preferred height + height = Math.max(height, preferredHeight); + + // Measure the header if it is visible. + if (mHeaderTextView != null && mHeaderTextView.getVisibility() == VISIBLE) { + mHeaderTextView.measure( + MeasureSpec.makeMeasureSpec(mHeaderWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + } + + setMeasuredDimension(specWidth, height); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + final int height = bottom - top; + final int width = right - left; + + // Determine the vertical bounds by laying out the header first. + int topBound = 0; + int bottomBound = height; + int leftBound = getPaddingLeft(); + int rightBound = width - getPaddingRight(); + + final boolean isLayoutRtl = ViewUtil.isViewLayoutRtl(this); + + // Put the section header on the left side of the contact view. + if (mIsSectionHeaderEnabled) { + // Align the text view all the way left, to be consistent with Contacts. + if (isLayoutRtl) { + rightBound = width; + } else { + leftBound = 0; + } + if (mHeaderTextView != null) { + int headerHeight = mHeaderTextView.getMeasuredHeight(); + int headerTopBound = (bottomBound + topBound - headerHeight) / 2 + mTextOffsetTop; + + mHeaderTextView.layout( + isLayoutRtl ? rightBound - mHeaderWidth : leftBound, + headerTopBound, + isLayoutRtl ? rightBound : leftBound + mHeaderWidth, + headerTopBound + headerHeight); + } + if (isLayoutRtl) { + rightBound -= mHeaderWidth; + } else { + leftBound += mHeaderWidth; + } + } + + mBoundsWithoutHeader.set(left + leftBound, topBound, left + rightBound, bottomBound); + mLeftOffset = left + leftBound; + mRightOffset = left + rightBound; + if (mIsSectionHeaderEnabled) { + if (isLayoutRtl) { + rightBound -= mGapBetweenImageAndText; + } else { + leftBound += mGapBetweenImageAndText; + } + } + + if (mActivatedStateSupported && isActivated()) { + mActivatedBackgroundDrawable.setBounds(mBoundsWithoutHeader); + } + + final View photoView = mQuickContact != null ? mQuickContact : mPhotoView; + if (mPhotoPosition == PhotoPosition.LEFT) { + // Photo is the left most view. All the other Views should on the right of the photo. + if (photoView != null) { + // Center the photo vertically + final int photoTop = topBound + (bottomBound - topBound - mPhotoViewHeight) / 2; + photoView.layout( + leftBound, photoTop, leftBound + mPhotoViewWidth, photoTop + mPhotoViewHeight); + leftBound += mPhotoViewWidth + mGapBetweenImageAndText; + } else if (mKeepHorizontalPaddingForPhotoView) { + // Draw nothing but keep the padding. + leftBound += mPhotoViewWidth + mGapBetweenImageAndText; + } + } else { + // Photo is the right most view. Right bound should be adjusted that way. + if (photoView != null) { + // Center the photo vertically + final int photoTop = topBound + (bottomBound - topBound - mPhotoViewHeight) / 2; + photoView.layout( + rightBound - mPhotoViewWidth, photoTop, rightBound, photoTop + mPhotoViewHeight); + rightBound -= (mPhotoViewWidth + mGapBetweenImageAndText); + } else if (mKeepHorizontalPaddingForPhotoView) { + // Draw nothing but keep the padding. + rightBound -= (mPhotoViewWidth + mGapBetweenImageAndText); + } + + // Add indent between left-most padding and texts. + leftBound += mTextIndent; + } + + if (mSupportVideoCallIcon) { + // Place the video call button at the end of the list (e.g. take into account RTL mode). + if (isVisible(mVideoCallIcon)) { + // Center the video icon vertically + final int videoIconTop = topBound + (bottomBound - topBound - mVideoCallIconSize) / 2; + + if (!isLayoutRtl) { + // When photo is on left, video icon is placed on the right edge. + mVideoCallIcon.layout( + rightBound - mVideoCallIconSize, + videoIconTop, + rightBound, + videoIconTop + mVideoCallIconSize); + } else { + // When photo is on right, video icon is placed on the left edge. + mVideoCallIcon.layout( + leftBound, + videoIconTop, + leftBound + mVideoCallIconSize, + videoIconTop + mVideoCallIconSize); + } + } + + if (mPhotoPosition == PhotoPosition.LEFT) { + rightBound -= (mVideoCallIconSize + mVideoCallIconMargin); + } else { + leftBound += mVideoCallIconSize + mVideoCallIconMargin; + } + } + + // Center text vertically, then apply the top offset. + final int totalTextHeight = + mNameTextViewHeight + + mPhoneticNameTextViewHeight + + mLabelAndDataViewMaxHeight + + mSnippetTextViewHeight + + mStatusTextViewHeight; + int textTopBound = (bottomBound + topBound - totalTextHeight) / 2 + mTextOffsetTop; + + // Work Profile icon align top + int workProfileIconWidth = 0; + if (isVisible(mWorkProfileIcon)) { + workProfileIconWidth = mWorkProfileIcon.getMeasuredWidth(); + final int distanceFromEnd = mCheckBoxWidth > 0 ? mCheckBoxWidth + mGapBetweenImageAndText : 0; + if (mPhotoPosition == PhotoPosition.LEFT) { + // When photo is on left, label is placed on the right edge of the list item. + mWorkProfileIcon.layout( + rightBound - workProfileIconWidth - distanceFromEnd, + textTopBound, + rightBound - distanceFromEnd, + textTopBound + mNameTextViewHeight); + } else { + // When photo is on right, label is placed on the left of data view. + mWorkProfileIcon.layout( + leftBound + distanceFromEnd, + textTopBound, + leftBound + workProfileIconWidth + distanceFromEnd, + textTopBound + mNameTextViewHeight); + } + } + + // Layout all text view and presence icon + // Put name TextView first + if (isVisible(mNameTextView)) { + final int distanceFromEnd = + workProfileIconWidth + + (mCheckBoxWidth > 0 ? mCheckBoxWidth + mGapBetweenImageAndText : 0); + if (mPhotoPosition == PhotoPosition.LEFT) { + mNameTextView.layout( + leftBound, + textTopBound, + rightBound - distanceFromEnd, + textTopBound + mNameTextViewHeight); + } else { + mNameTextView.layout( + leftBound + distanceFromEnd, + textTopBound, + rightBound, + textTopBound + mNameTextViewHeight); + } + } + + if (isVisible(mNameTextView) || isVisible(mWorkProfileIcon)) { + textTopBound += mNameTextViewHeight; + } + + // Presence and status + if (isLayoutRtl) { + int statusRightBound = rightBound; + if (isVisible(mPresenceIcon)) { + int iconWidth = mPresenceIcon.getMeasuredWidth(); + mPresenceIcon.layout( + rightBound - iconWidth, textTopBound, rightBound, textTopBound + mStatusTextViewHeight); + statusRightBound -= (iconWidth + mPresenceIconMargin); + } + + if (isVisible(mStatusView)) { + mStatusView.layout( + leftBound, textTopBound, statusRightBound, textTopBound + mStatusTextViewHeight); + } + } else { + int statusLeftBound = leftBound; + if (isVisible(mPresenceIcon)) { + int iconWidth = mPresenceIcon.getMeasuredWidth(); + mPresenceIcon.layout( + leftBound, textTopBound, leftBound + iconWidth, textTopBound + mStatusTextViewHeight); + statusLeftBound += (iconWidth + mPresenceIconMargin); + } + + if (isVisible(mStatusView)) { + mStatusView.layout( + statusLeftBound, textTopBound, rightBound, textTopBound + mStatusTextViewHeight); + } + } + + if (isVisible(mStatusView) || isVisible(mPresenceIcon)) { + textTopBound += mStatusTextViewHeight; + } + + // Rest of text views + int dataLeftBound = leftBound; + + // Label and Data align bottom. + if (isVisible(mLabelView)) { + if (!isLayoutRtl) { + mLabelView.layout( + dataLeftBound, + textTopBound + mLabelAndDataViewMaxHeight - mLabelViewHeight, + rightBound, + textTopBound + mLabelAndDataViewMaxHeight); + dataLeftBound += mLabelView.getMeasuredWidth() + mGapBetweenLabelAndData; + } else { + dataLeftBound = leftBound + mLabelView.getMeasuredWidth(); + mLabelView.layout( + rightBound - mLabelView.getMeasuredWidth(), + textTopBound + mLabelAndDataViewMaxHeight - mLabelViewHeight, + rightBound, + textTopBound + mLabelAndDataViewMaxHeight); + rightBound -= (mLabelView.getMeasuredWidth() + mGapBetweenLabelAndData); + } + } + + if (isVisible(mDataView)) { + if (!isLayoutRtl) { + mDataView.layout( + dataLeftBound, + textTopBound + mLabelAndDataViewMaxHeight - mDataViewHeight, + rightBound, + textTopBound + mLabelAndDataViewMaxHeight); + } else { + mDataView.layout( + rightBound - mDataView.getMeasuredWidth(), + textTopBound + mLabelAndDataViewMaxHeight - mDataViewHeight, + rightBound, + textTopBound + mLabelAndDataViewMaxHeight); + } + } + if (isVisible(mLabelView) || isVisible(mDataView)) { + textTopBound += mLabelAndDataViewMaxHeight; + } + + if (isVisible(mSnippetView)) { + mSnippetView.layout( + leftBound, textTopBound, rightBound, textTopBound + mSnippetTextViewHeight); + } + } + + @Override + public void adjustListItemSelectionBounds(Rect bounds) { + if (mAdjustSelectionBoundsEnabled) { + bounds.top += mBoundsWithoutHeader.top; + bounds.bottom = bounds.top + mBoundsWithoutHeader.height(); + bounds.left = mBoundsWithoutHeader.left; + bounds.right = mBoundsWithoutHeader.right; + } + } + + protected boolean isVisible(View view) { + return view != null && view.getVisibility() == View.VISIBLE; + } + + /** Extracts width and height from the style */ + private void ensurePhotoViewSize() { + if (!mPhotoViewWidthAndHeightAreReady) { + mPhotoViewWidth = mPhotoViewHeight = getDefaultPhotoViewSize(); + if (!mQuickContactEnabled && mPhotoView == null) { + if (!mKeepHorizontalPaddingForPhotoView) { + mPhotoViewWidth = 0; + } + if (!mKeepVerticalPaddingForPhotoView) { + mPhotoViewHeight = 0; + } + } + + mPhotoViewWidthAndHeightAreReady = true; + } + } + + protected int getDefaultPhotoViewSize() { + return mDefaultPhotoViewSize; + } + + /** + * Gets a LayoutParam that corresponds to the default photo size. + * + * @return A new LayoutParam. + */ + private LayoutParams getDefaultPhotoLayoutParams() { + LayoutParams params = generateDefaultLayoutParams(); + params.width = getDefaultPhotoViewSize(); + params.height = params.width; + return params; + } + + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + if (mActivatedStateSupported) { + mActivatedBackgroundDrawable.setState(getDrawableState()); + } + } + + @Override + protected boolean verifyDrawable(Drawable who) { + return who == mActivatedBackgroundDrawable || super.verifyDrawable(who); + } + + @Override + public void jumpDrawablesToCurrentState() { + super.jumpDrawablesToCurrentState(); + if (mActivatedStateSupported) { + mActivatedBackgroundDrawable.jumpToCurrentState(); + } + } + + @Override + public void dispatchDraw(Canvas canvas) { + if (mActivatedStateSupported && isActivated()) { + mActivatedBackgroundDrawable.draw(canvas); + } + + super.dispatchDraw(canvas); + } + + /** Sets section header or makes it invisible if the title is null. */ + public void setSectionHeader(String title) { + if (!TextUtils.isEmpty(title)) { + if (mHeaderTextView == null) { + mHeaderTextView = new TextView(getContext()); + mHeaderTextView.setTextAppearance(getContext(), R.style.SectionHeaderStyle); + mHeaderTextView.setGravity(Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL); + addView(mHeaderTextView); + } + setMarqueeText(mHeaderTextView, title); + mHeaderTextView.setVisibility(View.VISIBLE); + mHeaderTextView.setAllCaps(true); + } else if (mHeaderTextView != null) { + mHeaderTextView.setVisibility(View.GONE); + } + } + + public void setIsSectionHeaderEnabled(boolean isSectionHeaderEnabled) { + mIsSectionHeaderEnabled = isSectionHeaderEnabled; + } + + /** Returns the quick contact badge, creating it if necessary. */ + public QuickContactBadge getQuickContact() { + if (!mQuickContactEnabled) { + throw new IllegalStateException("QuickContact is disabled for this view"); + } + if (mQuickContact == null) { + mQuickContact = new QuickContactBadge(getContext()); + if (CompatUtils.isLollipopCompatible()) { + mQuickContact.setOverlay(null); + } + mQuickContact.setLayoutParams(getDefaultPhotoLayoutParams()); + if (mNameTextView != null) { + mQuickContact.setContentDescription( + getContext() + .getString(R.string.description_quick_contact_for, mNameTextView.getText())); + } + + addView(mQuickContact); + mPhotoViewWidthAndHeightAreReady = false; + } + return mQuickContact; + } + + /** Returns the photo view, creating it if necessary. */ + public ImageView getPhotoView() { + if (mPhotoView == null) { + mPhotoView = new ImageView(getContext()); + mPhotoView.setLayoutParams(getDefaultPhotoLayoutParams()); + // Quick contact style used above will set a background - remove it + mPhotoView.setBackground(null); + addView(mPhotoView); + mPhotoViewWidthAndHeightAreReady = false; + } + return mPhotoView; + } + + /** Removes the photo view. */ + public void removePhotoView() { + removePhotoView(false, true); + } + + /** + * Removes the photo view. + * + * @param keepHorizontalPadding True means data on the right side will have padding on left, + * pretending there is still a photo view. + * @param keepVerticalPadding True means the View will have some height enough for accommodating a + * photo view. + */ + public void removePhotoView(boolean keepHorizontalPadding, boolean keepVerticalPadding) { + mPhotoViewWidthAndHeightAreReady = false; + mKeepHorizontalPaddingForPhotoView = keepHorizontalPadding; + mKeepVerticalPaddingForPhotoView = keepVerticalPadding; + if (mPhotoView != null) { + removeView(mPhotoView); + mPhotoView = null; + } + if (mQuickContact != null) { + removeView(mQuickContact); + mQuickContact = null; + } + } + + /** + * Sets a word prefix that will be highlighted if encountered in fields like name and search + * snippet. This will disable the mask highlighting for names. + * + *

NOTE: must be all upper-case + */ + public void setHighlightedPrefix(String upperCasePrefix) { + mHighlightedPrefix = upperCasePrefix; + } + + /** Clears previously set highlight sequences for the view. */ + public void clearHighlightSequences() { + mNameHighlightSequence.clear(); + mNumberHighlightSequence.clear(); + mHighlightedPrefix = null; + } + + /** + * Adds a highlight sequence to the name highlighter. + * + * @param start The start position of the highlight sequence. + * @param end The end position of the highlight sequence. + */ + public void addNameHighlightSequence(int start, int end) { + mNameHighlightSequence.add(new HighlightSequence(start, end)); + } + + /** + * Adds a highlight sequence to the number highlighter. + * + * @param start The start position of the highlight sequence. + * @param end The end position of the highlight sequence. + */ + public void addNumberHighlightSequence(int start, int end) { + mNumberHighlightSequence.add(new HighlightSequence(start, end)); + } + + /** Returns the text view for the contact name, creating it if necessary. */ + public TextView getNameTextView() { + if (mNameTextView == null) { + mNameTextView = new TextView(getContext()); + mNameTextView.setSingleLine(true); + mNameTextView.setEllipsize(getTextEllipsis()); + mNameTextView.setTextColor(mNameTextViewTextColor); + mNameTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mNameTextViewTextSize); + // Manually call setActivated() since this view may be added after the first + // setActivated() call toward this whole item view. + mNameTextView.setActivated(isActivated()); + mNameTextView.setGravity(Gravity.CENTER_VERTICAL); + mNameTextView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); + mNameTextView.setId(R.id.cliv_name_textview); + if (CompatUtils.isLollipopCompatible()) { + mNameTextView.setElegantTextHeight(false); + } + addView(mNameTextView); + } + return mNameTextView; + } + + /** Adds or updates a text view for the data label. */ + public void setLabel(CharSequence text) { + if (TextUtils.isEmpty(text)) { + if (mLabelView != null) { + mLabelView.setVisibility(View.GONE); + } + } else { + getLabelView(); + setMarqueeText(mLabelView, text); + mLabelView.setVisibility(VISIBLE); + } + } + + /** Returns the text view for the data label, creating it if necessary. */ + public TextView getLabelView() { + if (mLabelView == null) { + mLabelView = new TextView(getContext()); + mLabelView.setLayoutParams( + new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); + + mLabelView.setSingleLine(true); + mLabelView.setEllipsize(getTextEllipsis()); + mLabelView.setTextAppearance(getContext(), R.style.TextAppearanceSmall); + if (mPhotoPosition == PhotoPosition.LEFT) { + mLabelView.setAllCaps(true); + } else { + mLabelView.setTypeface(mLabelView.getTypeface(), Typeface.BOLD); + } + mLabelView.setActivated(isActivated()); + mLabelView.setId(R.id.cliv_label_textview); + addView(mLabelView); + } + return mLabelView; + } + + /** + * Sets phone number for a list item. This takes care of number highlighting if the highlight mask + * exists. + */ + public void setPhoneNumber(String text) { + if (text == null) { + if (mDataView != null) { + mDataView.setVisibility(View.GONE); + } + } else { + getDataView(); + + // TODO: Format number using PhoneNumberUtils.formatNumber before assigning it to + // mDataView. Make sure that determination of the highlight sequences are done only + // after number formatting. + + // Sets phone number texts for display after highlighting it, if applicable. + // CharSequence textToSet = text; + final SpannableString textToSet = new SpannableString(text); + + if (mNumberHighlightSequence.size() != 0) { + final HighlightSequence highlightSequence = mNumberHighlightSequence.get(0); + mTextHighlighter.applyMaskingHighlight( + textToSet, highlightSequence.start, highlightSequence.end); + } + + setMarqueeText(mDataView, textToSet); + mDataView.setVisibility(VISIBLE); + + // We have a phone number as "mDataView" so make it always LTR and VIEW_START + mDataView.setTextDirection(View.TEXT_DIRECTION_LTR); + mDataView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); + } + } + + private void setMarqueeText(TextView textView, CharSequence text) { + if (getTextEllipsis() == TruncateAt.MARQUEE) { + // To show MARQUEE correctly (with END effect during non-active state), we need + // to build Spanned with MARQUEE in addition to TextView's ellipsize setting. + final SpannableString spannable = new SpannableString(text); + spannable.setSpan( + TruncateAt.MARQUEE, 0, spannable.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + textView.setText(spannable); + } else { + textView.setText(text); + } + } + + /** Returns the text view for the data text, creating it if necessary. */ + public TextView getDataView() { + if (mDataView == null) { + mDataView = new TextView(getContext()); + mDataView.setSingleLine(true); + mDataView.setEllipsize(getTextEllipsis()); + mDataView.setTextAppearance(getContext(), R.style.TextAppearanceSmall); + mDataView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); + mDataView.setActivated(isActivated()); + mDataView.setId(R.id.cliv_data_view); + if (CompatUtils.isLollipopCompatible()) { + mDataView.setElegantTextHeight(false); + } + addView(mDataView); + } + return mDataView; + } + + /** Adds or updates a text view for the search snippet. */ + public void setSnippet(String text) { + if (TextUtils.isEmpty(text)) { + if (mSnippetView != null) { + mSnippetView.setVisibility(View.GONE); + } + } else { + mTextHighlighter.setPrefixText(getSnippetView(), text, mHighlightedPrefix); + mSnippetView.setVisibility(VISIBLE); + if (ContactDisplayUtils.isPossiblePhoneNumber(text)) { + // Give the text-to-speech engine a hint that it's a phone number + mSnippetView.setContentDescription(PhoneNumberUtilsCompat.createTtsSpannable(text)); + } else { + mSnippetView.setContentDescription(null); + } + } + } + + /** Returns the text view for the search snippet, creating it if necessary. */ + public TextView getSnippetView() { + if (mSnippetView == null) { + mSnippetView = new TextView(getContext()); + mSnippetView.setSingleLine(true); + mSnippetView.setEllipsize(getTextEllipsis()); + mSnippetView.setTextAppearance(getContext(), android.R.style.TextAppearance_Small); + mSnippetView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); + mSnippetView.setActivated(isActivated()); + addView(mSnippetView); + } + return mSnippetView; + } + + /** Returns the text view for the status, creating it if necessary. */ + public TextView getStatusView() { + if (mStatusView == null) { + mStatusView = new TextView(getContext()); + mStatusView.setSingleLine(true); + mStatusView.setEllipsize(getTextEllipsis()); + mStatusView.setTextAppearance(getContext(), android.R.style.TextAppearance_Small); + mStatusView.setTextColor(mSecondaryTextColor); + mStatusView.setActivated(isActivated()); + mStatusView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); + addView(mStatusView); + } + return mStatusView; + } + + /** Adds or updates a text view for the status. */ + public void setStatus(CharSequence text) { + if (TextUtils.isEmpty(text)) { + if (mStatusView != null) { + mStatusView.setVisibility(View.GONE); + } + } else { + getStatusView(); + setMarqueeText(mStatusView, text); + mStatusView.setVisibility(VISIBLE); + } + } + + /** Adds or updates the presence icon view. */ + public void setPresence(Drawable icon) { + if (icon != null) { + if (mPresenceIcon == null) { + mPresenceIcon = new ImageView(getContext()); + addView(mPresenceIcon); + } + mPresenceIcon.setImageDrawable(icon); + mPresenceIcon.setScaleType(ScaleType.CENTER); + mPresenceIcon.setVisibility(View.VISIBLE); + } else { + if (mPresenceIcon != null) { + mPresenceIcon.setVisibility(View.GONE); + } + } + } + + /** + * Set to display work profile icon or not + * + * @param enabled set to display work profile icon or not + */ + public void setWorkProfileIconEnabled(boolean enabled) { + if (mWorkProfileIcon != null) { + mWorkProfileIcon.setVisibility(enabled ? View.VISIBLE : View.GONE); + } else if (enabled) { + mWorkProfileIcon = new ImageView(getContext()); + addView(mWorkProfileIcon); + mWorkProfileIcon.setImageResource(R.drawable.ic_work_profile); + mWorkProfileIcon.setScaleType(ScaleType.CENTER_INSIDE); + mWorkProfileIcon.setVisibility(View.VISIBLE); + } + } + + private TruncateAt getTextEllipsis() { + return TruncateAt.MARQUEE; + } + + public void showDisplayName(Cursor cursor, int nameColumnIndex) { + CharSequence name = cursor.getString(nameColumnIndex); + setDisplayName(name); + + // Since the quick contact content description is derived from the display name and there is + // no guarantee that when the quick contact is initialized the display name is already set, + // do it here too. + if (mQuickContact != null) { + mQuickContact.setContentDescription( + getContext().getString(R.string.description_quick_contact_for, mNameTextView.getText())); + } + } + + public void setDisplayName(CharSequence name) { + if (!TextUtils.isEmpty(name)) { + // Chooses the available highlighting method for highlighting. + if (mHighlightedPrefix != null) { + name = mTextHighlighter.applyPrefixHighlight(name, mHighlightedPrefix); + } else if (mNameHighlightSequence.size() != 0) { + final SpannableString spannableName = new SpannableString(name); + for (HighlightSequence highlightSequence : mNameHighlightSequence) { + mTextHighlighter.applyMaskingHighlight( + spannableName, highlightSequence.start, highlightSequence.end); + } + name = spannableName; + } + } else { + name = mUnknownNameText; + } + setMarqueeText(getNameTextView(), name); + + if (ContactDisplayUtils.isPossiblePhoneNumber(name)) { + // Give the text-to-speech engine a hint that it's a phone number + mNameTextView.setTextDirection(View.TEXT_DIRECTION_LTR); + mNameTextView.setContentDescription( + PhoneNumberUtilsCompat.createTtsSpannable(name.toString())); + } else { + // Remove span tags of highlighting for talkback to avoid reading highlighting and rest + // of the name into two separate parts. + mNameTextView.setContentDescription(name.toString()); + } + } + + public void hideDisplayName() { + if (mNameTextView != null) { + removeView(mNameTextView); + mNameTextView = null; + } + } + + /** Sets the proper icon (star or presence or nothing) and/or status message. */ + public void showPresenceAndStatusMessage( + Cursor cursor, int presenceColumnIndex, int contactStatusColumnIndex) { + Drawable icon = null; + int presence = 0; + if (!cursor.isNull(presenceColumnIndex)) { + presence = cursor.getInt(presenceColumnIndex); + icon = ContactPresenceIconUtil.getPresenceIcon(getContext(), presence); + } + setPresence(icon); + + String statusMessage = null; + if (contactStatusColumnIndex != 0 && !cursor.isNull(contactStatusColumnIndex)) { + statusMessage = cursor.getString(contactStatusColumnIndex); + } + // If there is no status message from the contact, but there was a presence value, then use + // the default status message string + if (statusMessage == null && presence != 0) { + statusMessage = ContactStatusUtil.getStatusString(getContext(), presence); + } + setStatus(statusMessage); + } + + /** Shows search snippet. */ + public void showSnippet(Cursor cursor, int summarySnippetColumnIndex) { + if (cursor.getColumnCount() <= summarySnippetColumnIndex + || !SearchSnippets.SNIPPET.equals(cursor.getColumnName(summarySnippetColumnIndex))) { + setSnippet(null); + return; + } + + String snippet = cursor.getString(summarySnippetColumnIndex); + + // Do client side snippeting if provider didn't do it + final Bundle extras = cursor.getExtras(); + if (extras.getBoolean(ContactsContract.DEFERRED_SNIPPETING)) { + + final String query = extras.getString(ContactsContract.DEFERRED_SNIPPETING_QUERY); + + String displayName = null; + int displayNameIndex = cursor.getColumnIndex(Contacts.DISPLAY_NAME); + if (displayNameIndex >= 0) { + displayName = cursor.getString(displayNameIndex); + } + + snippet = updateSnippet(snippet, query, displayName); + + } else { + if (snippet != null) { + int from = 0; + int to = snippet.length(); + int start = snippet.indexOf(SNIPPET_START_MATCH); + if (start == -1) { + snippet = null; + } else { + int firstNl = snippet.lastIndexOf('\n', start); + if (firstNl != -1) { + from = firstNl + 1; + } + int end = snippet.lastIndexOf(SNIPPET_END_MATCH); + if (end != -1) { + int lastNl = snippet.indexOf('\n', end); + if (lastNl != -1) { + to = lastNl; + } + } + + StringBuilder sb = new StringBuilder(); + for (int i = from; i < to; i++) { + char c = snippet.charAt(i); + if (c != SNIPPET_START_MATCH && c != SNIPPET_END_MATCH) { + sb.append(c); + } + } + snippet = sb.toString(); + } + } + } + + setSnippet(snippet); + } + + /** + * Used for deferred snippets from the database. The contents come back as large strings which + * need to be extracted for display. + * + * @param snippet The snippet from the database. + * @param query The search query substring. + * @param displayName The contact display name. + * @return The proper snippet to display. + */ + private String updateSnippet(String snippet, String query, String displayName) { + + if (TextUtils.isEmpty(snippet) || TextUtils.isEmpty(query)) { + return null; + } + query = SearchUtil.cleanStartAndEndOfSearchQuery(query.toLowerCase()); + + // If the display name already contains the query term, return empty - snippets should + // not be needed in that case. + if (!TextUtils.isEmpty(displayName)) { + final String lowerDisplayName = displayName.toLowerCase(); + final List nameTokens = split(lowerDisplayName); + for (String nameToken : nameTokens) { + if (nameToken.startsWith(query)) { + return null; + } + } + } + + // The snippet may contain multiple data lines. + // Show the first line that matches the query. + final SearchUtil.MatchedLine matched = SearchUtil.findMatchingLine(snippet, query); + + if (matched != null && matched.line != null) { + // Tokenize for long strings since the match may be at the end of it. + // Skip this part for short strings since the whole string will be displayed. + // Most contact strings are short so the snippetize method will be called infrequently. + final int lengthThreshold = + getResources().getInteger(R.integer.snippet_length_before_tokenize); + if (matched.line.length() > lengthThreshold) { + return snippetize(matched.line, matched.startIndex, lengthThreshold); + } else { + return matched.line; + } + } + + // No match found. + return null; + } + + private String snippetize(String line, int matchIndex, int maxLength) { + // Show up to maxLength characters. But we only show full tokens so show the last full token + // up to maxLength characters. So as many starting tokens as possible before trying ending + // tokens. + int remainingLength = maxLength; + int tempRemainingLength = remainingLength; + + // Start the end token after the matched query. + int index = matchIndex; + int endTokenIndex = index; + + // Find the match token first. + while (index < line.length()) { + if (!Character.isLetterOrDigit(line.charAt(index))) { + endTokenIndex = index; + remainingLength = tempRemainingLength; + break; + } + tempRemainingLength--; + index++; + } + + // Find as much content before the match. + index = matchIndex - 1; + tempRemainingLength = remainingLength; + int startTokenIndex = matchIndex; + while (index > -1 && tempRemainingLength > 0) { + if (!Character.isLetterOrDigit(line.charAt(index))) { + startTokenIndex = index; + remainingLength = tempRemainingLength; + } + tempRemainingLength--; + index--; + } + + index = endTokenIndex; + tempRemainingLength = remainingLength; + // Find remaining content at after match. + while (index < line.length() && tempRemainingLength > 0) { + if (!Character.isLetterOrDigit(line.charAt(index))) { + endTokenIndex = index; + } + tempRemainingLength--; + index++; + } + // Append ellipse if there is content before or after. + final StringBuilder sb = new StringBuilder(); + if (startTokenIndex > 0) { + sb.append("..."); + } + sb.append(line.substring(startTokenIndex, endTokenIndex)); + if (endTokenIndex < line.length()) { + sb.append("..."); + } + return sb.toString(); + } + + public void setActivatedStateSupported(boolean flag) { + this.mActivatedStateSupported = flag; + } + + public void setAdjustSelectionBoundsEnabled(boolean enabled) { + mAdjustSelectionBoundsEnabled = enabled; + } + + @Override + public void requestLayout() { + // We will assume that once measured this will not need to resize + // itself, so there is no need to pass the layout request to the parent + // view (ListView). + forceLayout(); + } + + public void setPhotoPosition(PhotoPosition photoPosition) { + mPhotoPosition = photoPosition; + } + + /** + * Set drawable resources directly for the drawable resource of the photo view. + * + * @param drawableId Id of drawable resource. + */ + public void setDrawableResource(int drawableId) { + ImageView photo = getPhotoView(); + photo.setScaleType(ImageView.ScaleType.CENTER); + final Drawable drawable = ContextCompat.getDrawable(getContext(), drawableId); + final int iconColor = ContextCompat.getColor(getContext(), R.color.search_shortcut_icon_color); + if (CompatUtils.isLollipopCompatible()) { + photo.setImageDrawable(drawable); + photo.setImageTintList(ColorStateList.valueOf(iconColor)); + } else { + final Drawable drawableWrapper = DrawableCompat.wrap(drawable).mutate(); + DrawableCompat.setTint(drawableWrapper, iconColor); + photo.setImageDrawable(drawableWrapper); + } + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + final float x = event.getX(); + final float y = event.getY(); + // If the touch event's coordinates are not within the view's header, then delegate + // to super.onTouchEvent so that regular view behavior is preserved. Otherwise, consume + // and ignore the touch event. + if (mBoundsWithoutHeader.contains((int) x, (int) y) || !pointIsInView(x, y)) { + return super.onTouchEvent(event); + } else { + return true; + } + } + + private final boolean pointIsInView(float localX, float localY) { + return localX >= mLeftOffset + && localX < mRightOffset + && localY >= 0 + && localY < (getBottom() - getTop()); + } + + /** + * Where to put contact photo. This affects the other Views' layout or look-and-feel. + * + *

TODO: replace enum with int constants + */ + public enum PhotoPosition { + LEFT, + RIGHT + } + + protected static class HighlightSequence { + + private final int start; + private final int end; + + HighlightSequence(int start, int end) { + this.start = start; + this.end = end; + } + } +} diff --git a/java/com/android/contacts/common/list/ContactListPinnedHeaderView.java b/java/com/android/contacts/common/list/ContactListPinnedHeaderView.java new file mode 100644 index 000000000..1f3e2bfe3 --- /dev/null +++ b/java/com/android/contacts/common/list/ContactListPinnedHeaderView.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.list; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.View; +import android.widget.LinearLayout.LayoutParams; +import android.widget.TextView; +import com.android.contacts.common.R; + +/** A custom view for the pinned section header shown at the top of the contact list. */ +public class ContactListPinnedHeaderView extends TextView { + + public ContactListPinnedHeaderView(Context context, AttributeSet attrs, View parent) { + super(context, attrs); + + if (R.styleable.ContactListItemView == null) { + return; + } + TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.ContactListItemView); + int backgroundColor = + a.getColor(R.styleable.ContactListItemView_list_item_background_color, Color.WHITE); + int textOffsetTop = + a.getDimensionPixelSize(R.styleable.ContactListItemView_list_item_text_offset_top, 0); + int paddingStartOffset = + a.getDimensionPixelSize(R.styleable.ContactListItemView_list_item_padding_left, 0); + int textWidth = getResources().getDimensionPixelSize(R.dimen.contact_list_section_header_width); + int widthIncludingPadding = paddingStartOffset + textWidth; + a.recycle(); + + setBackgroundColor(backgroundColor); + setTextAppearance(getContext(), R.style.SectionHeaderStyle); + setLayoutParams(new LayoutParams(textWidth, LayoutParams.WRAP_CONTENT)); + setLayoutDirection(parent.getLayoutDirection()); + setGravity(Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL); + + // Apply text top offset. Multiply by two, because we are implementing this by padding for a + // vertically centered view, rather than adjusting the position directly via a layout. + setPaddingRelative( + 0, getPaddingTop() + (textOffsetTop * 2), getPaddingEnd(), getPaddingBottom()); + } + + /** Sets section header or makes it invisible if the title is null. */ + public void setSectionHeaderTitle(String title) { + if (!TextUtils.isEmpty(title)) { + setText(title); + } else { + setVisibility(View.GONE); + } + } +} diff --git a/java/com/android/contacts/common/list/ContactTileView.java b/java/com/android/contacts/common/list/ContactTileView.java new file mode 100644 index 000000000..9273b0583 --- /dev/null +++ b/java/com/android/contacts/common/list/ContactTileView.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.contacts.common.list; + +import android.content.Context; +import android.graphics.Rect; +import android.net.Uri; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.TextView; +import com.android.contacts.common.ContactPhotoManager; +import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest; +import com.android.contacts.common.MoreContactUtils; +import com.android.contacts.common.R; + +/** A ContactTile displays a contact's picture and name */ +public abstract class ContactTileView extends FrameLayout { + + private static final String TAG = ContactTileView.class.getSimpleName(); + protected Listener mListener; + private Uri mLookupUri; + private ImageView mPhoto; + private TextView mName; + private ContactPhotoManager mPhotoManager = null; + + public ContactTileView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mName = (TextView) findViewById(R.id.contact_tile_name); + mPhoto = (ImageView) findViewById(R.id.contact_tile_image); + + OnClickListener listener = createClickListener(); + setOnClickListener(listener); + } + + protected OnClickListener createClickListener() { + return new OnClickListener() { + @Override + public void onClick(View v) { + if (mListener == null) { + return; + } + mListener.onContactSelected( + getLookupUri(), MoreContactUtils.getTargetRectFromView(ContactTileView.this)); + } + }; + } + + public void setPhotoManager(ContactPhotoManager photoManager) { + mPhotoManager = photoManager; + } + + /** + * Populates the data members to be displayed from the fields in {@link + * com.android.contacts.common.list.ContactEntry} + */ + public void loadFromContact(ContactEntry entry) { + + if (entry != null) { + mName.setText(getNameForView(entry)); + mLookupUri = entry.lookupUri; + + setVisibility(View.VISIBLE); + + if (mPhotoManager != null) { + DefaultImageRequest request = getDefaultImageRequest(entry.namePrimary, entry.lookupKey); + configureViewForImage(entry.photoUri == null); + if (mPhoto != null) { + mPhotoManager.loadPhoto( + mPhoto, + entry.photoUri, + getApproximateImageSize(), + isDarkTheme(), + isContactPhotoCircular(), + request); + + + } + } else { + Log.w(TAG, "contactPhotoManager not set"); + } + } else { + setVisibility(View.INVISIBLE); + } + } + + public void setListener(Listener listener) { + mListener = listener; + } + + public Uri getLookupUri() { + return mLookupUri; + } + + /** + * Returns the string that should actually be displayed as the contact's name. Subclasses can + * override this to return formatted versions of the name - i.e. first name only. + */ + protected String getNameForView(ContactEntry contactEntry) { + return contactEntry.namePrimary; + } + + /** + * Implemented by subclasses to estimate the size of the picture. This can return -1 if only a + * thumbnail is shown anyway + */ + protected abstract int getApproximateImageSize(); + + protected abstract boolean isDarkTheme(); + + /** + * Implemented by subclasses to reconfigure the view's layout and subviews, based on whether or + * not the contact has a user-defined photo. + * + * @param isDefaultImage True if the contact does not have a user-defined contact photo (which + * means a default contact image will be applied by the {@link ContactPhotoManager} + */ + protected void configureViewForImage(boolean isDefaultImage) { + // No-op by default. + } + + /** + * Implemented by subclasses to allow them to return a {@link DefaultImageRequest} with the + * various image parameters defined to match their own layouts. + * + * @param displayName The display name of the contact + * @param lookupKey The lookup key of the contact + * @return A {@link DefaultImageRequest} object with each field configured by the subclass as + * desired, or {@code null}. + */ + protected DefaultImageRequest getDefaultImageRequest(String displayName, String lookupKey) { + return new DefaultImageRequest(displayName, lookupKey, isContactPhotoCircular()); + } + + /** + * Whether contact photo should be displayed as a circular image. Implemented by subclasses so + * they can change which drawables to fetch. + */ + protected boolean isContactPhotoCircular() { + return true; + } + + public interface Listener { + + /** Notification that the contact was selected; no specific action is dictated. */ + void onContactSelected(Uri contactLookupUri, Rect viewRect); + + /** Notification that the specified number is to be called. */ + void onCallNumberDirectly(String phoneNumber); + } +} diff --git a/java/com/android/contacts/common/list/ContactsSectionIndexer.java b/java/com/android/contacts/common/list/ContactsSectionIndexer.java new file mode 100644 index 000000000..3f0f2b7ee --- /dev/null +++ b/java/com/android/contacts/common/list/ContactsSectionIndexer.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.list; + +import android.text.TextUtils; +import android.widget.SectionIndexer; +import java.util.Arrays; + +/** + * A section indexer that is configured with precomputed section titles and their respective counts. + */ +public class ContactsSectionIndexer implements SectionIndexer { + + private static final String BLANK_HEADER_STRING = " "; + private String[] mSections; + private int[] mPositions; + private int mCount; + + /** + * Constructor. + * + * @param sections a non-null array + * @param counts a non-null array of the same size as sections + */ + public ContactsSectionIndexer(String[] sections, int[] counts) { + if (sections == null || counts == null) { + throw new NullPointerException(); + } + + if (sections.length != counts.length) { + throw new IllegalArgumentException( + "The sections and counts arrays must have the same length"); + } + + // TODO process sections/counts based on current locale and/or specific section titles + + this.mSections = sections; + mPositions = new int[counts.length]; + int position = 0; + for (int i = 0; i < counts.length; i++) { + if (TextUtils.isEmpty(mSections[i])) { + mSections[i] = BLANK_HEADER_STRING; + } else if (!mSections[i].equals(BLANK_HEADER_STRING)) { + mSections[i] = mSections[i].trim(); + } + + mPositions[i] = position; + position += counts[i]; + } + mCount = position; + } + + public Object[] getSections() { + return mSections; + } + + public int getPositionForSection(int section) { + if (section < 0 || section >= mSections.length) { + return -1; + } + + return mPositions[section]; + } + + public int getSectionForPosition(int position) { + if (position < 0 || position >= mCount) { + return -1; + } + + int index = Arrays.binarySearch(mPositions, position); + + /* + * Consider this example: section positions are 0, 3, 5; the supplied + * position is 4. The section corresponding to position 4 starts at + * position 3, so the expected return value is 1. Binary search will not + * find 4 in the array and thus will return -insertPosition-1, i.e. -3. + * To get from that number to the expected value of 1 we need to negate + * and subtract 2. + */ + return index >= 0 ? index : -index - 2; + } + + public void setProfileAndFavoritesHeader(String header, int numberOfItemsToAdd) { + if (mSections != null) { + // Don't do anything if the header is already set properly. + if (mSections.length > 0 && header.equals(mSections[0])) { + return; + } + + // Since the section indexer isn't aware of the profile at the top, we need to add a + // special section at the top for it and shift everything else down. + String[] tempSections = new String[mSections.length + 1]; + int[] tempPositions = new int[mPositions.length + 1]; + tempSections[0] = header; + tempPositions[0] = 0; + for (int i = 1; i <= mPositions.length; i++) { + tempSections[i] = mSections[i - 1]; + tempPositions[i] = mPositions[i - 1] + numberOfItemsToAdd; + } + mSections = tempSections; + mPositions = tempPositions; + mCount = mCount + numberOfItemsToAdd; + } + } +} diff --git a/java/com/android/contacts/common/list/DefaultContactListAdapter.java b/java/com/android/contacts/common/list/DefaultContactListAdapter.java new file mode 100644 index 000000000..7bcae0e0e --- /dev/null +++ b/java/com/android/contacts/common/list/DefaultContactListAdapter.java @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.contacts.common.list; + +import android.content.Context; +import android.content.CursorLoader; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.net.Uri; +import android.net.Uri.Builder; +import android.preference.PreferenceManager; +import android.provider.ContactsContract; +import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.Directory; +import android.provider.ContactsContract.SearchSnippets; +import android.text.TextUtils; +import android.view.View; +import com.android.contacts.common.compat.ContactsCompat; +import com.android.contacts.common.preference.ContactsPreferences; +import java.util.ArrayList; +import java.util.List; + +/** A cursor adapter for the {@link ContactsContract.Contacts#CONTENT_TYPE} content type. */ +public class DefaultContactListAdapter extends ContactListAdapter { + + public DefaultContactListAdapter(Context context) { + super(context); + } + + @Override + public void configureLoader(CursorLoader loader, long directoryId) { + String sortOrder = null; + if (isSearchMode()) { + String query = getQueryString(); + if (query == null) { + query = ""; + } + query = query.trim(); + if (TextUtils.isEmpty(query)) { + // Regardless of the directory, we don't want anything returned, + // so let's just send a "nothing" query to the local directory. + loader.setUri(Contacts.CONTENT_URI); + loader.setProjection(getProjection(false)); + loader.setSelection("0"); + } else { + final Builder builder = ContactsCompat.getContentUri().buildUpon(); + appendSearchParameters(builder, query, directoryId); + loader.setUri(builder.build()); + loader.setProjection(getProjection(true)); + } + } else { + final ContactListFilter filter = getFilter(); + configureUri(loader, directoryId, filter); + loader.setProjection(getProjection(false)); + configureSelection(loader, directoryId, filter); + } + + if (getSortOrder() == ContactsPreferences.SORT_ORDER_PRIMARY) { + if (sortOrder == null) { + sortOrder = Contacts.SORT_KEY_PRIMARY; + } else { + sortOrder += ", " + Contacts.SORT_KEY_PRIMARY; + } + } else { + if (sortOrder == null) { + sortOrder = Contacts.SORT_KEY_ALTERNATIVE; + } else { + sortOrder += ", " + Contacts.SORT_KEY_ALTERNATIVE; + } + } + loader.setSortOrder(sortOrder); + } + + private void appendSearchParameters(Builder builder, String query, long directoryId) { + builder.appendPath(query); // Builder will encode the query + builder.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId)); + if (directoryId != Directory.DEFAULT && directoryId != Directory.LOCAL_INVISIBLE) { + builder.appendQueryParameter( + ContactsContract.LIMIT_PARAM_KEY, + String.valueOf(getDirectoryResultLimit(getDirectoryById(directoryId)))); + } + builder.appendQueryParameter(SearchSnippets.DEFERRED_SNIPPETING_KEY, "1"); + } + + protected void configureUri(CursorLoader loader, long directoryId, ContactListFilter filter) { + Uri uri = Contacts.CONTENT_URI; + + if (directoryId == Directory.DEFAULT && isSectionHeaderDisplayEnabled()) { + uri = ContactListAdapter.buildSectionIndexerUri(uri); + } + + // The "All accounts" filter is the same as the entire contents of Directory.DEFAULT + if (filter != null + && filter.filterType != ContactListFilter.FILTER_TYPE_CUSTOM + && filter.filterType != ContactListFilter.FILTER_TYPE_SINGLE_CONTACT) { + final Uri.Builder builder = uri.buildUpon(); + builder.appendQueryParameter( + ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT)); + if (filter.filterType == ContactListFilter.FILTER_TYPE_ACCOUNT) { + filter.addAccountQueryParameterToUrl(builder); + } + uri = builder.build(); + } + + loader.setUri(uri); + } + + private void configureSelection(CursorLoader loader, long directoryId, ContactListFilter filter) { + if (filter == null) { + return; + } + + if (directoryId != Directory.DEFAULT) { + return; + } + + StringBuilder selection = new StringBuilder(); + List selectionArgs = new ArrayList(); + + switch (filter.filterType) { + case ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS: + { + // We have already added directory=0 to the URI, which takes care of this + // filter + break; + } + case ContactListFilter.FILTER_TYPE_SINGLE_CONTACT: + { + // We have already added the lookup key to the URI, which takes care of this + // filter + break; + } + case ContactListFilter.FILTER_TYPE_STARRED: + { + selection.append(Contacts.STARRED + "!=0"); + break; + } + case ContactListFilter.FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY: + { + selection.append(Contacts.HAS_PHONE_NUMBER + "=1"); + break; + } + case ContactListFilter.FILTER_TYPE_CUSTOM: + { + selection.append(Contacts.IN_VISIBLE_GROUP + "=1"); + if (isCustomFilterForPhoneNumbersOnly()) { + selection.append(" AND " + Contacts.HAS_PHONE_NUMBER + "=1"); + } + break; + } + case ContactListFilter.FILTER_TYPE_ACCOUNT: + { + // We use query parameters for account filter, so no selection to add here. + break; + } + } + loader.setSelection(selection.toString()); + loader.setSelectionArgs(selectionArgs.toArray(new String[0])); + } + + @Override + protected void bindView(View itemView, int partition, Cursor cursor, int position) { + super.bindView(itemView, partition, cursor, position); + final ContactListItemView view = (ContactListItemView) itemView; + + view.setHighlightedPrefix(isSearchMode() ? getUpperCaseQueryString() : null); + + bindSectionHeaderAndDivider(view, position, cursor); + + if (isQuickContactEnabled()) { + bindQuickContact( + view, + partition, + cursor, + ContactQuery.CONTACT_PHOTO_ID, + ContactQuery.CONTACT_PHOTO_URI, + ContactQuery.CONTACT_ID, + ContactQuery.CONTACT_LOOKUP_KEY, + ContactQuery.CONTACT_DISPLAY_NAME); + } else { + if (getDisplayPhotos()) { + bindPhoto(view, partition, cursor); + } + } + + bindNameAndViewId(view, cursor); + bindPresenceAndStatusMessage(view, cursor); + + if (isSearchMode()) { + bindSearchSnippet(view, cursor); + } else { + view.setSnippet(null); + } + } + + private boolean isCustomFilterForPhoneNumbersOnly() { + // TODO: this flag should not be stored in shared prefs. It needs to be in the db. + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); + return prefs.getBoolean( + ContactsPreferences.PREF_DISPLAY_ONLY_PHONES, + ContactsPreferences.PREF_DISPLAY_ONLY_PHONES_DEFAULT); + } +} diff --git a/java/com/android/contacts/common/list/DirectoryListLoader.java b/java/com/android/contacts/common/list/DirectoryListLoader.java new file mode 100644 index 000000000..48b098c07 --- /dev/null +++ b/java/com/android/contacts/common/list/DirectoryListLoader.java @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.contacts.common.list; + +import android.content.AsyncTaskLoader; +import android.content.Context; +import android.content.pm.PackageManager; +import android.database.ContentObserver; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.net.Uri; +import android.os.Handler; +import android.provider.ContactsContract.Directory; +import android.text.TextUtils; +import android.util.Log; +import com.android.contacts.common.R; +import com.android.contacts.common.compat.DirectoryCompat; + +/** A specialized loader for the list of directories, see {@link Directory}. */ +public class DirectoryListLoader extends AsyncTaskLoader { + + public static final int SEARCH_MODE_NONE = 0; + public static final int SEARCH_MODE_DEFAULT = 1; + public static final int SEARCH_MODE_CONTACT_SHORTCUT = 2; + public static final int SEARCH_MODE_DATA_SHORTCUT = 3; + // This is a virtual column created for a MatrixCursor. + public static final String DIRECTORY_TYPE = "directoryType"; + private static final String TAG = "ContactEntryListAdapter"; + private static final String[] RESULT_PROJECTION = { + Directory._ID, DIRECTORY_TYPE, Directory.DISPLAY_NAME, Directory.PHOTO_SUPPORT, + }; + private final ContentObserver mObserver = + new ContentObserver(new Handler()) { + @Override + public void onChange(boolean selfChange) { + forceLoad(); + } + }; + private int mDirectorySearchMode; + private boolean mLocalInvisibleDirectoryEnabled; + private MatrixCursor mDefaultDirectoryList; + + public DirectoryListLoader(Context context) { + super(context); + } + + public void setDirectorySearchMode(int mode) { + mDirectorySearchMode = mode; + } + + /** + * A flag that indicates whether the {@link Directory#LOCAL_INVISIBLE} directory should be + * included in the results. + */ + public void setLocalInvisibleDirectoryEnabled(boolean flag) { + this.mLocalInvisibleDirectoryEnabled = flag; + } + + @Override + protected void onStartLoading() { + getContext().getContentResolver().registerContentObserver(DirectoryQuery.URI, false, mObserver); + forceLoad(); + } + + @Override + protected void onStopLoading() { + getContext().getContentResolver().unregisterContentObserver(mObserver); + } + + @Override + public Cursor loadInBackground() { + if (mDirectorySearchMode == SEARCH_MODE_NONE) { + return getDefaultDirectories(); + } + + MatrixCursor result = new MatrixCursor(RESULT_PROJECTION); + Context context = getContext(); + PackageManager pm = context.getPackageManager(); + String selection; + switch (mDirectorySearchMode) { + case SEARCH_MODE_DEFAULT: + selection = null; + break; + + case SEARCH_MODE_CONTACT_SHORTCUT: + selection = Directory.SHORTCUT_SUPPORT + "=" + Directory.SHORTCUT_SUPPORT_FULL; + break; + + case SEARCH_MODE_DATA_SHORTCUT: + selection = + Directory.SHORTCUT_SUPPORT + + " IN (" + + Directory.SHORTCUT_SUPPORT_FULL + + ", " + + Directory.SHORTCUT_SUPPORT_DATA_ITEMS_ONLY + + ")"; + break; + + default: + throw new RuntimeException("Unsupported directory search mode: " + mDirectorySearchMode); + } + Cursor cursor = null; + try { + cursor = + context + .getContentResolver() + .query( + DirectoryQuery.URI, + DirectoryQuery.PROJECTION, + selection, + null, + DirectoryQuery.ORDER_BY); + + if (cursor == null) { + return result; + } + + while (cursor.moveToNext()) { + long directoryId = cursor.getLong(DirectoryQuery.ID); + if (!mLocalInvisibleDirectoryEnabled && DirectoryCompat.isInvisibleDirectory(directoryId)) { + continue; + } + String directoryType = null; + + String packageName = cursor.getString(DirectoryQuery.PACKAGE_NAME); + int typeResourceId = cursor.getInt(DirectoryQuery.TYPE_RESOURCE_ID); + if (!TextUtils.isEmpty(packageName) && typeResourceId != 0) { + try { + directoryType = pm.getResourcesForApplication(packageName).getString(typeResourceId); + } catch (Exception e) { + Log.e(TAG, "Cannot obtain directory type from package: " + packageName); + } + } + String displayName = cursor.getString(DirectoryQuery.DISPLAY_NAME); + int photoSupport = cursor.getInt(DirectoryQuery.PHOTO_SUPPORT); + result.addRow(new Object[] {directoryId, directoryType, displayName, photoSupport}); + } + } catch (RuntimeException e) { + Log.w(TAG, "Runtime Exception when querying directory"); + } finally { + if (cursor != null) { + cursor.close(); + } + } + + return result; + } + + private Cursor getDefaultDirectories() { + if (mDefaultDirectoryList == null) { + mDefaultDirectoryList = new MatrixCursor(RESULT_PROJECTION); + mDefaultDirectoryList.addRow( + new Object[] {Directory.DEFAULT, getContext().getString(R.string.contactsList), null}); + mDefaultDirectoryList.addRow( + new Object[] { + Directory.LOCAL_INVISIBLE, + getContext().getString(R.string.local_invisible_directory), + null + }); + } + return mDefaultDirectoryList; + } + + @Override + protected void onReset() { + stopLoading(); + } + + private static final class DirectoryQuery { + + public static final Uri URI = DirectoryCompat.getContentUri(); + public static final String ORDER_BY = Directory._ID; + + public static final String[] PROJECTION = { + Directory._ID, + Directory.PACKAGE_NAME, + Directory.TYPE_RESOURCE_ID, + Directory.DISPLAY_NAME, + Directory.PHOTO_SUPPORT, + }; + + public static final int ID = 0; + public static final int PACKAGE_NAME = 1; + public static final int TYPE_RESOURCE_ID = 2; + public static final int DISPLAY_NAME = 3; + public static final int PHOTO_SUPPORT = 4; + } +} diff --git a/java/com/android/contacts/common/list/DirectoryPartition.java b/java/com/android/contacts/common/list/DirectoryPartition.java new file mode 100644 index 000000000..26b851041 --- /dev/null +++ b/java/com/android/contacts/common/list/DirectoryPartition.java @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.contacts.common.list; + +import android.provider.ContactsContract.Directory; +import com.android.common.widget.CompositeCursorAdapter; + +/** Model object for a {@link Directory} row. */ +public final class DirectoryPartition extends CompositeCursorAdapter.Partition { + + public static final int STATUS_NOT_LOADED = 0; + public static final int STATUS_LOADING = 1; + public static final int STATUS_LOADED = 2; + + public static final int RESULT_LIMIT_DEFAULT = -1; + + private long mDirectoryId; + private String mContentUri; + private String mDirectoryType; + private String mDisplayName; + private int mStatus; + private boolean mPriorityDirectory; + private boolean mPhotoSupported; + private int mResultLimit = RESULT_LIMIT_DEFAULT; + private boolean mDisplayNumber = true; + + private String mLabel; + + public DirectoryPartition(boolean showIfEmpty, boolean hasHeader) { + super(showIfEmpty, hasHeader); + } + + /** Directory ID, see {@link Directory}. */ + public long getDirectoryId() { + return mDirectoryId; + } + + public void setDirectoryId(long directoryId) { + this.mDirectoryId = directoryId; + } + + /** + * Directory type resolved from {@link Directory#PACKAGE_NAME} and {@link + * Directory#TYPE_RESOURCE_ID}; + */ + public String getDirectoryType() { + return mDirectoryType; + } + + public void setDirectoryType(String directoryType) { + this.mDirectoryType = directoryType; + } + + /** See {@link Directory#DISPLAY_NAME}. */ + public String getDisplayName() { + return mDisplayName; + } + + public void setDisplayName(String displayName) { + this.mDisplayName = displayName; + } + + public int getStatus() { + return mStatus; + } + + public void setStatus(int status) { + mStatus = status; + } + + public boolean isLoading() { + return mStatus == STATUS_NOT_LOADED || mStatus == STATUS_LOADING; + } + + /** Returns true if this directory should be loaded before non-priority directories. */ + public boolean isPriorityDirectory() { + return mPriorityDirectory; + } + + public void setPriorityDirectory(boolean priorityDirectory) { + mPriorityDirectory = priorityDirectory; + } + + /** Returns true if this directory supports photos. */ + public boolean isPhotoSupported() { + return mPhotoSupported; + } + + public void setPhotoSupported(boolean flag) { + this.mPhotoSupported = flag; + } + + /** + * Max number of results for this directory. Defaults to {@link #RESULT_LIMIT_DEFAULT} which + * implies using the adapter's {@link + * com.android.contacts.common.list.ContactListAdapter#getDirectoryResultLimit()} + */ + public int getResultLimit() { + return mResultLimit; + } + + public void setResultLimit(int resultLimit) { + mResultLimit = resultLimit; + } + + /** + * Used by extended directories to specify a custom content URI. Extended directories MUST have a + * content URI + */ + public String getContentUri() { + return mContentUri; + } + + public void setContentUri(String contentUri) { + mContentUri = contentUri; + } + + /** A label to display in the header next to the display name. */ + public String getLabel() { + return mLabel; + } + + public void setLabel(String label) { + mLabel = label; + } + + @Override + public String toString() { + return "DirectoryPartition{" + + "mDirectoryId=" + + mDirectoryId + + ", mContentUri='" + + mContentUri + + '\'' + + ", mDirectoryType='" + + mDirectoryType + + '\'' + + ", mDisplayName='" + + mDisplayName + + '\'' + + ", mStatus=" + + mStatus + + ", mPriorityDirectory=" + + mPriorityDirectory + + ", mPhotoSupported=" + + mPhotoSupported + + ", mResultLimit=" + + mResultLimit + + ", mLabel='" + + mLabel + + '\'' + + '}'; + } + + /** + * Whether or not to display the phone number in app that have that option - Dialer. If false, + * Phone Label should be used instead of Phone Number. + */ + public boolean isDisplayNumber() { + return mDisplayNumber; + } + + public void setDisplayNumber(boolean displayNumber) { + mDisplayNumber = displayNumber; + } +} diff --git a/java/com/android/contacts/common/list/IndexerListAdapter.java b/java/com/android/contacts/common/list/IndexerListAdapter.java new file mode 100644 index 000000000..2289f6e59 --- /dev/null +++ b/java/com/android/contacts/common/list/IndexerListAdapter.java @@ -0,0 +1,214 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.contacts.common.list; + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ListView; +import android.widget.SectionIndexer; + +/** A list adapter that supports section indexer and a pinned header. */ +public abstract class IndexerListAdapter extends PinnedHeaderListAdapter implements SectionIndexer { + + protected Context mContext; + private SectionIndexer mIndexer; + private int mIndexedPartition = 0; + private boolean mSectionHeaderDisplayEnabled; + private View mHeader; + private Placement mPlacementCache = new Placement(); + + /** Constructor. */ + public IndexerListAdapter(Context context) { + super(context); + mContext = context; + } + + /** + * Creates a section header view that will be pinned at the top of the list as the user scrolls. + */ + protected abstract View createPinnedSectionHeaderView(Context context, ViewGroup parent); + + /** Sets the title in the pinned header as the user scrolls. */ + protected abstract void setPinnedSectionTitle(View pinnedHeaderView, String title); + + public boolean isSectionHeaderDisplayEnabled() { + return mSectionHeaderDisplayEnabled; + } + + public void setSectionHeaderDisplayEnabled(boolean flag) { + this.mSectionHeaderDisplayEnabled = flag; + } + + public int getIndexedPartition() { + return mIndexedPartition; + } + + public void setIndexedPartition(int partition) { + this.mIndexedPartition = partition; + } + + public SectionIndexer getIndexer() { + return mIndexer; + } + + public void setIndexer(SectionIndexer indexer) { + mIndexer = indexer; + mPlacementCache.invalidate(); + } + + public Object[] getSections() { + if (mIndexer == null) { + return new String[] {" "}; + } else { + return mIndexer.getSections(); + } + } + + /** @return relative position of the section in the indexed partition */ + public int getPositionForSection(int sectionIndex) { + if (mIndexer == null) { + return -1; + } + + return mIndexer.getPositionForSection(sectionIndex); + } + + /** @param position relative position in the indexed partition */ + public int getSectionForPosition(int position) { + if (mIndexer == null) { + return -1; + } + + return mIndexer.getSectionForPosition(position); + } + + @Override + public int getPinnedHeaderCount() { + if (isSectionHeaderDisplayEnabled()) { + return super.getPinnedHeaderCount() + 1; + } else { + return super.getPinnedHeaderCount(); + } + } + + @Override + public View getPinnedHeaderView(int viewIndex, View convertView, ViewGroup parent) { + if (isSectionHeaderDisplayEnabled() && viewIndex == getPinnedHeaderCount() - 1) { + if (mHeader == null) { + mHeader = createPinnedSectionHeaderView(mContext, parent); + } + return mHeader; + } else { + return super.getPinnedHeaderView(viewIndex, convertView, parent); + } + } + + @Override + public void configurePinnedHeaders(PinnedHeaderListView listView) { + super.configurePinnedHeaders(listView); + + if (!isSectionHeaderDisplayEnabled()) { + return; + } + + int index = getPinnedHeaderCount() - 1; + if (mIndexer == null || getCount() == 0) { + listView.setHeaderInvisible(index, false); + } else { + int listPosition = listView.getPositionAt(listView.getTotalTopPinnedHeaderHeight()); + int position = listPosition - listView.getHeaderViewsCount(); + + int section = -1; + int partition = getPartitionForPosition(position); + if (partition == mIndexedPartition) { + int offset = getOffsetInPartition(position); + if (offset != -1) { + section = getSectionForPosition(offset); + } + } + + if (section == -1) { + listView.setHeaderInvisible(index, false); + } else { + View topChild = listView.getChildAt(listPosition); + if (topChild != null) { + // Match the pinned header's height to the height of the list item. + mHeader.setMinimumHeight(topChild.getMeasuredHeight()); + } + setPinnedSectionTitle(mHeader, (String) mIndexer.getSections()[section]); + + // Compute the item position where the current partition begins + int partitionStart = getPositionForPartition(mIndexedPartition); + if (hasHeader(mIndexedPartition)) { + partitionStart++; + } + + // Compute the item position where the next section begins + int nextSectionPosition = partitionStart + getPositionForSection(section + 1); + boolean isLastInSection = position == nextSectionPosition - 1; + listView.setFadingHeader(index, listPosition, isLastInSection); + } + } + } + + /** + * Computes the item's placement within its section and populates the {@code placement} object + * accordingly. Please note that the returned object is volatile and should be copied if the + * result needs to be used later. + */ + public Placement getItemPlacementInSection(int position) { + if (mPlacementCache.position == position) { + return mPlacementCache; + } + + mPlacementCache.position = position; + if (isSectionHeaderDisplayEnabled()) { + int section = getSectionForPosition(position); + if (section != -1 && getPositionForSection(section) == position) { + mPlacementCache.firstInSection = true; + mPlacementCache.sectionHeader = (String) getSections()[section]; + } else { + mPlacementCache.firstInSection = false; + mPlacementCache.sectionHeader = null; + } + + mPlacementCache.lastInSection = (getPositionForSection(section + 1) - 1 == position); + } else { + mPlacementCache.firstInSection = false; + mPlacementCache.lastInSection = false; + mPlacementCache.sectionHeader = null; + } + return mPlacementCache; + } + + /** + * An item view is displayed differently depending on whether it is placed at the beginning, + * middle or end of a section. It also needs to know the section header when it is at the + * beginning of a section. This object captures all this configuration. + */ + public static final class Placement { + + public boolean firstInSection; + public boolean lastInSection; + public String sectionHeader; + private int position = ListView.INVALID_POSITION; + + public void invalidate() { + position = ListView.INVALID_POSITION; + } + } +} diff --git a/java/com/android/contacts/common/list/OnPhoneNumberPickerActionListener.java b/java/com/android/contacts/common/list/OnPhoneNumberPickerActionListener.java new file mode 100644 index 000000000..89bd889e6 --- /dev/null +++ b/java/com/android/contacts/common/list/OnPhoneNumberPickerActionListener.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.contacts.common.list; + +import android.app.ActionBar; +import android.net.Uri; +import com.android.dialer.callintent.nano.CallSpecificAppData; + +/** Action callbacks that can be sent by a phone number picker. */ +public interface OnPhoneNumberPickerActionListener { + + int CALL_INITIATION_UNKNOWN = 0; + + /** Returns the selected phone number uri to the requester. */ + void onPickDataUri(Uri dataUri, boolean isVideoCall, CallSpecificAppData callSpecificAppData); + + /** + * Returns the specified phone number to the requester. May call the specified phone number, + * either as an audio or video call. + */ + void onPickPhoneNumber( + String phoneNumber, boolean isVideoCall, CallSpecificAppData callSpecificAppData); + + /** Called when home menu in {@link ActionBar} is clicked by the user. */ + void onHomeInActionBarSelected(); +} diff --git a/java/com/android/contacts/common/list/PhoneNumberListAdapter.java b/java/com/android/contacts/common/list/PhoneNumberListAdapter.java new file mode 100644 index 000000000..c7b24229f --- /dev/null +++ b/java/com/android/contacts/common/list/PhoneNumberListAdapter.java @@ -0,0 +1,583 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.list; + +import android.content.Context; +import android.content.CursorLoader; +import android.database.Cursor; +import android.net.Uri; +import android.net.Uri.Builder; +import android.provider.ContactsContract; +import android.provider.ContactsContract.CommonDataKinds.Callable; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.CommonDataKinds.SipAddress; +import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.Directory; +import android.text.TextUtils; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest; +import com.android.contacts.common.ContactsUtils; +import com.android.contacts.common.GeoUtil; +import com.android.contacts.common.R; +import com.android.contacts.common.compat.CallableCompat; +import com.android.contacts.common.compat.DirectoryCompat; +import com.android.contacts.common.compat.PhoneCompat; +import com.android.contacts.common.extensions.PhoneDirectoryExtenderAccessor; +import com.android.contacts.common.preference.ContactsPreferences; +import com.android.contacts.common.util.Constants; +import com.android.dialer.compat.CompatUtils; +import com.android.dialer.util.CallUtil; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * A cursor adapter for the {@link Phone#CONTENT_ITEM_TYPE} and {@link + * SipAddress#CONTENT_ITEM_TYPE}. + * + *

By default this adapter just handles phone numbers. When {@link #setUseCallableUri(boolean)} + * is called with "true", this adapter starts handling SIP addresses too, by using {@link Callable} + * API instead of {@link Phone}. + */ +public class PhoneNumberListAdapter extends ContactEntryListAdapter { + + private static final String TAG = PhoneNumberListAdapter.class.getSimpleName(); + private static final String IGNORE_NUMBER_TOO_LONG_CLAUSE = "length(" + Phone.NUMBER + ") < 1000"; + // A list of extended directories to add to the directories from the database + private final List mExtendedDirectories; + private final CharSequence mUnknownNameText; + // Extended directories will have ID's that are higher than any of the id's from the database, + // so that we can identify them and set them up properly. If no extended directories + // exist, this will be Long.MAX_VALUE + private long mFirstExtendedDirectoryId = Long.MAX_VALUE; + private ContactListItemView.PhotoPosition mPhotoPosition; + private boolean mUseCallableUri; + private Listener mListener; + private boolean mIsVideoEnabled; + private boolean mIsPresenceEnabled; + + public PhoneNumberListAdapter(Context context) { + super(context); + setDefaultFilterHeaderText(R.string.list_filter_phones); + mUnknownNameText = context.getText(android.R.string.unknownName); + + mExtendedDirectories = + PhoneDirectoryExtenderAccessor.get(mContext).getExtendedDirectories(mContext); + + int videoCapabilities = CallUtil.getVideoCallingAvailability(context); + mIsVideoEnabled = (videoCapabilities & CallUtil.VIDEO_CALLING_ENABLED) != 0; + mIsPresenceEnabled = (videoCapabilities & CallUtil.VIDEO_CALLING_PRESENCE) != 0; + } + + @Override + public void configureLoader(CursorLoader loader, long directoryId) { + String query = getQueryString(); + if (query == null) { + query = ""; + } + if (isExtendedDirectory(directoryId)) { + final DirectoryPartition directory = getExtendedDirectoryFromId(directoryId); + final String contentUri = directory.getContentUri(); + if (contentUri == null) { + throw new IllegalStateException("Extended directory must have a content URL: " + directory); + } + final Builder builder = Uri.parse(contentUri).buildUpon(); + builder.appendPath(query); + builder.appendQueryParameter( + ContactsContract.LIMIT_PARAM_KEY, String.valueOf(getDirectoryResultLimit(directory))); + loader.setUri(builder.build()); + loader.setProjection(PhoneQuery.PROJECTION_PRIMARY); + } else { + final boolean isRemoteDirectoryQuery = DirectoryCompat.isRemoteDirectoryId(directoryId); + final Builder builder; + if (isSearchMode()) { + final Uri baseUri; + if (isRemoteDirectoryQuery) { + baseUri = PhoneCompat.getContentFilterUri(); + } else if (mUseCallableUri) { + baseUri = CallableCompat.getContentFilterUri(); + } else { + baseUri = PhoneCompat.getContentFilterUri(); + } + builder = baseUri.buildUpon(); + builder.appendPath(query); // Builder will encode the query + builder.appendQueryParameter( + ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId)); + if (isRemoteDirectoryQuery) { + builder.appendQueryParameter( + ContactsContract.LIMIT_PARAM_KEY, + String.valueOf(getDirectoryResultLimit(getDirectoryById(directoryId)))); + } + } else { + Uri baseUri = mUseCallableUri ? Callable.CONTENT_URI : Phone.CONTENT_URI; + builder = + baseUri + .buildUpon() + .appendQueryParameter( + ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT)); + if (isSectionHeaderDisplayEnabled()) { + builder.appendQueryParameter(Phone.EXTRA_ADDRESS_BOOK_INDEX, "true"); + } + applyFilter(loader, builder, directoryId, getFilter()); + } + + // Ignore invalid phone numbers that are too long. These can potentially cause freezes + // in the UI and there is no reason to display them. + final String prevSelection = loader.getSelection(); + final String newSelection; + if (!TextUtils.isEmpty(prevSelection)) { + newSelection = prevSelection + " AND " + IGNORE_NUMBER_TOO_LONG_CLAUSE; + } else { + newSelection = IGNORE_NUMBER_TOO_LONG_CLAUSE; + } + loader.setSelection(newSelection); + + // Remove duplicates when it is possible. + builder.appendQueryParameter(ContactsContract.REMOVE_DUPLICATE_ENTRIES, "true"); + loader.setUri(builder.build()); + + // TODO a projection that includes the search snippet + if (getContactNameDisplayOrder() == ContactsPreferences.DISPLAY_ORDER_PRIMARY) { + loader.setProjection(PhoneQuery.PROJECTION_PRIMARY); + } else { + loader.setProjection(PhoneQuery.PROJECTION_ALTERNATIVE); + } + + if (getSortOrder() == ContactsPreferences.SORT_ORDER_PRIMARY) { + loader.setSortOrder(Phone.SORT_KEY_PRIMARY); + } else { + loader.setSortOrder(Phone.SORT_KEY_ALTERNATIVE); + } + } + } + + protected boolean isExtendedDirectory(long directoryId) { + return directoryId >= mFirstExtendedDirectoryId; + } + + private DirectoryPartition getExtendedDirectoryFromId(long directoryId) { + final int directoryIndex = (int) (directoryId - mFirstExtendedDirectoryId); + return mExtendedDirectories.get(directoryIndex); + } + + /** + * Configure {@code loader} and {@code uriBuilder} according to {@code directoryId} and {@code + * filter}. + */ + private void applyFilter( + CursorLoader loader, Uri.Builder uriBuilder, long directoryId, ContactListFilter filter) { + if (filter == null || directoryId != Directory.DEFAULT) { + return; + } + + final StringBuilder selection = new StringBuilder(); + final List selectionArgs = new ArrayList(); + + switch (filter.filterType) { + case ContactListFilter.FILTER_TYPE_CUSTOM: + { + selection.append(Contacts.IN_VISIBLE_GROUP + "=1"); + selection.append(" AND " + Contacts.HAS_PHONE_NUMBER + "=1"); + break; + } + case ContactListFilter.FILTER_TYPE_ACCOUNT: + { + filter.addAccountQueryParameterToUrl(uriBuilder); + break; + } + case ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS: + case ContactListFilter.FILTER_TYPE_DEFAULT: + break; // No selection needed. + case ContactListFilter.FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY: + break; // This adapter is always "phone only", so no selection needed either. + default: + Log.w( + TAG, + "Unsupported filter type came " + + "(type: " + + filter.filterType + + ", toString: " + + filter + + ")" + + " showing all contacts."); + // No selection. + break; + } + loader.setSelection(selection.toString()); + loader.setSelectionArgs(selectionArgs.toArray(new String[0])); + } + + public String getPhoneNumber(int position) { + final Cursor item = (Cursor) getItem(position); + return item != null ? item.getString(PhoneQuery.PHONE_NUMBER) : null; + } + + /** + * Retrieves the lookup key for the given cursor position. + * + * @param position The cursor position. + * @return The lookup key. + */ + public String getLookupKey(int position) { + final Cursor item = (Cursor) getItem(position); + return item != null ? item.getString(PhoneQuery.LOOKUP_KEY) : null; + } + + @Override + protected ContactListItemView newView( + Context context, int partition, Cursor cursor, int position, ViewGroup parent) { + ContactListItemView view = super.newView(context, partition, cursor, position, parent); + view.setUnknownNameText(mUnknownNameText); + view.setQuickContactEnabled(isQuickContactEnabled()); + view.setPhotoPosition(mPhotoPosition); + return view; + } + + protected void setHighlight(ContactListItemView view, Cursor cursor) { + view.setHighlightedPrefix(isSearchMode() ? getUpperCaseQueryString() : null); + } + + @Override + protected void bindView(View itemView, int partition, Cursor cursor, int position) { + super.bindView(itemView, partition, cursor, position); + ContactListItemView view = (ContactListItemView) itemView; + + setHighlight(view, cursor); + + // Look at elements before and after this position, checking if contact IDs are same. + // If they have one same contact ID, it means they can be grouped. + // + // In one group, only the first entry will show its photo and its name, and the other + // entries in the group show just their data (e.g. phone number, email address). + cursor.moveToPosition(position); + boolean isFirstEntry = true; + final long currentContactId = cursor.getLong(PhoneQuery.CONTACT_ID); + if (cursor.moveToPrevious() && !cursor.isBeforeFirst()) { + final long previousContactId = cursor.getLong(PhoneQuery.CONTACT_ID); + if (currentContactId == previousContactId) { + isFirstEntry = false; + } + } + cursor.moveToPosition(position); + + bindViewId(view, cursor, PhoneQuery.PHONE_ID); + + bindSectionHeaderAndDivider(view, position); + if (isFirstEntry) { + bindName(view, cursor); + if (isQuickContactEnabled()) { + bindQuickContact( + view, + partition, + cursor, + PhoneQuery.PHOTO_ID, + PhoneQuery.PHOTO_URI, + PhoneQuery.CONTACT_ID, + PhoneQuery.LOOKUP_KEY, + PhoneQuery.DISPLAY_NAME); + } else { + if (getDisplayPhotos()) { + bindPhoto(view, partition, cursor); + } + } + } else { + unbindName(view); + + view.removePhotoView(true, false); + } + + final DirectoryPartition directory = (DirectoryPartition) getPartition(partition); + + // If the first partition does not have a header, then all subsequent partitions' + // getPositionForPartition returns an index off by 1. + int partitionOffset = 0; + if (partition > 0 && !getPartition(0).getHasHeader()) { + partitionOffset = 1; + } + position += getPositionForPartition(partition) + partitionOffset; + + bindPhoneNumber(view, cursor, directory.isDisplayNumber(), position); + } + + protected void bindPhoneNumber( + ContactListItemView view, Cursor cursor, boolean displayNumber, int position) { + CharSequence label = null; + if (displayNumber && !cursor.isNull(PhoneQuery.PHONE_TYPE)) { + final int type = cursor.getInt(PhoneQuery.PHONE_TYPE); + final String customLabel = cursor.getString(PhoneQuery.PHONE_LABEL); + + // TODO cache + label = Phone.getTypeLabel(getContext().getResources(), type, customLabel); + } + view.setLabel(label); + final String text; + if (displayNumber) { + text = cursor.getString(PhoneQuery.PHONE_NUMBER); + } else { + // Display phone label. If that's null, display geocoded location for the number + final String phoneLabel = cursor.getString(PhoneQuery.PHONE_LABEL); + if (phoneLabel != null) { + text = phoneLabel; + } else { + final String phoneNumber = cursor.getString(PhoneQuery.PHONE_NUMBER); + text = GeoUtil.getGeocodedLocationFor(mContext, phoneNumber); + } + } + view.setPhoneNumber(text); + + if (CompatUtils.isVideoCompatible()) { + // Determine if carrier presence indicates the number supports video calling. + int carrierPresence = cursor.getInt(PhoneQuery.CARRIER_PRESENCE); + boolean isPresent = (carrierPresence & Phone.CARRIER_PRESENCE_VT_CAPABLE) != 0; + + boolean isVideoIconShown = mIsVideoEnabled && (!mIsPresenceEnabled || isPresent); + view.setShowVideoCallIcon(isVideoIconShown, mListener, position); + } + } + + protected void bindSectionHeaderAndDivider(final ContactListItemView view, int position) { + if (isSectionHeaderDisplayEnabled()) { + Placement placement = getItemPlacementInSection(position); + view.setSectionHeader(placement.firstInSection ? placement.sectionHeader : null); + } else { + view.setSectionHeader(null); + } + } + + protected void bindName(final ContactListItemView view, Cursor cursor) { + view.showDisplayName(cursor, PhoneQuery.DISPLAY_NAME); + // Note: we don't show phonetic names any more (see issue 5265330) + } + + protected void unbindName(final ContactListItemView view) { + view.hideDisplayName(); + } + + @Override + protected void bindWorkProfileIcon(final ContactListItemView view, int partition) { + final DirectoryPartition directory = (DirectoryPartition) getPartition(partition); + final long directoryId = directory.getDirectoryId(); + final long userType = ContactsUtils.determineUserType(directoryId, null); + // Work directory must not be a extended directory. An extended directory is custom + // directory in the app, but not a directory provided by framework. So it can't be + // USER_TYPE_WORK. + view.setWorkProfileIconEnabled( + !isExtendedDirectory(directoryId) && userType == ContactsUtils.USER_TYPE_WORK); + } + + protected void bindPhoto(final ContactListItemView view, int partitionIndex, Cursor cursor) { + if (!isPhotoSupported(partitionIndex)) { + view.removePhotoView(); + return; + } + + long photoId = 0; + if (!cursor.isNull(PhoneQuery.PHOTO_ID)) { + photoId = cursor.getLong(PhoneQuery.PHOTO_ID); + } + + if (photoId != 0) { + getPhotoLoader() + .loadThumbnail(view.getPhotoView(), photoId, false, getCircularPhotos(), null); + } else { + final String photoUriString = cursor.getString(PhoneQuery.PHOTO_URI); + final Uri photoUri = photoUriString == null ? null : Uri.parse(photoUriString); + + DefaultImageRequest request = null; + if (photoUri == null) { + final String displayName = cursor.getString(PhoneQuery.DISPLAY_NAME); + final String lookupKey = cursor.getString(PhoneQuery.LOOKUP_KEY); + request = new DefaultImageRequest(displayName, lookupKey, getCircularPhotos()); + } + getPhotoLoader() + .loadDirectoryPhoto(view.getPhotoView(), photoUri, false, getCircularPhotos(), request); + } + } + + public ContactListItemView.PhotoPosition getPhotoPosition() { + return mPhotoPosition; + } + + public void setPhotoPosition(ContactListItemView.PhotoPosition photoPosition) { + mPhotoPosition = photoPosition; + } + + public void setUseCallableUri(boolean useCallableUri) { + mUseCallableUri = useCallableUri; + } + + /** + * Override base implementation to inject extended directories between local & remote directories. + * This is done in the following steps: 1. Call base implementation to add directories from the + * cursor. 2. Iterate all base directories and establish the following information: a. The highest + * directory id so that we can assign unused id's to the extended directories. b. The index of the + * last non-remote directory. This is where we will insert extended directories. 3. Iterate the + * extended directories and for each one, assign an ID and insert it in the proper location. + */ + @Override + public void changeDirectories(Cursor cursor) { + super.changeDirectories(cursor); + if (getDirectorySearchMode() == DirectoryListLoader.SEARCH_MODE_NONE) { + return; + } + final int numExtendedDirectories = mExtendedDirectories.size(); + if (getPartitionCount() == cursor.getCount() + numExtendedDirectories) { + // already added all directories; + return; + } + // + mFirstExtendedDirectoryId = Long.MAX_VALUE; + if (numExtendedDirectories > 0) { + // The Directory.LOCAL_INVISIBLE is not in the cursor but we can't reuse it's + // "special" ID. + long maxId = Directory.LOCAL_INVISIBLE; + int insertIndex = 0; + for (int i = 0, n = getPartitionCount(); i < n; i++) { + final DirectoryPartition partition = (DirectoryPartition) getPartition(i); + final long id = partition.getDirectoryId(); + if (id > maxId) { + maxId = id; + } + if (!DirectoryCompat.isRemoteDirectoryId(id)) { + // assuming remote directories come after local, we will end up with the index + // where we should insert extended directories. This also works if there are no + // remote directories at all. + insertIndex = i + 1; + } + } + // Extended directories ID's cannot collide with base directories + mFirstExtendedDirectoryId = maxId + 1; + for (int i = 0; i < numExtendedDirectories; i++) { + final long id = mFirstExtendedDirectoryId + i; + final DirectoryPartition directory = mExtendedDirectories.get(i); + if (getPartitionByDirectoryId(id) == -1) { + addPartition(insertIndex, directory); + directory.setDirectoryId(id); + } + } + } + } + + @Override + protected Uri getContactUri( + int partitionIndex, Cursor cursor, int contactIdColumn, int lookUpKeyColumn) { + final DirectoryPartition directory = (DirectoryPartition) getPartition(partitionIndex); + final long directoryId = directory.getDirectoryId(); + if (!isExtendedDirectory(directoryId)) { + return super.getContactUri(partitionIndex, cursor, contactIdColumn, lookUpKeyColumn); + } + return Contacts.CONTENT_LOOKUP_URI + .buildUpon() + .appendPath(Constants.LOOKUP_URI_ENCODED) + .appendQueryParameter(Directory.DISPLAY_NAME, directory.getLabel()) + .appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId)) + .encodedFragment(cursor.getString(lookUpKeyColumn)) + .build(); + } + + public Listener getListener() { + return mListener; + } + + public void setListener(Listener listener) { + mListener = listener; + } + + public interface Listener { + + void onVideoCallIconClicked(int position); + } + + public static class PhoneQuery { + + /** + * Optional key used as part of a JSON lookup key to specify an analytics category associated + * with the row. + */ + public static final String ANALYTICS_CATEGORY = "analytics_category"; + + /** + * Optional key used as part of a JSON lookup key to specify an analytics action associated with + * the row. + */ + public static final String ANALYTICS_ACTION = "analytics_action"; + + /** + * Optional key used as part of a JSON lookup key to specify an analytics value associated with + * the row. + */ + public static final String ANALYTICS_VALUE = "analytics_value"; + + public static final String[] PROJECTION_PRIMARY_INTERNAL = + new String[] { + Phone._ID, // 0 + Phone.TYPE, // 1 + Phone.LABEL, // 2 + Phone.NUMBER, // 3 + Phone.CONTACT_ID, // 4 + Phone.LOOKUP_KEY, // 5 + Phone.PHOTO_ID, // 6 + Phone.DISPLAY_NAME_PRIMARY, // 7 + Phone.PHOTO_THUMBNAIL_URI, // 8 + }; + + public static final String[] PROJECTION_PRIMARY; + public static final String[] PROJECTION_ALTERNATIVE_INTERNAL = + new String[] { + Phone._ID, // 0 + Phone.TYPE, // 1 + Phone.LABEL, // 2 + Phone.NUMBER, // 3 + Phone.CONTACT_ID, // 4 + Phone.LOOKUP_KEY, // 5 + Phone.PHOTO_ID, // 6 + Phone.DISPLAY_NAME_ALTERNATIVE, // 7 + Phone.PHOTO_THUMBNAIL_URI, // 8 + }; + public static final String[] PROJECTION_ALTERNATIVE; + public static final int PHONE_ID = 0; + public static final int PHONE_TYPE = 1; + public static final int PHONE_LABEL = 2; + public static final int PHONE_NUMBER = 3; + public static final int CONTACT_ID = 4; + public static final int LOOKUP_KEY = 5; + public static final int PHOTO_ID = 6; + public static final int DISPLAY_NAME = 7; + public static final int PHOTO_URI = 8; + public static final int CARRIER_PRESENCE = 9; + + static { + final List projectionList = + new ArrayList<>(Arrays.asList(PROJECTION_PRIMARY_INTERNAL)); + if (CompatUtils.isMarshmallowCompatible()) { + projectionList.add(Phone.CARRIER_PRESENCE); // 9 + } + PROJECTION_PRIMARY = projectionList.toArray(new String[projectionList.size()]); + } + + static { + final List projectionList = + new ArrayList<>(Arrays.asList(PROJECTION_ALTERNATIVE_INTERNAL)); + if (CompatUtils.isMarshmallowCompatible()) { + projectionList.add(Phone.CARRIER_PRESENCE); // 9 + } + PROJECTION_ALTERNATIVE = projectionList.toArray(new String[projectionList.size()]); + } + } +} diff --git a/java/com/android/contacts/common/list/PhoneNumberPickerFragment.java b/java/com/android/contacts/common/list/PhoneNumberPickerFragment.java new file mode 100644 index 000000000..4ae81529b --- /dev/null +++ b/java/com/android/contacts/common/list/PhoneNumberPickerFragment.java @@ -0,0 +1,402 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.contacts.common.list; + +import android.content.Loader; +import android.database.Cursor; +import android.os.Bundle; +import android.support.annotation.MainThread; +import android.support.annotation.Nullable; +import android.text.TextUtils; +import android.util.ArraySet; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import com.android.contacts.common.R; +import com.android.contacts.common.util.AccountFilterUtil; +import com.android.dialer.callintent.nano.CallSpecificAppData; +import com.android.dialer.common.Assert; +import com.android.dialer.common.LogUtil; +import com.android.dialer.logging.Logger; +import java.util.Set; +import org.json.JSONException; +import org.json.JSONObject; + +/** Fragment containing a phone number list for picking. */ +public class PhoneNumberPickerFragment extends ContactEntryListFragment + implements PhoneNumberListAdapter.Listener { + + private static final String KEY_FILTER = "filter"; + private OnPhoneNumberPickerActionListener mListener; + private ContactListFilter mFilter; + private View mAccountFilterHeader; + /** + * Lives as ListView's header and is shown when {@link #mAccountFilterHeader} is set to View.GONE. + */ + private View mPaddingView; + /** true if the loader has started at least once. */ + private boolean mLoaderStarted; + + private boolean mUseCallableUri; + + private ContactListItemView.PhotoPosition mPhotoPosition = + ContactListItemView.getDefaultPhotoPosition(false /* normal/non opposite */); + + private final Set mLoadFinishedListeners = + new ArraySet(); + + private CursorReranker mCursorReranker; + + public PhoneNumberPickerFragment() { + setQuickContactEnabled(false); + setPhotoLoaderEnabled(true); + setSectionHeaderDisplayEnabled(true); + setDirectorySearchMode(DirectoryListLoader.SEARCH_MODE_NONE); + + // Show nothing instead of letting caller Activity show something. + setHasOptionsMenu(true); + } + + /** + * Handles a click on the video call icon for a row in the list. + * + * @param position The position in the list where the click ocurred. + */ + @Override + public void onVideoCallIconClicked(int position) { + callNumber(position, true /* isVideoCall */); + } + + public void setDirectorySearchEnabled(boolean flag) { + setDirectorySearchMode( + flag ? DirectoryListLoader.SEARCH_MODE_DEFAULT : DirectoryListLoader.SEARCH_MODE_NONE); + } + + public void setOnPhoneNumberPickerActionListener(OnPhoneNumberPickerActionListener listener) { + this.mListener = listener; + } + + public OnPhoneNumberPickerActionListener getOnPhoneNumberPickerListener() { + return mListener; + } + + @Override + protected void onCreateView(LayoutInflater inflater, ViewGroup container) { + super.onCreateView(inflater, container); + + View paddingView = inflater.inflate(R.layout.contact_detail_list_padding, null, false); + mPaddingView = paddingView.findViewById(R.id.contact_detail_list_padding); + getListView().addHeaderView(paddingView); + + mAccountFilterHeader = getView().findViewById(R.id.account_filter_header_container); + updateFilterHeaderView(); + + setVisibleScrollbarEnabled(getVisibleScrollbarEnabled()); + } + + protected boolean getVisibleScrollbarEnabled() { + return true; + } + + @Override + protected void setSearchMode(boolean flag) { + super.setSearchMode(flag); + updateFilterHeaderView(); + } + + private void updateFilterHeaderView() { + final ContactListFilter filter = getFilter(); + if (mAccountFilterHeader == null || filter == null) { + return; + } + final boolean shouldShowHeader = + !isSearchMode() + && AccountFilterUtil.updateAccountFilterTitleForPhone( + mAccountFilterHeader, filter, false); + if (shouldShowHeader) { + mPaddingView.setVisibility(View.GONE); + mAccountFilterHeader.setVisibility(View.VISIBLE); + } else { + mPaddingView.setVisibility(View.VISIBLE); + mAccountFilterHeader.setVisibility(View.GONE); + } + } + + @Override + public void restoreSavedState(Bundle savedState) { + super.restoreSavedState(savedState); + + if (savedState == null) { + return; + } + + mFilter = savedState.getParcelable(KEY_FILTER); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putParcelable(KEY_FILTER, mFilter); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + final int itemId = item.getItemId(); + if (itemId == android.R.id.home) { // See ActionBar#setDisplayHomeAsUpEnabled() + if (mListener != null) { + mListener.onHomeInActionBarSelected(); + } + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + protected void onItemClick(int position, long id) { + callNumber(position, false /* isVideoCall */); + } + + /** + * Initiates a call to the number at the specified position. + * + * @param position The position. + * @param isVideoCall {@code true} if the call should be initiated as a video call, {@code false} + * otherwise. + */ + private void callNumber(int position, boolean isVideoCall) { + final String number = getPhoneNumber(position); + if (!TextUtils.isEmpty(number)) { + cacheContactInfo(position); + CallSpecificAppData callSpecificAppData = new CallSpecificAppData(); + callSpecificAppData.callInitiationType = getCallInitiationType(true /* isRemoteDirectory */); + callSpecificAppData.positionOfSelectedSearchResult = position; + callSpecificAppData.charactersInSearchString = + getQueryString() == null ? 0 : getQueryString().length(); + mListener.onPickPhoneNumber(number, isVideoCall, callSpecificAppData); + } else { + LogUtil.i( + "PhoneNumberPickerFragment.callNumber", + "item at %d was clicked before adapter is ready, ignoring", + position); + } + + // Get the lookup key and track any analytics + final String lookupKey = getLookupKey(position); + if (!TextUtils.isEmpty(lookupKey)) { + maybeTrackAnalytics(lookupKey); + } + } + + protected void cacheContactInfo(int position) { + // Not implemented. Hook for child classes + } + + protected String getPhoneNumber(int position) { + final PhoneNumberListAdapter adapter = (PhoneNumberListAdapter) getAdapter(); + return adapter.getPhoneNumber(position); + } + + protected String getLookupKey(int position) { + final PhoneNumberListAdapter adapter = (PhoneNumberListAdapter) getAdapter(); + return adapter.getLookupKey(position); + } + + @Override + protected void startLoading() { + mLoaderStarted = true; + super.startLoading(); + } + + @Override + @MainThread + public void onLoadFinished(Loader loader, Cursor data) { + Assert.isMainThread(); + // TODO: define and verify behavior for "Nearby places", corp directories, + // and dividers listed in UI between these categories + if (mCursorReranker != null + && data != null + && !data.isClosed() + && data.getCount() > 0 + && loader.getId() != -1) { // skip invalid directory ID of -1 + data = mCursorReranker.rerankCursor(data); + } + super.onLoadFinished(loader, data); + + // disable scroll bar if there is no data + setVisibleScrollbarEnabled(data != null && !data.isClosed() && data.getCount() > 0); + + if (data != null) { + notifyListeners(); + } + } + + /** Ranks cursor data rows and returns reference to new cursor object with reordered data. */ + public interface CursorReranker { + @MainThread + Cursor rerankCursor(Cursor data); + } + + @MainThread + public void setReranker(@Nullable CursorReranker reranker) { + Assert.isMainThread(); + mCursorReranker = reranker; + } + + /** Listener that is notified when cursor has finished loading data. */ + public interface OnLoadFinishedListener { + void onLoadFinished(); + } + + @MainThread + public void addOnLoadFinishedListener(OnLoadFinishedListener listener) { + Assert.isMainThread(); + mLoadFinishedListeners.add(listener); + } + + @MainThread + public void removeOnLoadFinishedListener(OnLoadFinishedListener listener) { + Assert.isMainThread(); + mLoadFinishedListeners.remove(listener); + } + + @MainThread + protected void notifyListeners() { + Assert.isMainThread(); + for (OnLoadFinishedListener listener : mLoadFinishedListeners) { + listener.onLoadFinished(); + } + } + + @MainThread + @Override + public void onDetach() { + Assert.isMainThread(); + mLoadFinishedListeners.clear(); + super.onDetach(); + } + + public void setUseCallableUri(boolean useCallableUri) { + mUseCallableUri = useCallableUri; + } + + public boolean usesCallableUri() { + return mUseCallableUri; + } + + @Override + protected ContactEntryListAdapter createListAdapter() { + PhoneNumberListAdapter adapter = new PhoneNumberListAdapter(getActivity()); + adapter.setDisplayPhotos(true); + adapter.setUseCallableUri(mUseCallableUri); + return adapter; + } + + @Override + protected void configureAdapter() { + super.configureAdapter(); + + final ContactEntryListAdapter adapter = getAdapter(); + if (adapter == null) { + return; + } + + if (!isSearchMode() && mFilter != null) { + adapter.setFilter(mFilter); + } + + setPhotoPosition(adapter); + } + + protected void setPhotoPosition(ContactEntryListAdapter adapter) { + ((PhoneNumberListAdapter) adapter).setPhotoPosition(mPhotoPosition); + } + + @Override + protected View inflateView(LayoutInflater inflater, ViewGroup container) { + return inflater.inflate(R.layout.contact_list_content, null); + } + + public ContactListFilter getFilter() { + return mFilter; + } + + public void setFilter(ContactListFilter filter) { + if ((mFilter == null && filter == null) || (mFilter != null && mFilter.equals(filter))) { + return; + } + + mFilter = filter; + if (mLoaderStarted) { + reloadData(); + } + updateFilterHeaderView(); + } + + public void setPhotoPosition(ContactListItemView.PhotoPosition photoPosition) { + mPhotoPosition = photoPosition; + + final PhoneNumberListAdapter adapter = (PhoneNumberListAdapter) getAdapter(); + if (adapter != null) { + adapter.setPhotoPosition(photoPosition); + } + } + + /** + * @param isRemoteDirectory {@code true} if the call was initiated using a contact/phone number + * not in the local contacts database + */ + protected int getCallInitiationType(boolean isRemoteDirectory) { + return OnPhoneNumberPickerActionListener.CALL_INITIATION_UNKNOWN; + } + + /** + * Where a lookup key contains analytic event information, logs the associated analytics event. + * + * @param lookupKey The lookup key JSON object. + */ + private void maybeTrackAnalytics(String lookupKey) { + try { + JSONObject json = new JSONObject(lookupKey); + + String analyticsCategory = + json.getString(PhoneNumberListAdapter.PhoneQuery.ANALYTICS_CATEGORY); + String analyticsAction = json.getString(PhoneNumberListAdapter.PhoneQuery.ANALYTICS_ACTION); + String analyticsValue = json.getString(PhoneNumberListAdapter.PhoneQuery.ANALYTICS_VALUE); + + if (TextUtils.isEmpty(analyticsCategory) + || TextUtils.isEmpty(analyticsAction) + || TextUtils.isEmpty(analyticsValue)) { + return; + } + + // Assume that the analytic value being tracked could be a float value, but just cast + // to a long so that the analytic server can handle it. + long value; + try { + float floatValue = Float.parseFloat(analyticsValue); + value = (long) floatValue; + } catch (NumberFormatException nfe) { + return; + } + + Logger.get(getActivity()) + .sendHitEventAnalytics(analyticsCategory, analyticsAction, "" /* label */, value); + } catch (JSONException e) { + // Not an error; just a lookup key that doesn't have the right information. + } + } +} diff --git a/java/com/android/contacts/common/list/PinnedHeaderListAdapter.java b/java/com/android/contacts/common/list/PinnedHeaderListAdapter.java new file mode 100644 index 000000000..0bdcef084 --- /dev/null +++ b/java/com/android/contacts/common/list/PinnedHeaderListAdapter.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.contacts.common.list; + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; +import com.android.common.widget.CompositeCursorAdapter; + +/** A subclass of {@link CompositeCursorAdapter} that manages pinned partition headers. */ +public abstract class PinnedHeaderListAdapter extends CompositeCursorAdapter + implements PinnedHeaderListView.PinnedHeaderAdapter { + + public static final int PARTITION_HEADER_TYPE = 0; + + private boolean mPinnedPartitionHeadersEnabled; + private boolean[] mHeaderVisibility; + + public PinnedHeaderListAdapter(Context context) { + super(context); + } + + public boolean getPinnedPartitionHeadersEnabled() { + return mPinnedPartitionHeadersEnabled; + } + + public void setPinnedPartitionHeadersEnabled(boolean flag) { + this.mPinnedPartitionHeadersEnabled = flag; + } + + @Override + public int getPinnedHeaderCount() { + if (mPinnedPartitionHeadersEnabled) { + return getPartitionCount(); + } else { + return 0; + } + } + + protected boolean isPinnedPartitionHeaderVisible(int partition) { + return getPinnedPartitionHeadersEnabled() + && hasHeader(partition) + && !isPartitionEmpty(partition); + } + + /** The default implementation creates the same type of view as a normal partition header. */ + @Override + public View getPinnedHeaderView(int partition, View convertView, ViewGroup parent) { + if (hasHeader(partition)) { + View view = null; + if (convertView != null) { + Integer headerType = (Integer) convertView.getTag(); + if (headerType != null && headerType == PARTITION_HEADER_TYPE) { + view = convertView; + } + } + if (view == null) { + view = newHeaderView(getContext(), partition, null, parent); + view.setTag(PARTITION_HEADER_TYPE); + view.setFocusable(false); + view.setEnabled(false); + } + bindHeaderView(view, partition, getCursor(partition)); + view.setLayoutDirection(parent.getLayoutDirection()); + return view; + } else { + return null; + } + } + + @Override + public void configurePinnedHeaders(PinnedHeaderListView listView) { + if (!getPinnedPartitionHeadersEnabled()) { + return; + } + + int size = getPartitionCount(); + + // Cache visibility bits, because we will need them several times later on + if (mHeaderVisibility == null || mHeaderVisibility.length != size) { + mHeaderVisibility = new boolean[size]; + } + for (int i = 0; i < size; i++) { + boolean visible = isPinnedPartitionHeaderVisible(i); + mHeaderVisibility[i] = visible; + if (!visible) { + listView.setHeaderInvisible(i, true); + } + } + + int headerViewsCount = listView.getHeaderViewsCount(); + + // Starting at the top, find and pin headers for partitions preceding the visible one(s) + int maxTopHeader = -1; + int topHeaderHeight = 0; + for (int i = 0; i < size; i++) { + if (mHeaderVisibility[i]) { + int position = listView.getPositionAt(topHeaderHeight) - headerViewsCount; + int partition = getPartitionForPosition(position); + if (i > partition) { + break; + } + + listView.setHeaderPinnedAtTop(i, topHeaderHeight, false); + topHeaderHeight += listView.getPinnedHeaderHeight(i); + maxTopHeader = i; + } + } + + // Starting at the bottom, find and pin headers for partitions following the visible one(s) + int maxBottomHeader = size; + int bottomHeaderHeight = 0; + int listHeight = listView.getHeight(); + for (int i = size; --i > maxTopHeader; ) { + if (mHeaderVisibility[i]) { + int position = listView.getPositionAt(listHeight - bottomHeaderHeight) - headerViewsCount; + if (position < 0) { + break; + } + + int partition = getPartitionForPosition(position - 1); + if (partition == -1 || i <= partition) { + break; + } + + int height = listView.getPinnedHeaderHeight(i); + bottomHeaderHeight += height; + + listView.setHeaderPinnedAtBottom(i, listHeight - bottomHeaderHeight, false); + maxBottomHeader = i; + } + } + + // Headers in between the top-pinned and bottom-pinned should be hidden + for (int i = maxTopHeader + 1; i < maxBottomHeader; i++) { + if (mHeaderVisibility[i]) { + listView.setHeaderInvisible(i, isPartitionEmpty(i)); + } + } + } + + @Override + public int getScrollPositionForHeader(int viewIndex) { + return getPositionForPartition(viewIndex); + } +} diff --git a/java/com/android/contacts/common/list/PinnedHeaderListView.java b/java/com/android/contacts/common/list/PinnedHeaderListView.java new file mode 100644 index 000000000..33c68b68c --- /dev/null +++ b/java/com/android/contacts/common/list/PinnedHeaderListView.java @@ -0,0 +1,563 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.list; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView; +import android.widget.AbsListView.OnScrollListener; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemSelectedListener; +import android.widget.ListAdapter; +import com.android.dialer.util.ViewUtil; + +/** + * A ListView that maintains a header pinned at the top of the list. The pinned header can be pushed + * up and dissolved as needed. + */ +public class PinnedHeaderListView extends AutoScrollListView + implements OnScrollListener, OnItemSelectedListener { + + private static final int MAX_ALPHA = 255; + private static final int TOP = 0; + private static final int BOTTOM = 1; + private static final int FADING = 2; + private static final int DEFAULT_ANIMATION_DURATION = 20; + private static final int DEFAULT_SMOOTH_SCROLL_DURATION = 100; + private PinnedHeaderAdapter mAdapter; + private int mSize; + private PinnedHeader[] mHeaders; + private RectF mBounds = new RectF(); + private OnScrollListener mOnScrollListener; + private OnItemSelectedListener mOnItemSelectedListener; + private int mScrollState; + private boolean mScrollToSectionOnHeaderTouch = false; + private boolean mHeaderTouched = false; + private int mAnimationDuration = DEFAULT_ANIMATION_DURATION; + private boolean mAnimating; + private long mAnimationTargetTime; + private int mHeaderPaddingStart; + private int mHeaderWidth; + + public PinnedHeaderListView(Context context) { + this(context, null, android.R.attr.listViewStyle); + } + + public PinnedHeaderListView(Context context, AttributeSet attrs) { + this(context, attrs, android.R.attr.listViewStyle); + } + + public PinnedHeaderListView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + super.setOnScrollListener(this); + super.setOnItemSelectedListener(this); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + mHeaderPaddingStart = getPaddingStart(); + mHeaderWidth = r - l - mHeaderPaddingStart - getPaddingEnd(); + } + + @Override + public void setAdapter(ListAdapter adapter) { + mAdapter = (PinnedHeaderAdapter) adapter; + super.setAdapter(adapter); + } + + @Override + public void setOnScrollListener(OnScrollListener onScrollListener) { + mOnScrollListener = onScrollListener; + super.setOnScrollListener(this); + } + + @Override + public void setOnItemSelectedListener(OnItemSelectedListener listener) { + mOnItemSelectedListener = listener; + super.setOnItemSelectedListener(this); + } + + public void setScrollToSectionOnHeaderTouch(boolean value) { + mScrollToSectionOnHeaderTouch = value; + } + + @Override + public void onScroll( + AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { + if (mAdapter != null) { + int count = mAdapter.getPinnedHeaderCount(); + if (count != mSize) { + mSize = count; + if (mHeaders == null) { + mHeaders = new PinnedHeader[mSize]; + } else if (mHeaders.length < mSize) { + PinnedHeader[] headers = mHeaders; + mHeaders = new PinnedHeader[mSize]; + System.arraycopy(headers, 0, mHeaders, 0, headers.length); + } + } + + for (int i = 0; i < mSize; i++) { + if (mHeaders[i] == null) { + mHeaders[i] = new PinnedHeader(); + } + mHeaders[i].view = mAdapter.getPinnedHeaderView(i, mHeaders[i].view, this); + } + + mAnimationTargetTime = System.currentTimeMillis() + mAnimationDuration; + mAdapter.configurePinnedHeaders(this); + invalidateIfAnimating(); + } + if (mOnScrollListener != null) { + mOnScrollListener.onScroll(this, firstVisibleItem, visibleItemCount, totalItemCount); + } + } + + @Override + protected float getTopFadingEdgeStrength() { + // Disable vertical fading at the top when the pinned header is present + return mSize > 0 ? 0 : super.getTopFadingEdgeStrength(); + } + + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + mScrollState = scrollState; + if (mOnScrollListener != null) { + mOnScrollListener.onScrollStateChanged(this, scrollState); + } + } + + /** + * Ensures that the selected item is positioned below the top-pinned headers and above the + * bottom-pinned ones. + */ + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + int height = getHeight(); + + int windowTop = 0; + int windowBottom = height; + + for (int i = 0; i < mSize; i++) { + PinnedHeader header = mHeaders[i]; + if (header.visible) { + if (header.state == TOP) { + windowTop = header.y + header.height; + } else if (header.state == BOTTOM) { + windowBottom = header.y; + break; + } + } + } + + View selectedView = getSelectedView(); + if (selectedView != null) { + if (selectedView.getTop() < windowTop) { + setSelectionFromTop(position, windowTop); + } else if (selectedView.getBottom() > windowBottom) { + setSelectionFromTop(position, windowBottom - selectedView.getHeight()); + } + } + + if (mOnItemSelectedListener != null) { + mOnItemSelectedListener.onItemSelected(parent, view, position, id); + } + } + + @Override + public void onNothingSelected(AdapterView parent) { + if (mOnItemSelectedListener != null) { + mOnItemSelectedListener.onNothingSelected(parent); + } + } + + public int getPinnedHeaderHeight(int viewIndex) { + ensurePinnedHeaderLayout(viewIndex); + return mHeaders[viewIndex].view.getHeight(); + } + + /** + * Set header to be pinned at the top. + * + * @param viewIndex index of the header view + * @param y is position of the header in pixels. + * @param animate true if the transition to the new coordinate should be animated + */ + public void setHeaderPinnedAtTop(int viewIndex, int y, boolean animate) { + ensurePinnedHeaderLayout(viewIndex); + PinnedHeader header = mHeaders[viewIndex]; + header.visible = true; + header.y = y; + header.state = TOP; + + // TODO perhaps we should animate at the top as well + header.animating = false; + } + + /** + * Set header to be pinned at the bottom. + * + * @param viewIndex index of the header view + * @param y is position of the header in pixels. + * @param animate true if the transition to the new coordinate should be animated + */ + public void setHeaderPinnedAtBottom(int viewIndex, int y, boolean animate) { + ensurePinnedHeaderLayout(viewIndex); + PinnedHeader header = mHeaders[viewIndex]; + header.state = BOTTOM; + if (header.animating) { + header.targetTime = mAnimationTargetTime; + header.sourceY = header.y; + header.targetY = y; + } else if (animate && (header.y != y || !header.visible)) { + if (header.visible) { + header.sourceY = header.y; + } else { + header.visible = true; + header.sourceY = y + header.height; + } + header.animating = true; + header.targetVisible = true; + header.targetTime = mAnimationTargetTime; + header.targetY = y; + } else { + header.visible = true; + header.y = y; + } + } + + /** + * Set header to be pinned at the top of the first visible item. + * + * @param viewIndex index of the header view + * @param position is position of the header in pixels. + */ + public void setFadingHeader(int viewIndex, int position, boolean fade) { + ensurePinnedHeaderLayout(viewIndex); + + View child = getChildAt(position - getFirstVisiblePosition()); + if (child == null) { + return; + } + + PinnedHeader header = mHeaders[viewIndex]; + header.visible = true; + header.state = FADING; + header.alpha = MAX_ALPHA; + header.animating = false; + + int top = getTotalTopPinnedHeaderHeight(); + header.y = top; + if (fade) { + int bottom = child.getBottom() - top; + int headerHeight = header.height; + if (bottom < headerHeight) { + int portion = bottom - headerHeight; + header.alpha = MAX_ALPHA * (headerHeight + portion) / headerHeight; + header.y = top + portion; + } + } + } + + /** + * Makes header invisible. + * + * @param viewIndex index of the header view + * @param animate true if the transition to the new coordinate should be animated + */ + public void setHeaderInvisible(int viewIndex, boolean animate) { + PinnedHeader header = mHeaders[viewIndex]; + if (header.visible && (animate || header.animating) && header.state == BOTTOM) { + header.sourceY = header.y; + if (!header.animating) { + header.visible = true; + header.targetY = getBottom() + header.height; + } + header.animating = true; + header.targetTime = mAnimationTargetTime; + header.targetVisible = false; + } else { + header.visible = false; + } + } + + private void ensurePinnedHeaderLayout(int viewIndex) { + View view = mHeaders[viewIndex].view; + if (view.isLayoutRequested()) { + ViewGroup.LayoutParams layoutParams = view.getLayoutParams(); + int widthSpec; + int heightSpec; + + if (layoutParams != null && layoutParams.width > 0) { + widthSpec = View.MeasureSpec.makeMeasureSpec(layoutParams.width, View.MeasureSpec.EXACTLY); + } else { + widthSpec = View.MeasureSpec.makeMeasureSpec(mHeaderWidth, View.MeasureSpec.EXACTLY); + } + + if (layoutParams != null && layoutParams.height > 0) { + heightSpec = + View.MeasureSpec.makeMeasureSpec(layoutParams.height, View.MeasureSpec.EXACTLY); + } else { + heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); + } + view.measure(widthSpec, heightSpec); + int height = view.getMeasuredHeight(); + mHeaders[viewIndex].height = height; + view.layout(0, 0, view.getMeasuredWidth(), height); + } + } + + /** Returns the sum of heights of headers pinned to the top. */ + public int getTotalTopPinnedHeaderHeight() { + for (int i = mSize; --i >= 0; ) { + PinnedHeader header = mHeaders[i]; + if (header.visible && header.state == TOP) { + return header.y + header.height; + } + } + return 0; + } + + /** Returns the list item position at the specified y coordinate. */ + public int getPositionAt(int y) { + do { + int position = pointToPosition(getPaddingLeft() + 1, y); + if (position != -1) { + return position; + } + // If position == -1, we must have hit a separator. Let's examine + // a nearby pixel + y--; + } while (y > 0); + return 0; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + mHeaderTouched = false; + if (super.onInterceptTouchEvent(ev)) { + return true; + } + + if (mScrollState == SCROLL_STATE_IDLE) { + final int y = (int) ev.getY(); + final int x = (int) ev.getX(); + for (int i = mSize; --i >= 0; ) { + PinnedHeader header = mHeaders[i]; + // For RTL layouts, this also takes into account that the scrollbar is on the left + // side. + final int padding = getPaddingLeft(); + if (header.visible + && header.y <= y + && header.y + header.height > y + && x >= padding + && padding + header.view.getWidth() >= x) { + mHeaderTouched = true; + if (mScrollToSectionOnHeaderTouch && ev.getAction() == MotionEvent.ACTION_DOWN) { + return smoothScrollToPartition(i); + } else { + return true; + } + } + } + } + + return false; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (mHeaderTouched) { + if (ev.getAction() == MotionEvent.ACTION_UP) { + mHeaderTouched = false; + } + return true; + } + return super.onTouchEvent(ev); + } + + private boolean smoothScrollToPartition(int partition) { + if (mAdapter == null) { + return false; + } + final int position = mAdapter.getScrollPositionForHeader(partition); + if (position == -1) { + return false; + } + + int offset = 0; + for (int i = 0; i < partition; i++) { + PinnedHeader header = mHeaders[i]; + if (header.visible) { + offset += header.height; + } + } + smoothScrollToPositionFromTop( + position + getHeaderViewsCount(), offset, DEFAULT_SMOOTH_SCROLL_DURATION); + return true; + } + + private void invalidateIfAnimating() { + mAnimating = false; + for (int i = 0; i < mSize; i++) { + if (mHeaders[i].animating) { + mAnimating = true; + invalidate(); + return; + } + } + } + + @Override + protected void dispatchDraw(Canvas canvas) { + long currentTime = mAnimating ? System.currentTimeMillis() : 0; + + int top = 0; + int bottom = getBottom(); + boolean hasVisibleHeaders = false; + for (int i = 0; i < mSize; i++) { + PinnedHeader header = mHeaders[i]; + if (header.visible) { + hasVisibleHeaders = true; + if (header.state == BOTTOM && header.y < bottom) { + bottom = header.y; + } else if (header.state == TOP || header.state == FADING) { + int newTop = header.y + header.height; + if (newTop > top) { + top = newTop; + } + } + } + } + + if (hasVisibleHeaders) { + canvas.save(); + } + + super.dispatchDraw(canvas); + + if (hasVisibleHeaders) { + canvas.restore(); + + // If the first item is visible and if it has a positive top that is greater than the + // first header's assigned y-value, use that for the first header's y value. This way, + // the header inherits any padding applied to the list view. + if (mSize > 0 && getFirstVisiblePosition() == 0) { + View firstChild = getChildAt(0); + PinnedHeader firstHeader = mHeaders[0]; + + if (firstHeader != null) { + int firstHeaderTop = firstChild != null ? firstChild.getTop() : 0; + firstHeader.y = Math.max(firstHeader.y, firstHeaderTop); + } + } + + // First draw top headers, then the bottom ones to handle the Z axis correctly + for (int i = mSize; --i >= 0; ) { + PinnedHeader header = mHeaders[i]; + if (header.visible && (header.state == TOP || header.state == FADING)) { + drawHeader(canvas, header, currentTime); + } + } + + for (int i = 0; i < mSize; i++) { + PinnedHeader header = mHeaders[i]; + if (header.visible && header.state == BOTTOM) { + drawHeader(canvas, header, currentTime); + } + } + } + + invalidateIfAnimating(); + } + + private void drawHeader(Canvas canvas, PinnedHeader header, long currentTime) { + if (header.animating) { + int timeLeft = (int) (header.targetTime - currentTime); + if (timeLeft <= 0) { + header.y = header.targetY; + header.visible = header.targetVisible; + header.animating = false; + } else { + header.y = + header.targetY + (header.sourceY - header.targetY) * timeLeft / mAnimationDuration; + } + } + if (header.visible) { + View view = header.view; + int saveCount = canvas.save(); + int translateX = + ViewUtil.isViewLayoutRtl(this) + ? getWidth() - mHeaderPaddingStart - view.getWidth() + : mHeaderPaddingStart; + canvas.translate(translateX, header.y); + if (header.state == FADING) { + mBounds.set(0, 0, view.getWidth(), view.getHeight()); + canvas.saveLayerAlpha(mBounds, header.alpha, Canvas.ALL_SAVE_FLAG); + } + view.draw(canvas); + canvas.restoreToCount(saveCount); + } + } + + /** Adapter interface. The list adapter must implement this interface. */ + public interface PinnedHeaderAdapter { + + /** Returns the overall number of pinned headers, visible or not. */ + int getPinnedHeaderCount(); + + /** Creates or updates the pinned header view. */ + View getPinnedHeaderView(int viewIndex, View convertView, ViewGroup parent); + + /** + * Configures the pinned headers to match the visible list items. The adapter should call {@link + * PinnedHeaderListView#setHeaderPinnedAtTop}, {@link + * PinnedHeaderListView#setHeaderPinnedAtBottom}, {@link PinnedHeaderListView#setFadingHeader} + * or {@link PinnedHeaderListView#setHeaderInvisible}, for each header that needs to change its + * position or visibility. + */ + void configurePinnedHeaders(PinnedHeaderListView listView); + + /** + * Returns the list position to scroll to if the pinned header is touched. Return -1 if the list + * does not need to be scrolled. + */ + int getScrollPositionForHeader(int viewIndex); + } + + private static final class PinnedHeader { + + View view; + boolean visible; + int y; + int height; + int alpha; + int state; + + boolean animating; + boolean targetVisible; + int sourceY; + int targetY; + long targetTime; + } +} diff --git a/java/com/android/contacts/common/list/ViewPagerTabStrip.java b/java/com/android/contacts/common/list/ViewPagerTabStrip.java new file mode 100644 index 000000000..969a6d342 --- /dev/null +++ b/java/com/android/contacts/common/list/ViewPagerTabStrip.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.contacts.common.list; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.util.AttributeSet; +import android.view.View; +import android.widget.LinearLayout; +import com.android.contacts.common.R; + +public class ViewPagerTabStrip extends LinearLayout { + + private final Paint mSelectedUnderlinePaint; + private int mSelectedUnderlineThickness; + private int mIndexForSelection; + private float mSelectionOffset; + + public ViewPagerTabStrip(Context context) { + this(context, null); + } + + public ViewPagerTabStrip(Context context, AttributeSet attrs) { + super(context, attrs); + + final Resources res = context.getResources(); + + mSelectedUnderlineThickness = res.getDimensionPixelSize(R.dimen.tab_selected_underline_height); + int underlineColor = res.getColor(R.color.tab_selected_underline_color); + int backgroundColor = res.getColor(R.color.contactscommon_actionbar_background_color); + + mSelectedUnderlinePaint = new Paint(); + mSelectedUnderlinePaint.setColor(underlineColor); + + setBackgroundColor(backgroundColor); + setWillNotDraw(false); + } + + /** + * Notifies this view that view pager has been scrolled. We save the tab index and selection + * offset for interpolating the position and width of selection underline. + */ + void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + mIndexForSelection = position; + mSelectionOffset = positionOffset; + invalidate(); + } + + @Override + protected void onDraw(Canvas canvas) { + int childCount = getChildCount(); + + // Thick colored underline below the current selection + if (childCount > 0) { + View selectedTitle = getChildAt(mIndexForSelection); + + if (selectedTitle == null) { + // The view pager's tab count changed but we weren't notified yet. Ignore this draw + // pass, when we get a new selection we will update and draw the selection strip in + // the correct place. + return; + } + int selectedLeft = selectedTitle.getLeft(); + int selectedRight = selectedTitle.getRight(); + final boolean isRtl = isRtl(); + final boolean hasNextTab = + isRtl ? mIndexForSelection > 0 : (mIndexForSelection < (getChildCount() - 1)); + if ((mSelectionOffset > 0.0f) && hasNextTab) { + // Draw the selection partway between the tabs + View nextTitle = getChildAt(mIndexForSelection + (isRtl ? -1 : 1)); + int nextLeft = nextTitle.getLeft(); + int nextRight = nextTitle.getRight(); + + selectedLeft = + (int) (mSelectionOffset * nextLeft + (1.0f - mSelectionOffset) * selectedLeft); + selectedRight = + (int) (mSelectionOffset * nextRight + (1.0f - mSelectionOffset) * selectedRight); + } + + int height = getHeight(); + canvas.drawRect( + selectedLeft, + height - mSelectedUnderlineThickness, + selectedRight, + height, + mSelectedUnderlinePaint); + } + } + + private boolean isRtl() { + return getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + } +} diff --git a/java/com/android/contacts/common/list/ViewPagerTabs.java b/java/com/android/contacts/common/list/ViewPagerTabs.java new file mode 100644 index 000000000..34f623ef4 --- /dev/null +++ b/java/com/android/contacts/common/list/ViewPagerTabs.java @@ -0,0 +1,317 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.contacts.common.list; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.TypedArray; +import android.graphics.Outline; +import android.support.v4.view.PagerAdapter; +import android.support.v4.view.ViewPager; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewOutlineProvider; +import android.widget.FrameLayout; +import android.widget.HorizontalScrollView; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; +import com.android.contacts.common.R; +import com.android.dialer.compat.CompatUtils; + +/** + * Lightweight implementation of ViewPager tabs. This looks similar to traditional actionBar tabs, + * but allows for the view containing the tabs to be placed anywhere on screen. Text-related + * attributes can also be assigned in XML - these will get propogated to the child TextViews + * automatically. + */ +public class ViewPagerTabs extends HorizontalScrollView implements ViewPager.OnPageChangeListener { + + private static final ViewOutlineProvider VIEW_BOUNDS_OUTLINE_PROVIDER; + private static final int TAB_SIDE_PADDING_IN_DPS = 10; + // TODO: This should use in the future + private static final int[] ATTRS = + new int[] { + android.R.attr.textSize, + android.R.attr.textStyle, + android.R.attr.textColor, + android.R.attr.textAllCaps + }; + + static { + if (CompatUtils.isLollipopCompatible()) { + VIEW_BOUNDS_OUTLINE_PROVIDER = + new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + outline.setRect(0, 0, view.getWidth(), view.getHeight()); + } + }; + } else { + VIEW_BOUNDS_OUTLINE_PROVIDER = null; + } + } + + /** + * Linearlayout that will contain the TextViews serving as tabs. This is the only child of the + * parent HorizontalScrollView. + */ + final int mTextStyle; + + final ColorStateList mTextColor; + final int mTextSize; + final boolean mTextAllCaps; + ViewPager mPager; + int mPrevSelected = -1; + int mSidePadding; + private ViewPagerTabStrip mTabStrip; + private int[] mTabIcons; + // For displaying the unread count next to the tab icon. + private int[] mUnreadCounts; + + public ViewPagerTabs(Context context) { + this(context, null); + } + + public ViewPagerTabs(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ViewPagerTabs(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + setFillViewport(true); + + mSidePadding = (int) (getResources().getDisplayMetrics().density * TAB_SIDE_PADDING_IN_DPS); + + final TypedArray a = context.obtainStyledAttributes(attrs, ATTRS); + mTextSize = a.getDimensionPixelSize(0, 0); + mTextStyle = a.getInt(1, 0); + mTextColor = a.getColorStateList(2); + mTextAllCaps = a.getBoolean(3, false); + + mTabStrip = new ViewPagerTabStrip(context); + addView( + mTabStrip, + new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT)); + a.recycle(); + + if (CompatUtils.isLollipopCompatible()) { + // enable shadow casting from view bounds + setOutlineProvider(VIEW_BOUNDS_OUTLINE_PROVIDER); + } + } + + public void setViewPager(ViewPager viewPager) { + mPager = viewPager; + addTabs(mPager.getAdapter()); + } + + /** + * Set the tab icons and initialize an array for unread counts the same length as the icon array. + * + * @param tabIcons An array representing the tab icons in order. + */ + public void configureTabIcons(int[] tabIcons) { + mTabIcons = tabIcons; + mUnreadCounts = new int[tabIcons.length]; + } + + public void setUnreadCount(int count, int position) { + if (mUnreadCounts == null || position >= mUnreadCounts.length) { + return; + } + mUnreadCounts[position] = count; + } + + private void addTabs(PagerAdapter adapter) { + mTabStrip.removeAllViews(); + + final int count = adapter.getCount(); + for (int i = 0; i < count; i++) { + addTab(adapter.getPageTitle(i), i); + } + } + + private void addTab(CharSequence tabTitle, final int position) { + View tabView; + if (mTabIcons != null && position < mTabIcons.length) { + View layout = LayoutInflater.from(getContext()).inflate(R.layout.unread_count_tab, null); + View iconView = layout.findViewById(R.id.icon); + iconView.setBackgroundResource(mTabIcons[position]); + iconView.setContentDescription(tabTitle); + TextView textView = (TextView) layout.findViewById(R.id.count); + if (mUnreadCounts != null && mUnreadCounts[position] > 0) { + textView.setText(Integer.toString(mUnreadCounts[position])); + textView.setVisibility(View.VISIBLE); + iconView.setContentDescription( + getResources() + .getQuantityString( + R.plurals.tab_title_with_unread_items, + mUnreadCounts[position], + tabTitle.toString(), + mUnreadCounts[position])); + } else { + textView.setVisibility(View.INVISIBLE); + iconView.setContentDescription(getResources().getString(R.string.tab_title, tabTitle)); + } + tabView = layout; + } else { + final TextView textView = new TextView(getContext()); + textView.setText(tabTitle); + textView.setBackgroundResource(R.drawable.view_pager_tab_background); + + // Assign various text appearance related attributes to child views. + if (mTextStyle > 0) { + textView.setTypeface(textView.getTypeface(), mTextStyle); + } + if (mTextSize > 0) { + textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize); + } + if (mTextColor != null) { + textView.setTextColor(mTextColor); + } + textView.setAllCaps(mTextAllCaps); + textView.setGravity(Gravity.CENTER); + + tabView = textView; + } + + tabView.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View v) { + mPager.setCurrentItem(getRtlPosition(position)); + } + }); + + tabView.setOnLongClickListener(new OnTabLongClickListener(position)); + + tabView.setPadding(mSidePadding, 0, mSidePadding, 0); + + mTabStrip.addView( + tabView, + position, + new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT, 1)); + + // Default to the first child being selected + if (position == 0) { + mPrevSelected = 0; + tabView.setSelected(true); + } + } + + /** + * Remove a tab at a certain index. + * + * @param index The index of the tab view we wish to remove. + */ + public void removeTab(int index) { + View view = mTabStrip.getChildAt(index); + if (view != null) { + mTabStrip.removeView(view); + } + } + + /** + * Refresh a tab at a certain index by removing it and reconstructing it. + * + * @param index The index of the tab view we wish to update. + */ + public void updateTab(int index) { + removeTab(index); + + if (index < mPager.getAdapter().getCount()) { + addTab(mPager.getAdapter().getPageTitle(index), index); + } + } + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + position = getRtlPosition(position); + int tabStripChildCount = mTabStrip.getChildCount(); + if ((tabStripChildCount == 0) || (position < 0) || (position >= tabStripChildCount)) { + return; + } + + mTabStrip.onPageScrolled(position, positionOffset, positionOffsetPixels); + } + + @Override + public void onPageSelected(int position) { + position = getRtlPosition(position); + int tabStripChildCount = mTabStrip.getChildCount(); + if ((tabStripChildCount == 0) || (position < 0) || (position >= tabStripChildCount)) { + return; + } + + if (mPrevSelected >= 0 && mPrevSelected < tabStripChildCount) { + mTabStrip.getChildAt(mPrevSelected).setSelected(false); + } + final View selectedChild = mTabStrip.getChildAt(position); + selectedChild.setSelected(true); + + // Update scroll position + final int scrollPos = selectedChild.getLeft() - (getWidth() - selectedChild.getWidth()) / 2; + smoothScrollTo(scrollPos, 0); + mPrevSelected = position; + } + + @Override + public void onPageScrollStateChanged(int state) {} + + private int getRtlPosition(int position) { + if (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { + return mTabStrip.getChildCount() - 1 - position; + } + return position; + } + + /** Simulates actionbar tab behavior by showing a toast with the tab title when long clicked. */ + private class OnTabLongClickListener implements OnLongClickListener { + + final int mPosition; + + public OnTabLongClickListener(int position) { + mPosition = position; + } + + @Override + public boolean onLongClick(View v) { + final int[] screenPos = new int[2]; + getLocationOnScreen(screenPos); + + final Context context = getContext(); + final int width = getWidth(); + final int height = getHeight(); + final int screenWidth = context.getResources().getDisplayMetrics().widthPixels; + + Toast toast = + Toast.makeText(context, mPager.getAdapter().getPageTitle(mPosition), Toast.LENGTH_SHORT); + + // Show the toast under the tab + toast.setGravity( + Gravity.TOP | Gravity.CENTER_HORIZONTAL, + (screenPos[0] + width / 2) - screenWidth / 2, + screenPos[1] + height); + + toast.show(); + return true; + } + } +} diff --git a/java/com/android/contacts/common/location/CountryDetector.java b/java/com/android/contacts/common/location/CountryDetector.java new file mode 100644 index 000000000..7d9e42b38 --- /dev/null +++ b/java/com/android/contacts/common/location/CountryDetector.java @@ -0,0 +1,221 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.location; + +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.location.Geocoder; +import android.location.Location; +import android.location.LocationManager; +import android.preference.PreferenceManager; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import android.util.Log; +import com.android.dialer.util.PermissionsUtil; +import java.util.Locale; + +/** + * This class is used to detect the country where the user is. It is a simplified version of the + * country detector service in the framework. The sources of country location are queried in the + * following order of reliability: + * + *

    + *
  • Mobile network + *
  • Location manager + *
  • SIM's country + *
  • User's default locale + *
+ * + * As far as possible this class tries to replicate the behavior of the system's country detector + * service: 1) Order in priority of sources of country location 2) Mobile network information + * provided by CDMA phones is ignored 3) Location information is updated every 12 hours (instead of + * 24 hours in the system) 4) Location updates only uses the {@link + * LocationManager#PASSIVE_PROVIDER} to avoid active use of the GPS 5) If a location is successfully + * obtained and geocoded, we never fall back to use of the SIM's country (for the system, the + * fallback never happens without a reboot) 6) Location is not used if the device does not implement + * a {@link android.location.Geocoder} + */ +public class CountryDetector { + + public static final String KEY_PREFERENCE_TIME_UPDATED = "preference_time_updated"; + public static final String KEY_PREFERENCE_CURRENT_COUNTRY = "preference_current_country"; + private static final String TAG = "CountryDetector"; + // Wait 12 hours between updates + private static final long TIME_BETWEEN_UPDATES_MS = 1000L * 60 * 60 * 12; + // Minimum distance before an update is triggered, in meters. We don't need this to be too + // exact because all we care about is what country the user is in. + private static final long DISTANCE_BETWEEN_UPDATES_METERS = 5000; + private static CountryDetector sInstance; + private final TelephonyManager mTelephonyManager; + private final LocationManager mLocationManager; + private final LocaleProvider mLocaleProvider; + // Used as a default country code when all the sources of country data have failed in the + // exceedingly rare event that the device does not have a default locale set for some reason. + private static final String DEFAULT_COUNTRY_ISO = "US"; + private final Context mContext; + + private CountryDetector(Context context) { + this( + context, + (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE), + (LocationManager) context.getSystemService(Context.LOCATION_SERVICE), + new LocaleProvider()); + } + + private CountryDetector( + Context context, + TelephonyManager telephonyManager, + LocationManager locationManager, + LocaleProvider localeProvider) { + mTelephonyManager = telephonyManager; + mLocationManager = locationManager; + mLocaleProvider = localeProvider; + mContext = context; + + registerForLocationUpdates(context, mLocationManager); + } + + public static void registerForLocationUpdates(Context context, LocationManager locationManager) { + if (!PermissionsUtil.hasLocationPermissions(context)) { + Log.w(TAG, "No location permissions, not registering for location updates."); + return; + } + + if (!Geocoder.isPresent()) { + // Certain devices do not have an implementation of a geocoder - in that case there is + // no point trying to get location updates because we cannot retrieve the country based + // on the location anyway. + return; + } + final Intent activeIntent = new Intent(context, LocationChangedReceiver.class); + final PendingIntent pendingIntent = + PendingIntent.getBroadcast(context, 0, activeIntent, PendingIntent.FLAG_UPDATE_CURRENT); + + locationManager.requestLocationUpdates( + LocationManager.PASSIVE_PROVIDER, + TIME_BETWEEN_UPDATES_MS, + DISTANCE_BETWEEN_UPDATES_METERS, + pendingIntent); + } + + /** + * Returns the instance of the country detector. {@link #initialize(Context)} must have been + * called previously. + * + * @return the initialized country detector. + */ + public static synchronized CountryDetector getInstance(Context context) { + if (sInstance == null) { + sInstance = new CountryDetector(context.getApplicationContext()); + } + return sInstance; + } + + /** Factory method for {@link CountryDetector} that allows the caller to provide mock objects. */ + public CountryDetector getInstanceForTest( + Context context, + TelephonyManager telephonyManager, + LocationManager locationManager, + LocaleProvider localeProvider, + Geocoder geocoder) { + return new CountryDetector(context, telephonyManager, locationManager, localeProvider); + } + + public String getCurrentCountryIso() { + String result = null; + if (isNetworkCountryCodeAvailable()) { + result = getNetworkBasedCountryIso(); + } + if (TextUtils.isEmpty(result)) { + result = getLocationBasedCountryIso(); + } + if (TextUtils.isEmpty(result)) { + result = getSimBasedCountryIso(); + } + if (TextUtils.isEmpty(result)) { + result = getLocaleBasedCountryIso(); + } + if (TextUtils.isEmpty(result)) { + result = DEFAULT_COUNTRY_ISO; + } + return result.toUpperCase(Locale.US); + } + + /** @return the country code of the current telephony network the user is connected to. */ + private String getNetworkBasedCountryIso() { + return mTelephonyManager.getNetworkCountryIso(); + } + + /** @return the geocoded country code detected by the {@link LocationManager}. */ + private String getLocationBasedCountryIso() { + if (!Geocoder.isPresent() || !PermissionsUtil.hasLocationPermissions(mContext)) { + return null; + } + final SharedPreferences sharedPreferences = + PreferenceManager.getDefaultSharedPreferences(mContext); + return sharedPreferences.getString(KEY_PREFERENCE_CURRENT_COUNTRY, null); + } + + /** @return the country code of the SIM card currently inserted in the device. */ + private String getSimBasedCountryIso() { + return mTelephonyManager.getSimCountryIso(); + } + + /** @return the country code of the user's currently selected locale. */ + private String getLocaleBasedCountryIso() { + Locale defaultLocale = mLocaleProvider.getDefaultLocale(); + if (defaultLocale != null) { + return defaultLocale.getCountry(); + } + return null; + } + + private boolean isNetworkCountryCodeAvailable() { + // On CDMA TelephonyManager.getNetworkCountryIso() just returns the SIM's country code. + // In this case, we want to ignore the value returned and fallback to location instead. + return mTelephonyManager.getPhoneType() == TelephonyManager.PHONE_TYPE_GSM; + } + + /** + * Class that can be used to return the user's default locale. This is in its own class so that it + * can be mocked out. + */ + public static class LocaleProvider { + + public Locale getDefaultLocale() { + return Locale.getDefault(); + } + } + + public static class LocationChangedReceiver extends BroadcastReceiver { + + @Override + public void onReceive(final Context context, Intent intent) { + if (!intent.hasExtra(LocationManager.KEY_LOCATION_CHANGED)) { + return; + } + + final Location location = + (Location) intent.getExtras().get(LocationManager.KEY_LOCATION_CHANGED); + + UpdateCountryService.updateCountry(context, location); + } + } +} diff --git a/java/com/android/contacts/common/location/UpdateCountryService.java b/java/com/android/contacts/common/location/UpdateCountryService.java new file mode 100644 index 000000000..f23e09e20 --- /dev/null +++ b/java/com/android/contacts/common/location/UpdateCountryService.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.location; + +import android.app.IntentService; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; +import android.location.Address; +import android.location.Geocoder; +import android.location.Location; +import android.preference.PreferenceManager; +import android.util.Log; +import java.io.IOException; +import java.util.List; + +/** + * Service used to perform asynchronous geocoding from within a broadcast receiver. Given a {@link + * Location}, convert it into a country code, and save it in shared preferences. + */ +public class UpdateCountryService extends IntentService { + + private static final String TAG = UpdateCountryService.class.getSimpleName(); + + private static final String ACTION_UPDATE_COUNTRY = "saveCountry"; + + private static final String KEY_INTENT_LOCATION = "location"; + + public UpdateCountryService() { + super(TAG); + } + + public static void updateCountry(Context context, Location location) { + final Intent serviceIntent = new Intent(context, UpdateCountryService.class); + serviceIntent.setAction(ACTION_UPDATE_COUNTRY); + serviceIntent.putExtra(UpdateCountryService.KEY_INTENT_LOCATION, location); + context.startService(serviceIntent); + } + + @Override + protected void onHandleIntent(Intent intent) { + if (intent == null) { + Log.d(TAG, "onHandleIntent: could not handle null intent"); + return; + } + if (ACTION_UPDATE_COUNTRY.equals(intent.getAction())) { + final Location location = intent.getParcelableExtra(KEY_INTENT_LOCATION); + final String country = getCountryFromLocation(getApplicationContext(), location); + + if (country == null) { + return; + } + + final SharedPreferences prefs = + PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); + + final Editor editor = prefs.edit(); + editor.putLong(CountryDetector.KEY_PREFERENCE_TIME_UPDATED, System.currentTimeMillis()); + editor.putString(CountryDetector.KEY_PREFERENCE_CURRENT_COUNTRY, country); + editor.commit(); + } + } + + /** + * Given a {@link Location}, return a country code. + * + * @return the ISO 3166-1 two letter country code + */ + private String getCountryFromLocation(Context context, Location location) { + final Geocoder geocoder = new Geocoder(context); + String country = null; + try { + double latitude = location.getLatitude(); + // Latitude has to be between 90 and -90 (latitude of north and south poles wrt equator) + if (latitude <= 90 && latitude >= -90) { + final List
addresses = + geocoder.getFromLocation(location.getLatitude(), location.getLongitude(), 1); + if (addresses != null && addresses.size() > 0) { + country = addresses.get(0).getCountryCode(); + } + } else { + Log.w(TAG, "Invalid latitude"); + } + } catch (IOException e) { + Log.w(TAG, "Exception occurred when getting geocoded country from location"); + } + return country; + } +} diff --git a/java/com/android/contacts/common/model/AccountTypeManager.java b/java/com/android/contacts/common/model/AccountTypeManager.java new file mode 100644 index 000000000..f225ff6ac --- /dev/null +++ b/java/com/android/contacts/common/model/AccountTypeManager.java @@ -0,0 +1,813 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.accounts.AuthenticatorDescription; +import android.accounts.OnAccountsUpdateListener; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SyncAdapterType; +import android.content.SyncStatusObserver; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.os.SystemClock; +import android.provider.ContactsContract; +import android.support.annotation.VisibleForTesting; +import android.text.TextUtils; +import android.util.ArrayMap; +import android.util.Log; +import android.util.TimingLogger; +import com.android.contacts.common.MoreContactUtils; +import com.android.contacts.common.list.ContactListFilterController; +import com.android.contacts.common.model.account.AccountType; +import com.android.contacts.common.model.account.AccountTypeWithDataSet; +import com.android.contacts.common.model.account.AccountWithDataSet; +import com.android.contacts.common.model.account.ExchangeAccountType; +import com.android.contacts.common.model.account.ExternalAccountType; +import com.android.contacts.common.model.account.FallbackAccountType; +import com.android.contacts.common.model.account.GoogleAccountType; +import com.android.contacts.common.model.account.SamsungAccountType; +import com.android.contacts.common.model.dataitem.DataKind; +import com.android.contacts.common.util.Constants; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Singleton holder for all parsed {@link AccountType} available on the system, typically filled + * through {@link PackageManager} queries. + */ +public abstract class AccountTypeManager { + + static final String TAG = "AccountTypeManager"; + + private static final Object mInitializationLock = new Object(); + private static AccountTypeManager mAccountTypeManager; + + /** + * Requests the singleton instance of {@link AccountTypeManager} with data bound from the + * available authenticators. This method can safely be called from the UI thread. + */ + public static AccountTypeManager getInstance(Context context) { + synchronized (mInitializationLock) { + if (mAccountTypeManager == null) { + context = context.getApplicationContext(); + mAccountTypeManager = new AccountTypeManagerImpl(context); + } + } + return mAccountTypeManager; + } + + /** + * Set the instance of account type manager. This is only for and should only be used by unit + * tests. While having this method is not ideal, it's simpler than the alternative of holding this + * as a service in the ContactsApplication context class. + * + * @param mockManager The mock AccountTypeManager. + */ + public static void setInstanceForTest(AccountTypeManager mockManager) { + synchronized (mInitializationLock) { + mAccountTypeManager = mockManager; + } + } + + /** + * Returns the list of all accounts (if contactWritableOnly is false) or just the list of contact + * writable accounts (if contactWritableOnly is true). + */ + // TODO: Consider splitting this into getContactWritableAccounts() and getAllAccounts() + public abstract List getAccounts(boolean contactWritableOnly); + + /** Returns the list of accounts that are group writable. */ + public abstract List getGroupWritableAccounts(); + + public abstract AccountType getAccountType(AccountTypeWithDataSet accountTypeWithDataSet); + + public final AccountType getAccountType(String accountType, String dataSet) { + return getAccountType(AccountTypeWithDataSet.get(accountType, dataSet)); + } + + public final AccountType getAccountTypeForAccount(AccountWithDataSet account) { + if (account != null) { + return getAccountType(account.getAccountTypeWithDataSet()); + } + return getAccountType(null, null); + } + + /** + * @return Unmodifiable map from {@link AccountTypeWithDataSet}s to {@link AccountType}s which + * support the "invite" feature and have one or more account. + *

This is a filtered down and more "usable" list compared to {@link + * #getAllInvitableAccountTypes}, where usable is defined as: (1) making sure that the app + * that contributed the account type is not disabled (in order to avoid presenting the user + * with an option that does nothing), and (2) that there is at least one raw contact with that + * account type in the database (assuming that the user probably doesn't use that account + * type). + *

Warning: Don't use on the UI thread because this can scan the database. + */ + public abstract Map getUsableInvitableAccountTypes(); + + /** + * Find the best {@link DataKind} matching the requested {@link AccountType#accountType}, {@link + * AccountType#dataSet}, and {@link DataKind#mimeType}. If no direct match found, we try searching + * {@link FallbackAccountType}. + */ + public DataKind getKindOrFallback(AccountType type, String mimeType) { + return type == null ? null : type.getKindForMimetype(mimeType); + } + + /** + * Returns all registered {@link AccountType}s, including extension ones. + * + * @param contactWritableOnly if true, it only returns ones that support writing contacts. + */ + public abstract List getAccountTypes(boolean contactWritableOnly); + + /** + * @param contactWritableOnly if true, it only returns ones that support writing contacts. + * @return true when this instance contains the given account. + */ + public boolean contains(AccountWithDataSet account, boolean contactWritableOnly) { + for (AccountWithDataSet account_2 : getAccounts(false)) { + if (account.equals(account_2)) { + return true; + } + } + return false; + } +} + +class AccountTypeManagerImpl extends AccountTypeManager + implements OnAccountsUpdateListener, SyncStatusObserver { + + private static final Map + EMPTY_UNMODIFIABLE_ACCOUNT_TYPE_MAP = + Collections.unmodifiableMap(new HashMap()); + + /** + * A sample contact URI used to test whether any activities will respond to an invitable intent + * with the given URI as the intent data. This doesn't need to be specific to a real contact + * because an app that intercepts the intent should probably do so for all types of contact URIs. + */ + private static final Uri SAMPLE_CONTACT_URI = ContactsContract.Contacts.getLookupUri(1, "xxx"); + + private static final int MESSAGE_LOAD_DATA = 0; + private static final int MESSAGE_PROCESS_BROADCAST_INTENT = 1; + private static final Comparator ACCOUNT_COMPARATOR = + new Comparator() { + @Override + public int compare(AccountWithDataSet a, AccountWithDataSet b) { + if (Objects.equals(a.name, b.name) + && Objects.equals(a.type, b.type) + && Objects.equals(a.dataSet, b.dataSet)) { + return 0; + } else if (b.name == null || b.type == null) { + return -1; + } else if (a.name == null || a.type == null) { + return 1; + } else { + int diff = a.name.compareTo(b.name); + if (diff != 0) { + return diff; + } + diff = a.type.compareTo(b.type); + if (diff != 0) { + return diff; + } + + // Accounts without data sets get sorted before those that have them. + if (a.dataSet != null) { + return b.dataSet == null ? 1 : a.dataSet.compareTo(b.dataSet); + } else { + return -1; + } + } + } + }; + private final InvitableAccountTypeCache mInvitableAccountTypeCache; + /** + * The boolean value is equal to true if the {@link InvitableAccountTypeCache} has been + * initialized. False otherwise. + */ + private final AtomicBoolean mInvitablesCacheIsInitialized = new AtomicBoolean(false); + /** + * The boolean value is equal to true if the {@link FindInvitablesTask} is still executing. False + * otherwise. + */ + private final AtomicBoolean mInvitablesTaskIsRunning = new AtomicBoolean(false); + + private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper()); + private Context mContext; + private final Runnable mCheckFilterValidityRunnable = + new Runnable() { + @Override + public void run() { + ContactListFilterController.getInstance(mContext).checkFilterValidity(true); + } + }; + private AccountManager mAccountManager; + private AccountType mFallbackAccountType; + private List mAccounts = new ArrayList<>(); + private List mContactWritableAccounts = new ArrayList<>(); + private List mGroupWritableAccounts = new ArrayList<>(); + private Map mAccountTypesWithDataSets = new ArrayMap<>(); + private Map mInvitableAccountTypes = + EMPTY_UNMODIFIABLE_ACCOUNT_TYPE_MAP; + private HandlerThread mListenerThread; + private Handler mListenerHandler; + private BroadcastReceiver mBroadcastReceiver = + new BroadcastReceiver() { + + @Override + public void onReceive(Context context, Intent intent) { + Message msg = mListenerHandler.obtainMessage(MESSAGE_PROCESS_BROADCAST_INTENT, intent); + mListenerHandler.sendMessage(msg); + } + }; + /* A latch that ensures that asynchronous initialization completes before data is used */ + private volatile CountDownLatch mInitializationLatch = new CountDownLatch(1); + + /** Internal constructor that only performs initial parsing. */ + public AccountTypeManagerImpl(Context context) { + mContext = context; + mFallbackAccountType = new FallbackAccountType(context); + + mAccountManager = AccountManager.get(mContext); + + mListenerThread = new HandlerThread("AccountChangeListener"); + mListenerThread.start(); + mListenerHandler = + new Handler(mListenerThread.getLooper()) { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MESSAGE_LOAD_DATA: + loadAccountsInBackground(); + break; + case MESSAGE_PROCESS_BROADCAST_INTENT: + processBroadcastIntent((Intent) msg.obj); + break; + } + } + }; + + mInvitableAccountTypeCache = new InvitableAccountTypeCache(); + + // Request updates when packages or accounts change + IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED); + filter.addAction(Intent.ACTION_PACKAGE_REMOVED); + filter.addAction(Intent.ACTION_PACKAGE_CHANGED); + filter.addDataScheme("package"); + mContext.registerReceiver(mBroadcastReceiver, filter); + IntentFilter sdFilter = new IntentFilter(); + sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE); + sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE); + mContext.registerReceiver(mBroadcastReceiver, sdFilter); + + // Request updates when locale is changed so that the order of each field will + // be able to be changed on the locale change. + filter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED); + mContext.registerReceiver(mBroadcastReceiver, filter); + + mAccountManager.addOnAccountsUpdatedListener(this, mListenerHandler, false); + + ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, this); + + mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA); + } + + /** + * Find a specific {@link AuthenticatorDescription} in the provided list that matches the given + * account type. + */ + protected static AuthenticatorDescription findAuthenticator( + AuthenticatorDescription[] auths, String accountType) { + for (AuthenticatorDescription auth : auths) { + if (accountType.equals(auth.type)) { + return auth; + } + } + return null; + } + + /** + * Return all {@link AccountType}s with at least one account which supports "invite", i.e. its + * {@link AccountType#getInviteContactActivityClassName()} is not empty. + */ + @VisibleForTesting + static Map findAllInvitableAccountTypes( + Context context, + Collection accounts, + Map accountTypesByTypeAndDataSet) { + Map result = new ArrayMap<>(); + for (AccountWithDataSet account : accounts) { + AccountTypeWithDataSet accountTypeWithDataSet = account.getAccountTypeWithDataSet(); + AccountType type = accountTypesByTypeAndDataSet.get(accountTypeWithDataSet); + if (type == null) { + continue; // just in case + } + if (result.containsKey(accountTypeWithDataSet)) { + continue; + } + + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d( + TAG, + "Type " + + accountTypeWithDataSet + + " inviteClass=" + + type.getInviteContactActivityClassName()); + } + if (!TextUtils.isEmpty(type.getInviteContactActivityClassName())) { + result.put(accountTypeWithDataSet, type); + } + } + return Collections.unmodifiableMap(result); + } + + @Override + public void onStatusChanged(int which) { + mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA); + } + + public void processBroadcastIntent(Intent intent) { + mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA); + } + + /* This notification will arrive on the background thread */ + public void onAccountsUpdated(Account[] accounts) { + // Refresh to catch any changed accounts + loadAccountsInBackground(); + } + + /** + * Returns instantly if accounts and account types have already been loaded. Otherwise waits for + * the background thread to complete the loading. + */ + void ensureAccountsLoaded() { + CountDownLatch latch = mInitializationLatch; + if (latch == null) { + return; + } + while (true) { + try { + latch.await(); + return; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + + /** + * Loads account list and corresponding account types (potentially with data sets). Always called + * on a background thread. + */ + protected void loadAccountsInBackground() { + if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) { + Log.d(Constants.PERFORMANCE_TAG, "AccountTypeManager.loadAccountsInBackground start"); + } + TimingLogger timings = new TimingLogger(TAG, "loadAccountsInBackground"); + final long startTime = SystemClock.currentThreadTimeMillis(); + final long startTimeWall = SystemClock.elapsedRealtime(); + + // Account types, keyed off the account type and data set concatenation. + final Map accountTypesByTypeAndDataSet = new ArrayMap<>(); + + // The same AccountTypes, but keyed off {@link RawContacts#ACCOUNT_TYPE}. Since there can + // be multiple account types (with different data sets) for the same type of account, each + // type string may have multiple AccountType entries. + final Map> accountTypesByType = new ArrayMap<>(); + + final List allAccounts = new ArrayList<>(); + final List contactWritableAccounts = new ArrayList<>(); + final List groupWritableAccounts = new ArrayList<>(); + final Set extensionPackages = new HashSet<>(); + + final AccountManager am = mAccountManager; + + final SyncAdapterType[] syncs = ContentResolver.getSyncAdapterTypes(); + final AuthenticatorDescription[] auths = am.getAuthenticatorTypes(); + + // First process sync adapters to find any that provide contact data. + for (SyncAdapterType sync : syncs) { + if (!ContactsContract.AUTHORITY.equals(sync.authority)) { + // Skip sync adapters that don't provide contact data. + continue; + } + + // Look for the formatting details provided by each sync + // adapter, using the authenticator to find general resources. + final String type = sync.accountType; + final AuthenticatorDescription auth = findAuthenticator(auths, type); + if (auth == null) { + Log.w(TAG, "No authenticator found for type=" + type + ", ignoring it."); + continue; + } + + AccountType accountType; + if (GoogleAccountType.ACCOUNT_TYPE.equals(type)) { + accountType = new GoogleAccountType(mContext, auth.packageName); + } else if (ExchangeAccountType.isExchangeType(type)) { + accountType = new ExchangeAccountType(mContext, auth.packageName, type); + } else if (SamsungAccountType.isSamsungAccountType(mContext, type, auth.packageName)) { + accountType = new SamsungAccountType(mContext, auth.packageName, type); + } else { + Log.d( + TAG, "Registering external account type=" + type + ", packageName=" + auth.packageName); + accountType = new ExternalAccountType(mContext, auth.packageName, false); + } + if (!accountType.isInitialized()) { + if (accountType.isEmbedded()) { + throw new IllegalStateException( + "Problem initializing embedded type " + accountType.getClass().getCanonicalName()); + } else { + // Skip external account types that couldn't be initialized. + continue; + } + } + + accountType.accountType = auth.type; + accountType.titleRes = auth.labelId; + accountType.iconRes = auth.iconId; + + addAccountType(accountType, accountTypesByTypeAndDataSet, accountTypesByType); + + // Check to see if the account type knows of any other non-sync-adapter packages + // that may provide other data sets of contact data. + extensionPackages.addAll(accountType.getExtensionPackageNames()); + } + + // If any extension packages were specified, process them as well. + if (!extensionPackages.isEmpty()) { + Log.d(TAG, "Registering " + extensionPackages.size() + " extension packages"); + for (String extensionPackage : extensionPackages) { + ExternalAccountType accountType = new ExternalAccountType(mContext, extensionPackage, true); + if (!accountType.isInitialized()) { + // Skip external account types that couldn't be initialized. + continue; + } + if (!accountType.hasContactsMetadata()) { + Log.w( + TAG, + "Skipping extension package " + + extensionPackage + + " because" + + " it doesn't have the CONTACTS_STRUCTURE metadata"); + continue; + } + if (TextUtils.isEmpty(accountType.accountType)) { + Log.w( + TAG, + "Skipping extension package " + + extensionPackage + + " because" + + " the CONTACTS_STRUCTURE metadata doesn't have the accountType" + + " attribute"); + continue; + } + Log.d( + TAG, + "Registering extension package account type=" + + accountType.accountType + + ", dataSet=" + + accountType.dataSet + + ", packageName=" + + extensionPackage); + + addAccountType(accountType, accountTypesByTypeAndDataSet, accountTypesByType); + } + } + timings.addSplit("Loaded account types"); + + // Map in accounts to associate the account names with each account type entry. + Account[] accounts = mAccountManager.getAccounts(); + for (Account account : accounts) { + boolean syncable = ContentResolver.getIsSyncable(account, ContactsContract.AUTHORITY) > 0; + + if (syncable) { + List accountTypes = accountTypesByType.get(account.type); + if (accountTypes != null) { + // Add an account-with-data-set entry for each account type that is + // authenticated by this account. + for (AccountType accountType : accountTypes) { + AccountWithDataSet accountWithDataSet = + new AccountWithDataSet(account.name, account.type, accountType.dataSet); + allAccounts.add(accountWithDataSet); + if (accountType.areContactsWritable()) { + contactWritableAccounts.add(accountWithDataSet); + } + if (accountType.isGroupMembershipEditable()) { + groupWritableAccounts.add(accountWithDataSet); + } + } + } + } + } + + Collections.sort(allAccounts, ACCOUNT_COMPARATOR); + Collections.sort(contactWritableAccounts, ACCOUNT_COMPARATOR); + Collections.sort(groupWritableAccounts, ACCOUNT_COMPARATOR); + + timings.addSplit("Loaded accounts"); + + synchronized (this) { + mAccountTypesWithDataSets = accountTypesByTypeAndDataSet; + mAccounts = allAccounts; + mContactWritableAccounts = contactWritableAccounts; + mGroupWritableAccounts = groupWritableAccounts; + mInvitableAccountTypes = + findAllInvitableAccountTypes(mContext, allAccounts, accountTypesByTypeAndDataSet); + } + + timings.dumpToLog(); + final long endTimeWall = SystemClock.elapsedRealtime(); + final long endTime = SystemClock.currentThreadTimeMillis(); + + Log.i( + TAG, + "Loaded meta-data for " + + mAccountTypesWithDataSets.size() + + " account types, " + + mAccounts.size() + + " accounts in " + + (endTimeWall - startTimeWall) + + "ms(wall) " + + (endTime - startTime) + + "ms(cpu)"); + + if (mInitializationLatch != null) { + mInitializationLatch.countDown(); + mInitializationLatch = null; + } + if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) { + Log.d(Constants.PERFORMANCE_TAG, "AccountTypeManager.loadAccountsInBackground finish"); + } + + // Check filter validity since filter may become obsolete after account update. It must be + // done from UI thread. + mMainThreadHandler.post(mCheckFilterValidityRunnable); + } + + // Bookkeeping method for tracking the known account types in the given maps. + private void addAccountType( + AccountType accountType, + Map accountTypesByTypeAndDataSet, + Map> accountTypesByType) { + accountTypesByTypeAndDataSet.put(accountType.getAccountTypeAndDataSet(), accountType); + List accountsForType = accountTypesByType.get(accountType.accountType); + if (accountsForType == null) { + accountsForType = new ArrayList<>(); + } + accountsForType.add(accountType); + accountTypesByType.put(accountType.accountType, accountsForType); + } + + /** Return list of all known, contact writable {@link AccountWithDataSet}'s. */ + @Override + public List getAccounts(boolean contactWritableOnly) { + ensureAccountsLoaded(); + return contactWritableOnly ? mContactWritableAccounts : mAccounts; + } + + /** Return the list of all known, group writable {@link AccountWithDataSet}'s. */ + public List getGroupWritableAccounts() { + ensureAccountsLoaded(); + return mGroupWritableAccounts; + } + + /** + * Find the best {@link DataKind} matching the requested {@link AccountType#accountType}, {@link + * AccountType#dataSet}, and {@link DataKind#mimeType}. If no direct match found, we try searching + * {@link FallbackAccountType}. + */ + @Override + public DataKind getKindOrFallback(AccountType type, String mimeType) { + ensureAccountsLoaded(); + DataKind kind = null; + + // Try finding account type and kind matching request + if (type != null) { + kind = type.getKindForMimetype(mimeType); + } + + if (kind == null) { + // Nothing found, so try fallback as last resort + kind = mFallbackAccountType.getKindForMimetype(mimeType); + } + + if (kind == null) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Unknown type=" + type + ", mime=" + mimeType); + } + } + + return kind; + } + + /** Return {@link AccountType} for the given account type and data set. */ + @Override + public AccountType getAccountType(AccountTypeWithDataSet accountTypeWithDataSet) { + ensureAccountsLoaded(); + synchronized (this) { + AccountType type = mAccountTypesWithDataSets.get(accountTypeWithDataSet); + return type != null ? type : mFallbackAccountType; + } + } + + /** + * @return Unmodifiable map from {@link AccountTypeWithDataSet}s to {@link AccountType}s which + * support the "invite" feature and have one or more account. This is an unfiltered list. See + * {@link #getUsableInvitableAccountTypes()}. + */ + private Map getAllInvitableAccountTypes() { + ensureAccountsLoaded(); + return mInvitableAccountTypes; + } + + @Override + public Map getUsableInvitableAccountTypes() { + ensureAccountsLoaded(); + // Since this method is not thread-safe, it's possible for multiple threads to encounter + // the situation where (1) the cache has not been initialized yet or + // (2) an async task to refresh the account type list in the cache has already been + // started. Hence we use {@link AtomicBoolean}s and return cached values immediately + // while we compute the actual result in the background. We use this approach instead of + // using "synchronized" because computing the account type list involves a DB read, and + // can potentially cause a deadlock situation if this method is called from code which + // holds the DB lock. The trade-off of potentially having an incorrect list of invitable + // account types for a short period of time seems more manageable than enforcing the + // context in which this method is called. + + // Computing the list of usable invitable account types is done on the fly as requested. + // If this method has never been called before, then block until the list has been computed. + if (!mInvitablesCacheIsInitialized.get()) { + mInvitableAccountTypeCache.setCachedValue(findUsableInvitableAccountTypes(mContext)); + mInvitablesCacheIsInitialized.set(true); + } else { + // Otherwise, there is a value in the cache. If the value has expired and + // an async task has not already been started by another thread, then kick off a new + // async task to compute the list. + if (mInvitableAccountTypeCache.isExpired() + && mInvitablesTaskIsRunning.compareAndSet(false, true)) { + new FindInvitablesTask().execute(); + } + } + + return mInvitableAccountTypeCache.getCachedValue(); + } + + /** + * Return all usable {@link AccountType}s that support the "invite" feature from the list of all + * potential invitable account types (retrieved from {@link #getAllInvitableAccountTypes}). A + * usable invitable account type means: (1) there is at least 1 raw contact in the database with + * that account type, and (2) the app contributing the account type is not disabled. + * + *

Warning: Don't use on the UI thread because this can scan the database. + */ + private Map findUsableInvitableAccountTypes( + Context context) { + Map allInvitables = getAllInvitableAccountTypes(); + if (allInvitables.isEmpty()) { + return EMPTY_UNMODIFIABLE_ACCOUNT_TYPE_MAP; + } + + final Map result = new ArrayMap<>(); + result.putAll(allInvitables); + + final PackageManager packageManager = context.getPackageManager(); + for (AccountTypeWithDataSet accountTypeWithDataSet : allInvitables.keySet()) { + AccountType accountType = allInvitables.get(accountTypeWithDataSet); + + // Make sure that account types don't come from apps that are disabled. + Intent invitableIntent = MoreContactUtils.getInvitableIntent(accountType, SAMPLE_CONTACT_URI); + if (invitableIntent == null) { + result.remove(accountTypeWithDataSet); + continue; + } + ResolveInfo resolveInfo = + packageManager.resolveActivity(invitableIntent, PackageManager.MATCH_DEFAULT_ONLY); + if (resolveInfo == null) { + // If we can't find an activity to start for this intent, then there's no point in + // showing this option to the user. + result.remove(accountTypeWithDataSet); + continue; + } + + // Make sure that there is at least 1 raw contact with this account type. This check + // is non-trivial and should not be done on the UI thread. + if (!accountTypeWithDataSet.hasData(context)) { + result.remove(accountTypeWithDataSet); + } + } + + return Collections.unmodifiableMap(result); + } + + @Override + public List getAccountTypes(boolean contactWritableOnly) { + ensureAccountsLoaded(); + final List accountTypes = new ArrayList<>(); + synchronized (this) { + for (AccountType type : mAccountTypesWithDataSets.values()) { + if (!contactWritableOnly || type.areContactsWritable()) { + accountTypes.add(type); + } + } + } + return accountTypes; + } + + /** + * This cache holds a list of invitable {@link AccountTypeWithDataSet}s, in the form of a {@link + * Map}. Note that the cached value is valid only for {@link + * #TIME_TO_LIVE} milliseconds. + */ + private static final class InvitableAccountTypeCache { + + /** + * The cached {@link #mInvitableAccountTypes} list expires after this number of milliseconds has + * elapsed. + */ + private static final long TIME_TO_LIVE = 60000; + + private Map mInvitableAccountTypes; + + private long mTimeLastSet; + + /** + * Returns true if the data in this cache is stale and needs to be refreshed. Returns false + * otherwise. + */ + public boolean isExpired() { + return SystemClock.elapsedRealtime() - mTimeLastSet > TIME_TO_LIVE; + } + + /** + * Returns the cached value. Note that the caller is responsible for checking {@link + * #isExpired()} to ensure that the value is not stale. + */ + public Map getCachedValue() { + return mInvitableAccountTypes; + } + + public void setCachedValue(Map map) { + mInvitableAccountTypes = map; + mTimeLastSet = SystemClock.elapsedRealtime(); + } + } + + /** + * Background task to find all usable {@link AccountType}s that support the "invite" feature from + * the list of all potential invitable account types. Once the work is completed, the list of + * account types is stored in the {@link AccountTypeManager}'s {@link InvitableAccountTypeCache}. + */ + private class FindInvitablesTask + extends AsyncTask> { + + @Override + protected Map doInBackground(Void... params) { + return findUsableInvitableAccountTypes(mContext); + } + + @Override + protected void onPostExecute(Map accountTypes) { + mInvitableAccountTypeCache.setCachedValue(accountTypes); + mInvitablesTaskIsRunning.set(false); + } + } +} diff --git a/java/com/android/contacts/common/model/BuilderWrapper.java b/java/com/android/contacts/common/model/BuilderWrapper.java new file mode 100644 index 000000000..9c666e59c --- /dev/null +++ b/java/com/android/contacts/common/model/BuilderWrapper.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model; + +import android.content.ContentProviderOperation.Builder; + +/** + * This class is created for the purpose of compatibility and make the type of + * ContentProviderOperation available on pre-M SDKs. Since ContentProviderOperation is usually + * created by Builder and we don’t have access to the type via Builder, so we need to create a + * wrapper class for Builder first and include type. Then we could use the builder and the type in + * this class to create a wrapper of ContentProviderOperation. + */ +public class BuilderWrapper { + + private Builder mBuilder; + private int mType; + + public BuilderWrapper(Builder builder, int type) { + mBuilder = builder; + mType = type; + } + + public int getType() { + return mType; + } + + public void setType(int mType) { + this.mType = mType; + } + + public Builder getBuilder() { + return mBuilder; + } + + public void setBuilder(Builder mBuilder) { + this.mBuilder = mBuilder; + } +} diff --git a/java/com/android/contacts/common/model/CPOWrapper.java b/java/com/android/contacts/common/model/CPOWrapper.java new file mode 100644 index 000000000..4a67e6700 --- /dev/null +++ b/java/com/android/contacts/common/model/CPOWrapper.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model; + +import android.content.ContentProviderOperation; + +/** + * This class is created for the purpose of compatibility and make the type of + * ContentProviderOperation available on pre-M SDKs. + */ +public class CPOWrapper { + + private ContentProviderOperation mOperation; + private int mType; + + public CPOWrapper(ContentProviderOperation builder, int type) { + mOperation = builder; + mType = type; + } + + public int getType() { + return mType; + } + + public void setType(int type) { + this.mType = type; + } + + public ContentProviderOperation getOperation() { + return mOperation; + } + + public void setOperation(ContentProviderOperation operation) { + this.mOperation = operation; + } +} diff --git a/java/com/android/contacts/common/model/Contact.java b/java/com/android/contacts/common/model/Contact.java new file mode 100644 index 000000000..ad0b66efe --- /dev/null +++ b/java/com/android/contacts/common/model/Contact.java @@ -0,0 +1,384 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model; + +import android.content.ContentValues; +import android.net.Uri; +import android.provider.ContactsContract.CommonDataKinds.Photo; +import android.provider.ContactsContract.Data; +import android.provider.ContactsContract.Directory; +import android.provider.ContactsContract.DisplayNameSources; +import android.support.annotation.VisibleForTesting; +import com.android.contacts.common.GroupMetaData; +import com.android.contacts.common.model.account.AccountType; +import com.google.common.collect.ImmutableList; +import java.util.ArrayList; + +/** + * A Contact represents a single person or logical entity as perceived by the user. The information + * about a contact can come from multiple data sources, which are each represented by a RawContact + * object. Thus, a Contact is associated with a collection of RawContact objects. + * + *

The aggregation of raw contacts into a single contact is performed automatically, and it is + * also possible for users to manually split and join raw contacts into various contacts. + * + *

Only the {@link ContactLoader} class can create a Contact object with various flags to allow + * partial loading of contact data. Thus, an instance of this class should be treated as a read-only + * object. + */ +public class Contact { + + private final Uri mRequestedUri; + private final Uri mLookupUri; + private final Uri mUri; + private final long mDirectoryId; + private final String mLookupKey; + private final long mId; + private final long mNameRawContactId; + private final int mDisplayNameSource; + private final long mPhotoId; + private final String mPhotoUri; + private final String mDisplayName; + private final String mAltDisplayName; + private final String mPhoneticName; + private final boolean mStarred; + private final Integer mPresence; + private final boolean mSendToVoicemail; + private final String mCustomRingtone; + private final boolean mIsUserProfile; + private final Contact.Status mStatus; + private final Exception mException; + private ImmutableList mRawContacts; + private ImmutableList mInvitableAccountTypes; + private String mDirectoryDisplayName; + private String mDirectoryType; + private String mDirectoryAccountType; + private String mDirectoryAccountName; + private int mDirectoryExportSupport; + private ImmutableList mGroups; + private byte[] mPhotoBinaryData; + /** + * Small version of the contact photo loaded from a blob instead of from a file. If a large + * contact photo is not available yet, then this has the same value as mPhotoBinaryData. + */ + private byte[] mThumbnailPhotoBinaryData; + + /** Constructor for special results, namely "no contact found" and "error". */ + private Contact(Uri requestedUri, Contact.Status status, Exception exception) { + if (status == Status.ERROR && exception == null) { + throw new IllegalArgumentException("ERROR result must have exception"); + } + mStatus = status; + mException = exception; + mRequestedUri = requestedUri; + mLookupUri = null; + mUri = null; + mDirectoryId = -1; + mLookupKey = null; + mId = -1; + mRawContacts = null; + mNameRawContactId = -1; + mDisplayNameSource = DisplayNameSources.UNDEFINED; + mPhotoId = -1; + mPhotoUri = null; + mDisplayName = null; + mAltDisplayName = null; + mPhoneticName = null; + mStarred = false; + mPresence = null; + mInvitableAccountTypes = null; + mSendToVoicemail = false; + mCustomRingtone = null; + mIsUserProfile = false; + } + + /** Constructor to call when contact was found */ + public Contact( + Uri requestedUri, + Uri uri, + Uri lookupUri, + long directoryId, + String lookupKey, + long id, + long nameRawContactId, + int displayNameSource, + long photoId, + String photoUri, + String displayName, + String altDisplayName, + String phoneticName, + boolean starred, + Integer presence, + boolean sendToVoicemail, + String customRingtone, + boolean isUserProfile) { + mStatus = Status.LOADED; + mException = null; + mRequestedUri = requestedUri; + mLookupUri = lookupUri; + mUri = uri; + mDirectoryId = directoryId; + mLookupKey = lookupKey; + mId = id; + mRawContacts = null; + mNameRawContactId = nameRawContactId; + mDisplayNameSource = displayNameSource; + mPhotoId = photoId; + mPhotoUri = photoUri; + mDisplayName = displayName; + mAltDisplayName = altDisplayName; + mPhoneticName = phoneticName; + mStarred = starred; + mPresence = presence; + mInvitableAccountTypes = null; + mSendToVoicemail = sendToVoicemail; + mCustomRingtone = customRingtone; + mIsUserProfile = isUserProfile; + } + + public Contact(Uri requestedUri, Contact from) { + mRequestedUri = requestedUri; + + mStatus = from.mStatus; + mException = from.mException; + mLookupUri = from.mLookupUri; + mUri = from.mUri; + mDirectoryId = from.mDirectoryId; + mLookupKey = from.mLookupKey; + mId = from.mId; + mNameRawContactId = from.mNameRawContactId; + mDisplayNameSource = from.mDisplayNameSource; + mPhotoId = from.mPhotoId; + mPhotoUri = from.mPhotoUri; + mDisplayName = from.mDisplayName; + mAltDisplayName = from.mAltDisplayName; + mPhoneticName = from.mPhoneticName; + mStarred = from.mStarred; + mPresence = from.mPresence; + mRawContacts = from.mRawContacts; + mInvitableAccountTypes = from.mInvitableAccountTypes; + + mDirectoryDisplayName = from.mDirectoryDisplayName; + mDirectoryType = from.mDirectoryType; + mDirectoryAccountType = from.mDirectoryAccountType; + mDirectoryAccountName = from.mDirectoryAccountName; + mDirectoryExportSupport = from.mDirectoryExportSupport; + + mGroups = from.mGroups; + + mPhotoBinaryData = from.mPhotoBinaryData; + mSendToVoicemail = from.mSendToVoicemail; + mCustomRingtone = from.mCustomRingtone; + mIsUserProfile = from.mIsUserProfile; + } + + public static Contact forError(Uri requestedUri, Exception exception) { + return new Contact(requestedUri, Status.ERROR, exception); + } + + public static Contact forNotFound(Uri requestedUri) { + return new Contact(requestedUri, Status.NOT_FOUND, null); + } + + /** @param exportSupport See {@link Directory#EXPORT_SUPPORT}. */ + public void setDirectoryMetaData( + String displayName, + String directoryType, + String accountType, + String accountName, + int exportSupport) { + mDirectoryDisplayName = displayName; + mDirectoryType = directoryType; + mDirectoryAccountType = accountType; + mDirectoryAccountName = accountName; + mDirectoryExportSupport = exportSupport; + } + + /** + * Returns the URI for the contact that contains both the lookup key and the ID. This is the best + * URI to reference a contact. For directory contacts, this is the same a the URI as returned by + * {@link #getUri()} + */ + public Uri getLookupUri() { + return mLookupUri; + } + + public String getLookupKey() { + return mLookupKey; + } + + /** + * Returns the contact Uri that was passed to the provider to make the query. This is the same as + * the requested Uri, unless the requested Uri doesn't specify a Contact: If it either references + * a Raw-Contact or a Person (a pre-Eclair style Uri), this Uri will always reference the full + * aggregate contact. + */ + public Uri getUri() { + return mUri; + } + + /** Returns the contact ID. */ + @VisibleForTesting + public long getId() { + return mId; + } + + /** + * @return true when an exception happened during loading, in which case {@link #getException} + * returns the actual exception object. + */ + public boolean isError() { + return mStatus == Status.ERROR; + } + + public Exception getException() { + return mException; + } + + /** @return true if the specified contact is successfully loaded. */ + public boolean isLoaded() { + return mStatus == Status.LOADED; + } + + public long getNameRawContactId() { + return mNameRawContactId; + } + + public int getDisplayNameSource() { + return mDisplayNameSource; + } + + public long getPhotoId() { + return mPhotoId; + } + + public String getPhotoUri() { + return mPhotoUri; + } + + public String getDisplayName() { + return mDisplayName; + } + + public boolean getStarred() { + return mStarred; + } + + public Integer getPresence() { + return mPresence; + } + + /** + * This can return non-null invitable account types only if the {@link ContactLoader} was + * configured to load invitable account types in its constructor. + */ + public ImmutableList getInvitableAccountTypes() { + return mInvitableAccountTypes; + } + + /* package */ void setInvitableAccountTypes(ImmutableList accountTypes) { + mInvitableAccountTypes = accountTypes; + } + + public ImmutableList getRawContacts() { + return mRawContacts; + } + + /* package */ void setRawContacts(ImmutableList rawContacts) { + mRawContacts = rawContacts; + } + + public long getDirectoryId() { + return mDirectoryId; + } + + public boolean isDirectoryEntry() { + return mDirectoryId != -1 + && mDirectoryId != Directory.DEFAULT + && mDirectoryId != Directory.LOCAL_INVISIBLE; + } + + /* package */ void setPhotoBinaryData(byte[] photoBinaryData) { + mPhotoBinaryData = photoBinaryData; + } + + public byte[] getThumbnailPhotoBinaryData() { + return mThumbnailPhotoBinaryData; + } + + /* package */ void setThumbnailPhotoBinaryData(byte[] photoBinaryData) { + mThumbnailPhotoBinaryData = photoBinaryData; + } + + public ArrayList getContentValues() { + if (mRawContacts.size() != 1) { + throw new IllegalStateException("Cannot extract content values from an aggregated contact"); + } + + RawContact rawContact = mRawContacts.get(0); + ArrayList result = rawContact.getContentValues(); + + // If the photo was loaded using the URI, create an entry for the photo + // binary data. + if (mPhotoId == 0 && mPhotoBinaryData != null) { + ContentValues photo = new ContentValues(); + photo.put(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE); + photo.put(Photo.PHOTO, mPhotoBinaryData); + result.add(photo); + } + + return result; + } + + /** + * This can return non-null group meta-data only if the {@link ContactLoader} was configured to + * load group metadata in its constructor. + */ + public ImmutableList getGroupMetaData() { + return mGroups; + } + + /* package */ void setGroupMetaData(ImmutableList groups) { + mGroups = groups; + } + + public boolean isUserProfile() { + return mIsUserProfile; + } + + @Override + public String toString() { + return "{requested=" + + mRequestedUri + + ",lookupkey=" + + mLookupKey + + ",uri=" + + mUri + + ",status=" + + mStatus + + "}"; + } + + private enum Status { + /** Contact is successfully loaded */ + LOADED, + /** There was an error loading the contact */ + ERROR, + /** Contact is not found */ + NOT_FOUND, + } +} diff --git a/java/com/android/contacts/common/model/ContactLoader.java b/java/com/android/contacts/common/model/ContactLoader.java new file mode 100644 index 000000000..eb16bffcd --- /dev/null +++ b/java/com/android/contacts/common/model/ContactLoader.java @@ -0,0 +1,998 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.contacts.common.model; + +import android.content.AsyncTaskLoader; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.res.AssetFileDescriptor; +import android.content.res.Resources; +import android.database.Cursor; +import android.net.Uri; +import android.provider.ContactsContract; +import android.provider.ContactsContract.CommonDataKinds.GroupMembership; +import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.Data; +import android.provider.ContactsContract.Directory; +import android.provider.ContactsContract.Groups; +import android.provider.ContactsContract.RawContacts; +import android.text.TextUtils; +import android.util.Log; +import com.android.contacts.common.GeoUtil; +import com.android.contacts.common.GroupMetaData; +import com.android.contacts.common.model.account.AccountType; +import com.android.contacts.common.model.account.AccountTypeWithDataSet; +import com.android.contacts.common.model.dataitem.DataItem; +import com.android.contacts.common.model.dataitem.PhoneDataItem; +import com.android.contacts.common.model.dataitem.PhotoDataItem; +import com.android.contacts.common.util.Constants; +import com.android.contacts.common.util.ContactLoaderUtils; +import com.android.contacts.common.util.UriUtils; +import com.android.dialer.compat.CompatUtils; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +/** Loads a single Contact and all it constituent RawContacts. */ +public class ContactLoader extends AsyncTaskLoader { + + private static final String TAG = ContactLoader.class.getSimpleName(); + + /** A short-lived cache that can be set by {@link #cacheResult()} */ + private static Contact sCachedResult = null; + + private final Uri mRequestedUri; + private final Set mNotifiedRawContactIds = Sets.newHashSet(); + private Uri mLookupUri; + private boolean mLoadGroupMetaData; + private boolean mLoadInvitableAccountTypes; + private boolean mPostViewNotification; + private boolean mComputeFormattedPhoneNumber; + private Contact mContact; + private ForceLoadContentObserver mObserver; + + public ContactLoader(Context context, Uri lookupUri, boolean postViewNotification) { + this(context, lookupUri, false, false, postViewNotification, false); + } + + public ContactLoader( + Context context, + Uri lookupUri, + boolean loadGroupMetaData, + boolean loadInvitableAccountTypes, + boolean postViewNotification, + boolean computeFormattedPhoneNumber) { + super(context); + mLookupUri = lookupUri; + mRequestedUri = lookupUri; + mLoadGroupMetaData = loadGroupMetaData; + mLoadInvitableAccountTypes = loadInvitableAccountTypes; + mPostViewNotification = postViewNotification; + mComputeFormattedPhoneNumber = computeFormattedPhoneNumber; + } + + /** + * Parses a {@link Contact} stored as a JSON string in a lookup URI. + * + * @param lookupUri The contact information to parse . + * @return The parsed {@code Contact} information. + */ + public static Contact parseEncodedContactEntity(Uri lookupUri) { + try { + return loadEncodedContactEntity(lookupUri, lookupUri); + } catch (JSONException je) { + return null; + } + } + + private static Contact loadEncodedContactEntity(Uri uri, Uri lookupUri) throws JSONException { + final String jsonString = uri.getEncodedFragment(); + final JSONObject json = new JSONObject(jsonString); + + final long directoryId = + Long.valueOf(uri.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY)); + + final String displayName = json.optString(Contacts.DISPLAY_NAME); + final String altDisplayName = json.optString(Contacts.DISPLAY_NAME_ALTERNATIVE, displayName); + final int displayNameSource = json.getInt(Contacts.DISPLAY_NAME_SOURCE); + final String photoUri = json.optString(Contacts.PHOTO_URI, null); + final Contact contact = + new Contact( + uri, + uri, + lookupUri, + directoryId, + null /* lookupKey */, + -1 /* id */, + -1 /* nameRawContactId */, + displayNameSource, + 0 /* photoId */, + photoUri, + displayName, + altDisplayName, + null /* phoneticName */, + false /* starred */, + null /* presence */, + false /* sendToVoicemail */, + null /* customRingtone */, + false /* isUserProfile */); + + final String accountName = json.optString(RawContacts.ACCOUNT_NAME, null); + final String directoryName = uri.getQueryParameter(Directory.DISPLAY_NAME); + if (accountName != null) { + final String accountType = json.getString(RawContacts.ACCOUNT_TYPE); + contact.setDirectoryMetaData( + directoryName, + null, + accountName, + accountType, + json.optInt(Directory.EXPORT_SUPPORT, Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY)); + } else { + contact.setDirectoryMetaData( + directoryName, + null, + null, + null, + json.optInt(Directory.EXPORT_SUPPORT, Directory.EXPORT_SUPPORT_ANY_ACCOUNT)); + } + + final ContentValues values = new ContentValues(); + values.put(Data._ID, -1); + values.put(Data.CONTACT_ID, -1); + final RawContact rawContact = new RawContact(values); + + final JSONObject items = json.getJSONObject(Contacts.CONTENT_ITEM_TYPE); + final Iterator keys = items.keys(); + while (keys.hasNext()) { + final String mimetype = (String) keys.next(); + + // Could be single object or array. + final JSONObject obj = items.optJSONObject(mimetype); + if (obj == null) { + final JSONArray array = items.getJSONArray(mimetype); + for (int i = 0; i < array.length(); i++) { + final JSONObject item = array.getJSONObject(i); + processOneRecord(rawContact, item, mimetype); + } + } else { + processOneRecord(rawContact, obj, mimetype); + } + } + + contact.setRawContacts(new ImmutableList.Builder().add(rawContact).build()); + return contact; + } + + private static void processOneRecord(RawContact rawContact, JSONObject item, String mimetype) + throws JSONException { + final ContentValues itemValues = new ContentValues(); + itemValues.put(Data.MIMETYPE, mimetype); + itemValues.put(Data._ID, -1); + + final Iterator iterator = item.keys(); + while (iterator.hasNext()) { + String name = (String) iterator.next(); + final Object o = item.get(name); + if (o instanceof String) { + itemValues.put(name, (String) o); + } else if (o instanceof Integer) { + itemValues.put(name, (Integer) o); + } + } + rawContact.addDataItemValues(itemValues); + } + + @Override + public Contact loadInBackground() { + Log.e(TAG, "loadInBackground=" + mLookupUri); + try { + final ContentResolver resolver = getContext().getContentResolver(); + final Uri uriCurrentFormat = ContactLoaderUtils.ensureIsContactUri(resolver, mLookupUri); + final Contact cachedResult = sCachedResult; + sCachedResult = null; + // Is this the same Uri as what we had before already? In that case, reuse that result + final Contact result; + final boolean resultIsCached; + if (cachedResult != null && UriUtils.areEqual(cachedResult.getLookupUri(), mLookupUri)) { + // We are using a cached result from earlier. Below, we should make sure + // we are not doing any more network or disc accesses + result = new Contact(mRequestedUri, cachedResult); + resultIsCached = true; + } else { + if (uriCurrentFormat.getLastPathSegment().equals(Constants.LOOKUP_URI_ENCODED)) { + result = loadEncodedContactEntity(uriCurrentFormat, mLookupUri); + } else { + result = loadContactEntity(resolver, uriCurrentFormat); + } + resultIsCached = false; + } + if (result.isLoaded()) { + if (result.isDirectoryEntry()) { + if (!resultIsCached) { + loadDirectoryMetaData(result); + } + } else if (mLoadGroupMetaData) { + if (result.getGroupMetaData() == null) { + loadGroupMetaData(result); + } + } + if (mComputeFormattedPhoneNumber) { + computeFormattedPhoneNumbers(result); + } + if (!resultIsCached) { + loadPhotoBinaryData(result); + } + + // Note ME profile should never have "Add connection" + if (mLoadInvitableAccountTypes && result.getInvitableAccountTypes() == null) { + loadInvitableAccountTypes(result); + } + } + return result; + } catch (Exception e) { + Log.e(TAG, "Error loading the contact: " + mLookupUri, e); + return Contact.forError(mRequestedUri, e); + } + } + + private Contact loadContactEntity(ContentResolver resolver, Uri contactUri) { + Uri entityUri = Uri.withAppendedPath(contactUri, Contacts.Entity.CONTENT_DIRECTORY); + Cursor cursor = + resolver.query(entityUri, ContactQuery.COLUMNS, null, null, Contacts.Entity.RAW_CONTACT_ID); + if (cursor == null) { + Log.e(TAG, "No cursor returned in loadContactEntity"); + return Contact.forNotFound(mRequestedUri); + } + + try { + if (!cursor.moveToFirst()) { + cursor.close(); + return Contact.forNotFound(mRequestedUri); + } + + // Create the loaded contact starting with the header data. + Contact contact = loadContactHeaderData(cursor, contactUri); + + // Fill in the raw contacts, which is wrapped in an Entity and any + // status data. Initially, result has empty entities and statuses. + long currentRawContactId = -1; + RawContact rawContact = null; + ImmutableList.Builder rawContactsBuilder = + new ImmutableList.Builder(); + do { + long rawContactId = cursor.getLong(ContactQuery.RAW_CONTACT_ID); + if (rawContactId != currentRawContactId) { + // First time to see this raw contact id, so create a new entity, and + // add it to the result's entities. + currentRawContactId = rawContactId; + rawContact = new RawContact(loadRawContactValues(cursor)); + rawContactsBuilder.add(rawContact); + } + if (!cursor.isNull(ContactQuery.DATA_ID)) { + ContentValues data = loadDataValues(cursor); + rawContact.addDataItemValues(data); + } + } while (cursor.moveToNext()); + + contact.setRawContacts(rawContactsBuilder.build()); + + return contact; + } finally { + cursor.close(); + } + } + + /** + * Looks for the photo data item in entities. If found, a thumbnail will be stored. A larger photo + * will also be stored if available. + */ + private void loadPhotoBinaryData(Contact contactData) { + loadThumbnailBinaryData(contactData); + + // Try to load the large photo from a file using the photo URI. + String photoUri = contactData.getPhotoUri(); + if (photoUri != null) { + try { + final InputStream inputStream; + final AssetFileDescriptor fd; + final Uri uri = Uri.parse(photoUri); + final String scheme = uri.getScheme(); + if ("http".equals(scheme) || "https".equals(scheme)) { + // Support HTTP urls that might come from extended directories + inputStream = new URL(photoUri).openStream(); + fd = null; + } else { + fd = getContext().getContentResolver().openAssetFileDescriptor(uri, "r"); + inputStream = fd.createInputStream(); + } + byte[] buffer = new byte[16 * 1024]; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + int size; + while ((size = inputStream.read(buffer)) != -1) { + baos.write(buffer, 0, size); + } + contactData.setPhotoBinaryData(baos.toByteArray()); + } finally { + inputStream.close(); + if (fd != null) { + fd.close(); + } + } + return; + } catch (IOException ioe) { + // Just fall back to the case below. + } + } + + // If we couldn't load from a file, fall back to the data blob. + contactData.setPhotoBinaryData(contactData.getThumbnailPhotoBinaryData()); + } + + private void loadThumbnailBinaryData(Contact contactData) { + final long photoId = contactData.getPhotoId(); + if (photoId <= 0) { + // No photo ID + return; + } + + for (RawContact rawContact : contactData.getRawContacts()) { + for (DataItem dataItem : rawContact.getDataItems()) { + if (dataItem.getId() == photoId) { + if (!(dataItem instanceof PhotoDataItem)) { + break; + } + + final PhotoDataItem photo = (PhotoDataItem) dataItem; + contactData.setThumbnailPhotoBinaryData(photo.getPhoto()); + break; + } + } + } + } + + /** Sets the "invitable" account types to {@link Contact#mInvitableAccountTypes}. */ + private void loadInvitableAccountTypes(Contact contactData) { + final ImmutableList.Builder resultListBuilder = + new ImmutableList.Builder(); + if (!contactData.isUserProfile()) { + Map invitables = + AccountTypeManager.getInstance(getContext()).getUsableInvitableAccountTypes(); + if (!invitables.isEmpty()) { + final Map resultMap = Maps.newHashMap(invitables); + + // Remove the ones that already have a raw contact in the current contact + for (RawContact rawContact : contactData.getRawContacts()) { + final AccountTypeWithDataSet type = + AccountTypeWithDataSet.get( + rawContact.getAccountTypeString(), rawContact.getDataSet()); + resultMap.remove(type); + } + + resultListBuilder.addAll(resultMap.values()); + } + } + + // Set to mInvitableAccountTypes + contactData.setInvitableAccountTypes(resultListBuilder.build()); + } + + /** Extracts Contact level columns from the cursor. */ + private Contact loadContactHeaderData(final Cursor cursor, Uri contactUri) { + final String directoryParameter = + contactUri.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY); + final long directoryId = + directoryParameter == null ? Directory.DEFAULT : Long.parseLong(directoryParameter); + final long contactId = cursor.getLong(ContactQuery.CONTACT_ID); + final String lookupKey = cursor.getString(ContactQuery.LOOKUP_KEY); + final long nameRawContactId = cursor.getLong(ContactQuery.NAME_RAW_CONTACT_ID); + final int displayNameSource = cursor.getInt(ContactQuery.DISPLAY_NAME_SOURCE); + final String displayName = cursor.getString(ContactQuery.DISPLAY_NAME); + final String altDisplayName = cursor.getString(ContactQuery.ALT_DISPLAY_NAME); + final String phoneticName = cursor.getString(ContactQuery.PHONETIC_NAME); + final long photoId = cursor.getLong(ContactQuery.PHOTO_ID); + final String photoUri = cursor.getString(ContactQuery.PHOTO_URI); + final boolean starred = cursor.getInt(ContactQuery.STARRED) != 0; + final Integer presence = + cursor.isNull(ContactQuery.CONTACT_PRESENCE) + ? null + : cursor.getInt(ContactQuery.CONTACT_PRESENCE); + final boolean sendToVoicemail = cursor.getInt(ContactQuery.SEND_TO_VOICEMAIL) == 1; + final String customRingtone = cursor.getString(ContactQuery.CUSTOM_RINGTONE); + final boolean isUserProfile = cursor.getInt(ContactQuery.IS_USER_PROFILE) == 1; + + Uri lookupUri; + if (directoryId == Directory.DEFAULT || directoryId == Directory.LOCAL_INVISIBLE) { + lookupUri = + ContentUris.withAppendedId( + Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey), contactId); + } else { + lookupUri = contactUri; + } + + return new Contact( + mRequestedUri, + contactUri, + lookupUri, + directoryId, + lookupKey, + contactId, + nameRawContactId, + displayNameSource, + photoId, + photoUri, + displayName, + altDisplayName, + phoneticName, + starred, + presence, + sendToVoicemail, + customRingtone, + isUserProfile); + } + + /** Extracts RawContact level columns from the cursor. */ + private ContentValues loadRawContactValues(Cursor cursor) { + ContentValues cv = new ContentValues(); + + cv.put(RawContacts._ID, cursor.getLong(ContactQuery.RAW_CONTACT_ID)); + + cursorColumnToContentValues(cursor, cv, ContactQuery.ACCOUNT_NAME); + cursorColumnToContentValues(cursor, cv, ContactQuery.ACCOUNT_TYPE); + cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SET); + cursorColumnToContentValues(cursor, cv, ContactQuery.DIRTY); + cursorColumnToContentValues(cursor, cv, ContactQuery.VERSION); + cursorColumnToContentValues(cursor, cv, ContactQuery.SOURCE_ID); + cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC1); + cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC2); + cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC3); + cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC4); + cursorColumnToContentValues(cursor, cv, ContactQuery.DELETED); + cursorColumnToContentValues(cursor, cv, ContactQuery.CONTACT_ID); + cursorColumnToContentValues(cursor, cv, ContactQuery.STARRED); + + return cv; + } + + /** Extracts Data level columns from the cursor. */ + private ContentValues loadDataValues(Cursor cursor) { + ContentValues cv = new ContentValues(); + + cv.put(Data._ID, cursor.getLong(ContactQuery.DATA_ID)); + + cursorColumnToContentValues(cursor, cv, ContactQuery.DATA1); + cursorColumnToContentValues(cursor, cv, ContactQuery.DATA2); + cursorColumnToContentValues(cursor, cv, ContactQuery.DATA3); + cursorColumnToContentValues(cursor, cv, ContactQuery.DATA4); + cursorColumnToContentValues(cursor, cv, ContactQuery.DATA5); + cursorColumnToContentValues(cursor, cv, ContactQuery.DATA6); + cursorColumnToContentValues(cursor, cv, ContactQuery.DATA7); + cursorColumnToContentValues(cursor, cv, ContactQuery.DATA8); + cursorColumnToContentValues(cursor, cv, ContactQuery.DATA9); + cursorColumnToContentValues(cursor, cv, ContactQuery.DATA10); + cursorColumnToContentValues(cursor, cv, ContactQuery.DATA11); + cursorColumnToContentValues(cursor, cv, ContactQuery.DATA12); + cursorColumnToContentValues(cursor, cv, ContactQuery.DATA13); + cursorColumnToContentValues(cursor, cv, ContactQuery.DATA14); + cursorColumnToContentValues(cursor, cv, ContactQuery.DATA15); + cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC1); + cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC2); + cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC3); + cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC4); + cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_VERSION); + cursorColumnToContentValues(cursor, cv, ContactQuery.IS_PRIMARY); + cursorColumnToContentValues(cursor, cv, ContactQuery.IS_SUPERPRIMARY); + cursorColumnToContentValues(cursor, cv, ContactQuery.MIMETYPE); + cursorColumnToContentValues(cursor, cv, ContactQuery.GROUP_SOURCE_ID); + cursorColumnToContentValues(cursor, cv, ContactQuery.CHAT_CAPABILITY); + cursorColumnToContentValues(cursor, cv, ContactQuery.TIMES_USED); + cursorColumnToContentValues(cursor, cv, ContactQuery.LAST_TIME_USED); + if (CompatUtils.isMarshmallowCompatible()) { + cursorColumnToContentValues(cursor, cv, ContactQuery.CARRIER_PRESENCE); + } + + return cv; + } + + private void cursorColumnToContentValues(Cursor cursor, ContentValues values, int index) { + switch (cursor.getType(index)) { + case Cursor.FIELD_TYPE_NULL: + // don't put anything in the content values + break; + case Cursor.FIELD_TYPE_INTEGER: + values.put(ContactQuery.COLUMNS[index], cursor.getLong(index)); + break; + case Cursor.FIELD_TYPE_STRING: + values.put(ContactQuery.COLUMNS[index], cursor.getString(index)); + break; + case Cursor.FIELD_TYPE_BLOB: + values.put(ContactQuery.COLUMNS[index], cursor.getBlob(index)); + break; + default: + throw new IllegalStateException("Invalid or unhandled data type"); + } + } + + private void loadDirectoryMetaData(Contact result) { + long directoryId = result.getDirectoryId(); + + Cursor cursor = + getContext() + .getContentResolver() + .query( + ContentUris.withAppendedId(Directory.CONTENT_URI, directoryId), + DirectoryQuery.COLUMNS, + null, + null, + null); + if (cursor == null) { + return; + } + try { + if (cursor.moveToFirst()) { + final String displayName = cursor.getString(DirectoryQuery.DISPLAY_NAME); + final String packageName = cursor.getString(DirectoryQuery.PACKAGE_NAME); + final int typeResourceId = cursor.getInt(DirectoryQuery.TYPE_RESOURCE_ID); + final String accountType = cursor.getString(DirectoryQuery.ACCOUNT_TYPE); + final String accountName = cursor.getString(DirectoryQuery.ACCOUNT_NAME); + final int exportSupport = cursor.getInt(DirectoryQuery.EXPORT_SUPPORT); + String directoryType = null; + if (!TextUtils.isEmpty(packageName)) { + PackageManager pm = getContext().getPackageManager(); + try { + Resources resources = pm.getResourcesForApplication(packageName); + directoryType = resources.getString(typeResourceId); + } catch (NameNotFoundException e) { + Log.w( + TAG, "Contact directory resource not found: " + packageName + "." + typeResourceId); + } + } + + result.setDirectoryMetaData( + displayName, directoryType, accountType, accountName, exportSupport); + } + } finally { + cursor.close(); + } + } + + /** + * Loads groups meta-data for all groups associated with all constituent raw contacts' accounts. + */ + private void loadGroupMetaData(Contact result) { + StringBuilder selection = new StringBuilder(); + ArrayList selectionArgs = new ArrayList(); + final HashSet accountsSeen = new HashSet<>(); + for (RawContact rawContact : result.getRawContacts()) { + final String accountName = rawContact.getAccountName(); + final String accountType = rawContact.getAccountTypeString(); + final String dataSet = rawContact.getDataSet(); + final AccountKey accountKey = new AccountKey(accountName, accountType, dataSet); + if (accountName != null && accountType != null && !accountsSeen.contains(accountKey)) { + accountsSeen.add(accountKey); + if (selection.length() != 0) { + selection.append(" OR "); + } + selection.append("(" + Groups.ACCOUNT_NAME + "=? AND " + Groups.ACCOUNT_TYPE + "=?"); + selectionArgs.add(accountName); + selectionArgs.add(accountType); + + if (dataSet != null) { + selection.append(" AND " + Groups.DATA_SET + "=?"); + selectionArgs.add(dataSet); + } else { + selection.append(" AND " + Groups.DATA_SET + " IS NULL"); + } + selection.append(")"); + } + } + final ImmutableList.Builder groupListBuilder = + new ImmutableList.Builder(); + final Cursor cursor = + getContext() + .getContentResolver() + .query( + Groups.CONTENT_URI, + GroupQuery.COLUMNS, + selection.toString(), + selectionArgs.toArray(new String[0]), + null); + if (cursor != null) { + try { + while (cursor.moveToNext()) { + final String accountName = cursor.getString(GroupQuery.ACCOUNT_NAME); + final String accountType = cursor.getString(GroupQuery.ACCOUNT_TYPE); + final String dataSet = cursor.getString(GroupQuery.DATA_SET); + final long groupId = cursor.getLong(GroupQuery.ID); + final String title = cursor.getString(GroupQuery.TITLE); + final boolean defaultGroup = + !cursor.isNull(GroupQuery.AUTO_ADD) && cursor.getInt(GroupQuery.AUTO_ADD) != 0; + final boolean favorites = + !cursor.isNull(GroupQuery.FAVORITES) && cursor.getInt(GroupQuery.FAVORITES) != 0; + + groupListBuilder.add( + new GroupMetaData( + accountName, accountType, dataSet, groupId, title, defaultGroup, favorites)); + } + } finally { + cursor.close(); + } + } + result.setGroupMetaData(groupListBuilder.build()); + } + + /** + * Iterates over all data items that represent phone numbers are tries to calculate a formatted + * number. This function can safely be called several times as no unformatted data is overwritten + */ + private void computeFormattedPhoneNumbers(Contact contactData) { + final String countryIso = GeoUtil.getCurrentCountryIso(getContext()); + final ImmutableList rawContacts = contactData.getRawContacts(); + final int rawContactCount = rawContacts.size(); + for (int rawContactIndex = 0; rawContactIndex < rawContactCount; rawContactIndex++) { + final RawContact rawContact = rawContacts.get(rawContactIndex); + final List dataItems = rawContact.getDataItems(); + final int dataCount = dataItems.size(); + for (int dataIndex = 0; dataIndex < dataCount; dataIndex++) { + final DataItem dataItem = dataItems.get(dataIndex); + if (dataItem instanceof PhoneDataItem) { + final PhoneDataItem phoneDataItem = (PhoneDataItem) dataItem; + phoneDataItem.computeFormattedPhoneNumber(countryIso); + } + } + } + } + + @Override + public void deliverResult(Contact result) { + unregisterObserver(); + + // The creator isn't interested in any further updates + if (isReset() || result == null) { + return; + } + + mContact = result; + + if (result.isLoaded()) { + mLookupUri = result.getLookupUri(); + + if (!result.isDirectoryEntry()) { + Log.i(TAG, "Registering content observer for " + mLookupUri); + if (mObserver == null) { + mObserver = new ForceLoadContentObserver(); + } + getContext().getContentResolver().registerContentObserver(mLookupUri, true, mObserver); + } + + if (mPostViewNotification) { + // inform the source of the data that this contact is being looked at + postViewNotificationToSyncAdapter(); + } + } + + super.deliverResult(mContact); + } + + /** + * Posts a message to the contributing sync adapters that have opted-in, notifying them that the + * contact has just been loaded + */ + private void postViewNotificationToSyncAdapter() { + Context context = getContext(); + for (RawContact rawContact : mContact.getRawContacts()) { + final long rawContactId = rawContact.getId(); + if (mNotifiedRawContactIds.contains(rawContactId)) { + continue; // Already notified for this raw contact. + } + mNotifiedRawContactIds.add(rawContactId); + final AccountType accountType = rawContact.getAccountType(context); + final String serviceName = accountType.getViewContactNotifyServiceClassName(); + final String servicePackageName = accountType.getViewContactNotifyServicePackageName(); + if (!TextUtils.isEmpty(serviceName) && !TextUtils.isEmpty(servicePackageName)) { + final Uri uri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId); + final Intent intent = new Intent(); + intent.setClassName(servicePackageName, serviceName); + intent.setAction(Intent.ACTION_VIEW); + intent.setDataAndType(uri, RawContacts.CONTENT_ITEM_TYPE); + try { + context.startService(intent); + } catch (Exception e) { + Log.e(TAG, "Error sending message to source-app", e); + } + } + } + } + + private void unregisterObserver() { + if (mObserver != null) { + getContext().getContentResolver().unregisterContentObserver(mObserver); + mObserver = null; + } + } + + public Uri getLookupUri() { + return mLookupUri; + } + + public void setLookupUri(Uri lookupUri) { + mLookupUri = lookupUri; + } + + @Override + protected void onStartLoading() { + if (mContact != null) { + deliverResult(mContact); + } + + if (takeContentChanged() || mContact == null) { + forceLoad(); + } + } + + @Override + protected void onStopLoading() { + cancelLoad(); + } + + @Override + protected void onReset() { + super.onReset(); + cancelLoad(); + unregisterObserver(); + mContact = null; + } + + /** + * Projection used for the query that loads all data for the entire contact (except for social + * stream items). + */ + private static class ContactQuery { + + public static final int NAME_RAW_CONTACT_ID = 0; + public static final int DISPLAY_NAME_SOURCE = 1; + public static final int LOOKUP_KEY = 2; + public static final int DISPLAY_NAME = 3; + public static final int ALT_DISPLAY_NAME = 4; + public static final int PHONETIC_NAME = 5; + public static final int PHOTO_ID = 6; + public static final int STARRED = 7; + public static final int CONTACT_PRESENCE = 8; + public static final int CONTACT_STATUS = 9; + public static final int CONTACT_STATUS_TIMESTAMP = 10; + public static final int CONTACT_STATUS_RES_PACKAGE = 11; + public static final int CONTACT_STATUS_LABEL = 12; + public static final int CONTACT_ID = 13; + public static final int RAW_CONTACT_ID = 14; + public static final int ACCOUNT_NAME = 15; + public static final int ACCOUNT_TYPE = 16; + public static final int DATA_SET = 17; + public static final int DIRTY = 18; + public static final int VERSION = 19; + public static final int SOURCE_ID = 20; + public static final int SYNC1 = 21; + public static final int SYNC2 = 22; + public static final int SYNC3 = 23; + public static final int SYNC4 = 24; + public static final int DELETED = 25; + public static final int DATA_ID = 26; + public static final int DATA1 = 27; + public static final int DATA2 = 28; + public static final int DATA3 = 29; + public static final int DATA4 = 30; + public static final int DATA5 = 31; + public static final int DATA6 = 32; + public static final int DATA7 = 33; + public static final int DATA8 = 34; + public static final int DATA9 = 35; + public static final int DATA10 = 36; + public static final int DATA11 = 37; + public static final int DATA12 = 38; + public static final int DATA13 = 39; + public static final int DATA14 = 40; + public static final int DATA15 = 41; + public static final int DATA_SYNC1 = 42; + public static final int DATA_SYNC2 = 43; + public static final int DATA_SYNC3 = 44; + public static final int DATA_SYNC4 = 45; + public static final int DATA_VERSION = 46; + public static final int IS_PRIMARY = 47; + public static final int IS_SUPERPRIMARY = 48; + public static final int MIMETYPE = 49; + public static final int GROUP_SOURCE_ID = 50; + public static final int PRESENCE = 51; + public static final int CHAT_CAPABILITY = 52; + public static final int STATUS = 53; + public static final int STATUS_RES_PACKAGE = 54; + public static final int STATUS_ICON = 55; + public static final int STATUS_LABEL = 56; + public static final int STATUS_TIMESTAMP = 57; + public static final int PHOTO_URI = 58; + public static final int SEND_TO_VOICEMAIL = 59; + public static final int CUSTOM_RINGTONE = 60; + public static final int IS_USER_PROFILE = 61; + public static final int TIMES_USED = 62; + public static final int LAST_TIME_USED = 63; + public static final int CARRIER_PRESENCE = 64; + static final String[] COLUMNS_INTERNAL = + new String[] { + Contacts.NAME_RAW_CONTACT_ID, + Contacts.DISPLAY_NAME_SOURCE, + Contacts.LOOKUP_KEY, + Contacts.DISPLAY_NAME, + Contacts.DISPLAY_NAME_ALTERNATIVE, + Contacts.PHONETIC_NAME, + Contacts.PHOTO_ID, + Contacts.STARRED, + Contacts.CONTACT_PRESENCE, + Contacts.CONTACT_STATUS, + Contacts.CONTACT_STATUS_TIMESTAMP, + Contacts.CONTACT_STATUS_RES_PACKAGE, + Contacts.CONTACT_STATUS_LABEL, + Contacts.Entity.CONTACT_ID, + Contacts.Entity.RAW_CONTACT_ID, + RawContacts.ACCOUNT_NAME, + RawContacts.ACCOUNT_TYPE, + RawContacts.DATA_SET, + RawContacts.DIRTY, + RawContacts.VERSION, + RawContacts.SOURCE_ID, + RawContacts.SYNC1, + RawContacts.SYNC2, + RawContacts.SYNC3, + RawContacts.SYNC4, + RawContacts.DELETED, + Contacts.Entity.DATA_ID, + Data.DATA1, + Data.DATA2, + Data.DATA3, + Data.DATA4, + Data.DATA5, + Data.DATA6, + Data.DATA7, + Data.DATA8, + Data.DATA9, + Data.DATA10, + Data.DATA11, + Data.DATA12, + Data.DATA13, + Data.DATA14, + Data.DATA15, + Data.SYNC1, + Data.SYNC2, + Data.SYNC3, + Data.SYNC4, + Data.DATA_VERSION, + Data.IS_PRIMARY, + Data.IS_SUPER_PRIMARY, + Data.MIMETYPE, + GroupMembership.GROUP_SOURCE_ID, + Data.PRESENCE, + Data.CHAT_CAPABILITY, + Data.STATUS, + Data.STATUS_RES_PACKAGE, + Data.STATUS_ICON, + Data.STATUS_LABEL, + Data.STATUS_TIMESTAMP, + Contacts.PHOTO_URI, + Contacts.SEND_TO_VOICEMAIL, + Contacts.CUSTOM_RINGTONE, + Contacts.IS_USER_PROFILE, + Data.TIMES_USED, + Data.LAST_TIME_USED + }; + static final String[] COLUMNS; + + static { + List projectionList = Lists.newArrayList(COLUMNS_INTERNAL); + if (CompatUtils.isMarshmallowCompatible()) { + projectionList.add(Data.CARRIER_PRESENCE); + } + COLUMNS = projectionList.toArray(new String[projectionList.size()]); + } + } + + /** Projection used for the query that loads all data for the entire contact. */ + private static class DirectoryQuery { + + public static final int DISPLAY_NAME = 0; + public static final int PACKAGE_NAME = 1; + public static final int TYPE_RESOURCE_ID = 2; + public static final int ACCOUNT_TYPE = 3; + public static final int ACCOUNT_NAME = 4; + public static final int EXPORT_SUPPORT = 5; + static final String[] COLUMNS = + new String[] { + Directory.DISPLAY_NAME, + Directory.PACKAGE_NAME, + Directory.TYPE_RESOURCE_ID, + Directory.ACCOUNT_TYPE, + Directory.ACCOUNT_NAME, + Directory.EXPORT_SUPPORT, + }; + } + + private static class GroupQuery { + + public static final int ACCOUNT_NAME = 0; + public static final int ACCOUNT_TYPE = 1; + public static final int DATA_SET = 2; + public static final int ID = 3; + public static final int TITLE = 4; + public static final int AUTO_ADD = 5; + public static final int FAVORITES = 6; + static final String[] COLUMNS = + new String[] { + Groups.ACCOUNT_NAME, + Groups.ACCOUNT_TYPE, + Groups.DATA_SET, + Groups._ID, + Groups.TITLE, + Groups.AUTO_ADD, + Groups.FAVORITES, + }; + } + + private static class AccountKey { + + private final String mAccountName; + private final String mAccountType; + private final String mDataSet; + + public AccountKey(String accountName, String accountType, String dataSet) { + mAccountName = accountName; + mAccountType = accountType; + mDataSet = dataSet; + } + + @Override + public int hashCode() { + return Objects.hash(mAccountName, mAccountType, mDataSet); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof AccountKey)) { + return false; + } + final AccountKey other = (AccountKey) obj; + return Objects.equals(mAccountName, other.mAccountName) + && Objects.equals(mAccountType, other.mAccountType) + && Objects.equals(mDataSet, other.mDataSet); + } + } +} diff --git a/java/com/android/contacts/common/model/RawContact.java b/java/com/android/contacts/common/model/RawContact.java new file mode 100644 index 000000000..9efc8a878 --- /dev/null +++ b/java/com/android/contacts/common/model/RawContact.java @@ -0,0 +1,351 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model; + +import android.content.ContentValues; +import android.content.Context; +import android.content.Entity; +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; +import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.Data; +import android.provider.ContactsContract.RawContacts; +import com.android.contacts.common.model.account.AccountType; +import com.android.contacts.common.model.account.AccountWithDataSet; +import com.android.contacts.common.model.dataitem.DataItem; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * RawContact represents a single raw contact in the raw contacts database. It has specialized + * getters/setters for raw contact items, and also contains a collection of DataItem objects. A + * RawContact contains the information from a single account. + * + *

This allows RawContact objects to be thought of as a class with raw contact fields (like + * account type, name, data set, sync state, etc.) and a list of DataItem objects that represent + * contact information elements (like phone numbers, email, address, etc.). + */ +public final class RawContact implements Parcelable { + + /** Create for building the parcelable. */ + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public RawContact createFromParcel(Parcel parcel) { + return new RawContact(parcel); + } + + @Override + public RawContact[] newArray(int i) { + return new RawContact[i]; + } + }; + + private final ContentValues mValues; + private final ArrayList mDataItems; + private AccountTypeManager mAccountTypeManager; + + /** A RawContact object can be created with or without a context. */ + public RawContact() { + this(new ContentValues()); + } + + public RawContact(ContentValues values) { + mValues = values; + mDataItems = new ArrayList(); + } + + /** + * Constructor for the parcelable. + * + * @param parcel The parcel to de-serialize from. + */ + private RawContact(Parcel parcel) { + mValues = parcel.readParcelable(ContentValues.class.getClassLoader()); + mDataItems = new ArrayList<>(); + parcel.readTypedList(mDataItems, NamedDataItem.CREATOR); + } + + public static RawContact createFrom(Entity entity) { + final ContentValues values = entity.getEntityValues(); + final ArrayList subValues = entity.getSubValues(); + + RawContact rawContact = new RawContact(values); + for (Entity.NamedContentValues subValue : subValues) { + rawContact.addNamedDataItemValues(subValue.uri, subValue.values); + } + return rawContact; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int i) { + parcel.writeParcelable(mValues, i); + parcel.writeTypedList(mDataItems); + } + + public AccountTypeManager getAccountTypeManager(Context context) { + if (mAccountTypeManager == null) { + mAccountTypeManager = AccountTypeManager.getInstance(context); + } + return mAccountTypeManager; + } + + public ContentValues getValues() { + return mValues; + } + + /** Returns the id of the raw contact. */ + public Long getId() { + return getValues().getAsLong(RawContacts._ID); + } + + /** Returns the account name of the raw contact. */ + public String getAccountName() { + return getValues().getAsString(RawContacts.ACCOUNT_NAME); + } + + /** Returns the account type of the raw contact. */ + public String getAccountTypeString() { + return getValues().getAsString(RawContacts.ACCOUNT_TYPE); + } + + /** Returns the data set of the raw contact. */ + public String getDataSet() { + return getValues().getAsString(RawContacts.DATA_SET); + } + + public boolean isDirty() { + return getValues().getAsBoolean(RawContacts.DIRTY); + } + + public String getSourceId() { + return getValues().getAsString(RawContacts.SOURCE_ID); + } + + public String getSync1() { + return getValues().getAsString(RawContacts.SYNC1); + } + + public String getSync2() { + return getValues().getAsString(RawContacts.SYNC2); + } + + public String getSync3() { + return getValues().getAsString(RawContacts.SYNC3); + } + + public String getSync4() { + return getValues().getAsString(RawContacts.SYNC4); + } + + public boolean isDeleted() { + return getValues().getAsBoolean(RawContacts.DELETED); + } + + public long getContactId() { + return getValues().getAsLong(Contacts.Entity.CONTACT_ID); + } + + public boolean isStarred() { + return getValues().getAsBoolean(Contacts.STARRED); + } + + public AccountType getAccountType(Context context) { + return getAccountTypeManager(context).getAccountType(getAccountTypeString(), getDataSet()); + } + + /** + * Sets the account name, account type, and data set strings. Valid combinations for account-name, + * account-type, data-set 1) null, null, null (local account) 2) non-null, non-null, null (valid + * account without data-set) 3) non-null, non-null, non-null (valid account with data-set) + */ + private void setAccount(String accountName, String accountType, String dataSet) { + final ContentValues values = getValues(); + if (accountName == null) { + if (accountType == null && dataSet == null) { + // This is a local account + values.putNull(RawContacts.ACCOUNT_NAME); + values.putNull(RawContacts.ACCOUNT_TYPE); + values.putNull(RawContacts.DATA_SET); + return; + } + } else { + if (accountType != null) { + // This is a valid account, either with or without a dataSet. + values.put(RawContacts.ACCOUNT_NAME, accountName); + values.put(RawContacts.ACCOUNT_TYPE, accountType); + if (dataSet == null) { + values.putNull(RawContacts.DATA_SET); + } else { + values.put(RawContacts.DATA_SET, dataSet); + } + return; + } + } + throw new IllegalArgumentException( + "Not a valid combination of account name, type, and data set."); + } + + public void setAccount(AccountWithDataSet accountWithDataSet) { + if (accountWithDataSet != null) { + setAccount(accountWithDataSet.name, accountWithDataSet.type, accountWithDataSet.dataSet); + } else { + setAccount(null, null, null); + } + } + + public void setAccountToLocal() { + setAccount(null, null, null); + } + + /** Creates and inserts a DataItem object that wraps the content values, and returns it. */ + public void addDataItemValues(ContentValues values) { + addNamedDataItemValues(Data.CONTENT_URI, values); + } + + public NamedDataItem addNamedDataItemValues(Uri uri, ContentValues values) { + final NamedDataItem namedItem = new NamedDataItem(uri, values); + mDataItems.add(namedItem); + return namedItem; + } + + public ArrayList getContentValues() { + final ArrayList list = new ArrayList<>(mDataItems.size()); + for (NamedDataItem dataItem : mDataItems) { + if (Data.CONTENT_URI.equals(dataItem.mUri)) { + list.add(dataItem.mContentValues); + } + } + return list; + } + + public List getDataItems() { + final ArrayList list = new ArrayList<>(mDataItems.size()); + for (NamedDataItem dataItem : mDataItems) { + if (Data.CONTENT_URI.equals(dataItem.mUri)) { + list.add(DataItem.createFrom(dataItem.mContentValues)); + } + } + return list; + } + + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append("RawContact: ").append(mValues); + for (RawContact.NamedDataItem namedDataItem : mDataItems) { + sb.append("\n ").append(namedDataItem.mUri); + sb.append("\n -> ").append(namedDataItem.mContentValues); + } + return sb.toString(); + } + + @Override + public int hashCode() { + return Objects.hash(mValues, mDataItems); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + + RawContact other = (RawContact) obj; + return Objects.equals(mValues, other.mValues) && Objects.equals(mDataItems, other.mDataItems); + } + + public static final class NamedDataItem implements Parcelable { + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public NamedDataItem createFromParcel(Parcel parcel) { + return new NamedDataItem(parcel); + } + + @Override + public NamedDataItem[] newArray(int i) { + return new NamedDataItem[i]; + } + }; + public final Uri mUri; + // This use to be a DataItem. DataItem creation is now delayed until the point of request + // since there is no benefit to storing them here due to the multiple inheritance. + // Eventually instanceof still has to be used anyways to determine which sub-class of + // DataItem it is. And having parent DataItem's here makes it very difficult to serialize or + // parcelable. + // + // Instead of having a common DataItem super class, we should refactor this to be a generic + // Object where the object is a concrete class that no longer relies on ContentValues. + // (this will also make the classes easier to use). + // Since instanceof is used later anyways, having a list of Objects won't hurt and is no + // worse than having a DataItem. + public final ContentValues mContentValues; + + public NamedDataItem(Uri uri, ContentValues values) { + this.mUri = uri; + this.mContentValues = values; + } + + public NamedDataItem(Parcel parcel) { + this.mUri = parcel.readParcelable(Uri.class.getClassLoader()); + this.mContentValues = parcel.readParcelable(ContentValues.class.getClassLoader()); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int i) { + parcel.writeParcelable(mUri, i); + parcel.writeParcelable(mContentValues, i); + } + + @Override + public int hashCode() { + return Objects.hash(mUri, mContentValues); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + + final NamedDataItem other = (NamedDataItem) obj; + return Objects.equals(mUri, other.mUri) + && Objects.equals(mContentValues, other.mContentValues); + } + } +} diff --git a/java/com/android/contacts/common/model/account/AccountType.java b/java/com/android/contacts/common/model/account/AccountType.java new file mode 100644 index 000000000..1ae485a5f --- /dev/null +++ b/java/com/android/contacts/common/model/account/AccountType.java @@ -0,0 +1,501 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model.account; + +import android.content.ContentValues; +import android.content.Context; +import android.content.pm.PackageManager; +import android.graphics.drawable.Drawable; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; +import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.RawContacts; +import android.support.annotation.VisibleForTesting; +import android.util.ArrayMap; +import android.view.inputmethod.EditorInfo; +import android.widget.EditText; +import com.android.contacts.common.R; +import com.android.contacts.common.model.dataitem.DataKind; +import java.text.Collator; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; + +/** + * Internal structure that represents constraints and styles for a specific data source, such as the + * various data types they support, including details on how those types should be rendered and + * edited. + * + *

In the future this may be inflated from XML defined by a data source. + */ +public abstract class AccountType { + + private static final String TAG = "AccountType"; + /** {@link Comparator} to sort by {@link DataKind#weight}. */ + private static Comparator sWeightComparator = + new Comparator() { + @Override + public int compare(DataKind object1, DataKind object2) { + return object1.weight - object2.weight; + } + }; + /** The {@link RawContacts#ACCOUNT_TYPE} these constraints apply to. */ + public String accountType = null; + /** The {@link RawContacts#DATA_SET} these constraints apply to. */ + public String dataSet = null; + /** + * Package that resources should be loaded from. Will be null for embedded types, in which case + * resources are stored in this package itself. + * + *

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

There's the following invariants: - {@link #syncAdapterPackageName} is always set to the + * actual sync adapter package name. - {@link #resourcePackageName} too is set to the same value, + * unless {@link #isEmbedded()}, in which case it'll be null. There's an unfortunate exception of + * {@link FallbackAccountType}. Even though it {@link #isEmbedded()}, but we set non-null to + * {@link #resourcePackageName} for unit tests. + */ + public String resourcePackageName; + /** + * The package name for the authenticator (for the embedded types, i.e. Google and Exchange) or + * the sync adapter (for external type, including extensions). + */ + public String syncAdapterPackageName; + + public int titleRes; + public int iconRes; + protected boolean mIsInitialized; + /** Set of {@link DataKind} supported by this source. */ + private ArrayList mKinds = new ArrayList<>(); + /** Lookup map of {@link #mKinds} on {@link DataKind#mimeType}. */ + private Map mMimeKinds = new ArrayMap<>(); + + /** + * Return a string resource loaded from the given package (or the current package if {@code + * packageName} is null), unless {@code resId} is -1, in which case it returns {@code + * defaultValue}. + * + *

(The behavior is undefined if the resource or package doesn't exist.) + */ + @VisibleForTesting + static CharSequence getResourceText( + Context context, String packageName, int resId, String defaultValue) { + if (resId != -1 && packageName != null) { + final PackageManager pm = context.getPackageManager(); + return pm.getText(packageName, resId, null); + } else if (resId != -1) { + return context.getText(resId); + } else { + return defaultValue; + } + } + + public static Drawable getDisplayIcon( + Context context, int titleRes, int iconRes, String syncAdapterPackageName) { + if (titleRes != -1 && syncAdapterPackageName != null) { + final PackageManager pm = context.getPackageManager(); + return pm.getDrawable(syncAdapterPackageName, iconRes, null); + } else if (titleRes != -1) { + return context.getResources().getDrawable(iconRes); + } else { + return null; + } + } + + /** + * Whether this account type was able to be fully initialized. This may be false if (for example) + * the package name associated with the account type could not be found. + */ + public final boolean isInitialized() { + return mIsInitialized; + } + + /** + * @return Whether this type is an "embedded" type. i.e. any of {@link FallbackAccountType}, + * {@link GoogleAccountType} or {@link ExternalAccountType}. + *

If an embedded type cannot be initialized (i.e. if {@link #isInitialized()} returns + * {@code false}) it's considered critical, and the application will crash. On the other hand + * if it's not an embedded type, we just skip loading the type. + */ + public boolean isEmbedded() { + return true; + } + + public boolean isExtension() { + return false; + } + + /** + * @return True if contacts can be created and edited using this app. If false, there could still + * be an external editor as provided by {@link #getEditContactActivityClassName()} or {@link + * #getCreateContactActivityClassName()} + */ + public abstract boolean areContactsWritable(); + + /** + * Returns an optional custom edit activity. + * + *

Only makes sense for non-embedded account types. The activity class should reside in the + * sync adapter package as determined by {@link #syncAdapterPackageName}. + */ + public String getEditContactActivityClassName() { + return null; + } + + /** + * Returns an optional custom new contact activity. + * + *

Only makes sense for non-embedded account types. The activity class should reside in the + * sync adapter package as determined by {@link #syncAdapterPackageName}. + */ + public String getCreateContactActivityClassName() { + return null; + } + + /** + * Returns an optional custom invite contact activity. + * + *

Only makes sense for non-embedded account types. The activity class should reside in the + * sync adapter package as determined by {@link #syncAdapterPackageName}. + */ + public String getInviteContactActivityClassName() { + return null; + } + + /** + * Returns an optional service that can be launched whenever a contact is being looked at. This + * allows the sync adapter to provide more up-to-date information. + * + *

The service class should reside in the sync adapter package as determined by {@link + * #getViewContactNotifyServicePackageName()}. + */ + public String getViewContactNotifyServiceClassName() { + return null; + } + + /** + * TODO This is way too hacky should be removed. + * + *

This is introduced for {@link GoogleAccountType} where {@link #syncAdapterPackageName} is + * the authenticator package name but the notification service is in the sync adapter package. See + * {@link #resourcePackageName} -- we should clean up those. + */ + public String getViewContactNotifyServicePackageName() { + return syncAdapterPackageName; + } + + /** Returns an optional Activity string that can be used to view the group. */ + public String getViewGroupActivity() { + return null; + } + + public CharSequence getDisplayLabel(Context context) { + // Note this resource is defined in the sync adapter package, not resourcePackageName. + return getResourceText(context, syncAdapterPackageName, titleRes, accountType); + } + + /** @return resource ID for the "invite contact" action label, or -1 if not defined. */ + protected int getInviteContactActionResId() { + return -1; + } + + /** @return resource ID for the "view group" label, or -1 if not defined. */ + protected int getViewGroupLabelResId() { + return -1; + } + + /** Returns {@link AccountTypeWithDataSet} for this type. */ + public AccountTypeWithDataSet getAccountTypeAndDataSet() { + return AccountTypeWithDataSet.get(accountType, dataSet); + } + + /** + * Returns a list of additional package names that should be inspected as additional external + * account types. This allows for a primary account type to indicate other packages that may not + * be sync adapters but which still provide contact data, perhaps under a separate data set within + * the account. + */ + public List getExtensionPackageNames() { + return new ArrayList(); + } + + /** + * Returns an optional custom label for the "invite contact" action, which will be shown on the + * contact card. (If not defined, returns null.) + */ + public CharSequence getInviteContactActionLabel(Context context) { + // Note this resource is defined in the sync adapter package, not resourcePackageName. + return getResourceText(context, syncAdapterPackageName, getInviteContactActionResId(), ""); + } + + /** + * Returns a label for the "view group" action. If not defined, this falls back to our own "View + * Updates" string + */ + public CharSequence getViewGroupLabel(Context context) { + // Note this resource is defined in the sync adapter package, not resourcePackageName. + final CharSequence customTitle = + getResourceText(context, syncAdapterPackageName, getViewGroupLabelResId(), null); + + return customTitle == null ? context.getText(R.string.view_updates_from_group) : customTitle; + } + + public Drawable getDisplayIcon(Context context) { + return getDisplayIcon(context, titleRes, iconRes, syncAdapterPackageName); + } + + /** Whether or not groups created under this account type have editable membership lists. */ + public abstract boolean isGroupMembershipEditable(); + + /** Return list of {@link DataKind} supported, sorted by {@link DataKind#weight}. */ + public ArrayList getSortedDataKinds() { + // TODO: optimize by marking if already sorted + Collections.sort(mKinds, sWeightComparator); + return mKinds; + } + + /** Find the {@link DataKind} for a specific MIME-type, if it's handled by this data source. */ + public DataKind getKindForMimetype(String mimeType) { + return this.mMimeKinds.get(mimeType); + } + + /** Add given {@link DataKind} to list of those provided by this source. */ + public DataKind addKind(DataKind kind) throws DefinitionException { + if (kind.mimeType == null) { + throw new DefinitionException("null is not a valid mime type"); + } + if (mMimeKinds.get(kind.mimeType) != null) { + throw new DefinitionException("mime type '" + kind.mimeType + "' is already registered"); + } + + kind.resourcePackageName = this.resourcePackageName; + this.mKinds.add(kind); + this.mMimeKinds.put(kind.mimeType, kind); + return kind; + } + + /** + * Generic method of inflating a given {@link ContentValues} into a user-readable {@link + * CharSequence}. For example, an inflater could combine the multiple columns of {@link + * StructuredPostal} together using a string resource before presenting to the user. + */ + public interface StringInflater { + + CharSequence inflateUsing(Context context, ContentValues values); + } + + protected static class DefinitionException extends Exception { + + public DefinitionException(String message) { + super(message); + } + + public DefinitionException(String message, Exception inner) { + super(message, inner); + } + } + + /** + * Description of a specific "type" or "label" of a {@link DataKind} row, such as {@link + * Phone#TYPE_WORK}. Includes constraints on total number of rows a {@link Contacts} may have of + * this type, and details on how user-defined labels are stored. + */ + public static class EditType { + + public int rawValue; + public int labelRes; + public boolean secondary; + /** + * The number of entries allowed for the type. -1 if not specified. + * + * @see DataKind#typeOverallMax + */ + public int specificMax; + + public String customColumn; + + public EditType(int rawValue, int labelRes) { + this.rawValue = rawValue; + this.labelRes = labelRes; + this.specificMax = -1; + } + + public EditType setSecondary(boolean secondary) { + this.secondary = secondary; + return this; + } + + public EditType setSpecificMax(int specificMax) { + this.specificMax = specificMax; + return this; + } + + public EditType setCustomColumn(String customColumn) { + this.customColumn = customColumn; + return this; + } + + @Override + public boolean equals(Object object) { + if (object instanceof EditType) { + final EditType other = (EditType) object; + return other.rawValue == rawValue; + } + return false; + } + + @Override + public int hashCode() { + return rawValue; + } + + @Override + public String toString() { + return this.getClass().getSimpleName() + + " rawValue=" + + rawValue + + " labelRes=" + + labelRes + + " secondary=" + + secondary + + " specificMax=" + + specificMax + + " customColumn=" + + customColumn; + } + } + + public static class EventEditType extends EditType { + + private boolean mYearOptional; + + public EventEditType(int rawValue, int labelRes) { + super(rawValue, labelRes); + } + + public boolean isYearOptional() { + return mYearOptional; + } + + public EventEditType setYearOptional(boolean yearOptional) { + mYearOptional = yearOptional; + return this; + } + + @Override + public String toString() { + return super.toString() + " mYearOptional=" + mYearOptional; + } + } + + /** + * Description of a user-editable field on a {@link DataKind} row, such as {@link Phone#NUMBER}. + * Includes flags to apply to an {@link EditText}, and the column where this field is stored. + */ + public static final class EditField { + + public String column; + public int titleRes; + public int inputType; + public int minLines; + public boolean optional; + public boolean shortForm; + public boolean longForm; + + public EditField(String column, int titleRes) { + this.column = column; + this.titleRes = titleRes; + } + + public EditField(String column, int titleRes, int inputType) { + this(column, titleRes); + this.inputType = inputType; + } + + public EditField setOptional(boolean optional) { + this.optional = optional; + return this; + } + + public EditField setShortForm(boolean shortForm) { + this.shortForm = shortForm; + return this; + } + + public EditField setLongForm(boolean longForm) { + this.longForm = longForm; + return this; + } + + public EditField setMinLines(int minLines) { + this.minLines = minLines; + return this; + } + + public boolean isMultiLine() { + return (inputType & EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE) != 0; + } + + @Override + public String toString() { + return this.getClass().getSimpleName() + + ":" + + " column=" + + column + + " titleRes=" + + titleRes + + " inputType=" + + inputType + + " minLines=" + + minLines + + " optional=" + + optional + + " shortForm=" + + shortForm + + " longForm=" + + longForm; + } + } + + /** + * Compare two {@link AccountType} by their {@link AccountType#getDisplayLabel} with the current + * locale. + */ + public static class DisplayLabelComparator implements Comparator { + + private final Context mContext; + /** {@link Comparator} for the current locale. */ + private final Collator mCollator = Collator.getInstance(); + + public DisplayLabelComparator(Context context) { + mContext = context; + } + + private String getDisplayLabel(AccountType type) { + CharSequence label = type.getDisplayLabel(mContext); + return (label == null) ? "" : label.toString(); + } + + @Override + public int compare(AccountType lhs, AccountType rhs) { + return mCollator.compare(getDisplayLabel(lhs), getDisplayLabel(rhs)); + } + } +} diff --git a/java/com/android/contacts/common/model/account/AccountTypeWithDataSet.java b/java/com/android/contacts/common/model/account/AccountTypeWithDataSet.java new file mode 100644 index 000000000..a32ebe139 --- /dev/null +++ b/java/com/android/contacts/common/model/account/AccountTypeWithDataSet.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model.account; + +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.provider.BaseColumns; +import android.provider.ContactsContract; +import android.provider.ContactsContract.RawContacts; +import android.text.TextUtils; +import java.util.Objects; + +/** Encapsulates an "account type" string and a "data set" string. */ +public class AccountTypeWithDataSet { + + private static final String[] ID_PROJECTION = new String[] {BaseColumns._ID}; + private static final Uri RAW_CONTACTS_URI_LIMIT_1 = + RawContacts.CONTENT_URI + .buildUpon() + .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY, "1") + .build(); + + /** account type. Can be null for fallback type. */ + public final String accountType; + + /** dataSet may be null, but never be "". */ + public final String dataSet; + + private AccountTypeWithDataSet(String accountType, String dataSet) { + this.accountType = TextUtils.isEmpty(accountType) ? null : accountType; + this.dataSet = TextUtils.isEmpty(dataSet) ? null : dataSet; + } + + public static AccountTypeWithDataSet get(String accountType, String dataSet) { + return new AccountTypeWithDataSet(accountType, dataSet); + } + + /** + * Return true if there are any contacts in the database with this account type and data set. + * Touches DB. Don't use in the UI thread. + */ + public boolean hasData(Context context) { + final String BASE_SELECTION = RawContacts.ACCOUNT_TYPE + " = ?"; + final String selection; + final String[] args; + if (TextUtils.isEmpty(dataSet)) { + selection = BASE_SELECTION + " AND " + RawContacts.DATA_SET + " IS NULL"; + args = new String[] {accountType}; + } else { + selection = BASE_SELECTION + " AND " + RawContacts.DATA_SET + " = ?"; + args = new String[] {accountType, dataSet}; + } + + final Cursor c = + context + .getContentResolver() + .query(RAW_CONTACTS_URI_LIMIT_1, ID_PROJECTION, selection, args, null); + if (c == null) { + return false; + } + try { + return c.moveToFirst(); + } finally { + c.close(); + } + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof AccountTypeWithDataSet)) { + return false; + } + + AccountTypeWithDataSet other = (AccountTypeWithDataSet) o; + return Objects.equals(accountType, other.accountType) && Objects.equals(dataSet, other.dataSet); + } + + @Override + public int hashCode() { + return (accountType == null ? 0 : accountType.hashCode()) + ^ (dataSet == null ? 0 : dataSet.hashCode()); + } + + @Override + public String toString() { + return "[" + accountType + "/" + dataSet + "]"; + } +} diff --git a/java/com/android/contacts/common/model/account/AccountWithDataSet.java b/java/com/android/contacts/common/model/account/AccountWithDataSet.java new file mode 100644 index 000000000..71faf509c --- /dev/null +++ b/java/com/android/contacts/common/model/account/AccountWithDataSet.java @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model.account; + +import android.accounts.Account; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; +import android.provider.BaseColumns; +import android.provider.ContactsContract; +import android.provider.ContactsContract.RawContacts; +import android.text.TextUtils; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.regex.Pattern; + +/** Wrapper for an account that includes a data set (which may be null). */ +public class AccountWithDataSet implements Parcelable { + + // For Parcelable + public static final Creator CREATOR = + new Creator() { + public AccountWithDataSet createFromParcel(Parcel source) { + return new AccountWithDataSet(source); + } + + public AccountWithDataSet[] newArray(int size) { + return new AccountWithDataSet[size]; + } + }; + private static final String STRINGIFY_SEPARATOR = "\u0001"; + private static final String ARRAY_STRINGIFY_SEPARATOR = "\u0002"; + private static final Pattern STRINGIFY_SEPARATOR_PAT = + Pattern.compile(Pattern.quote(STRINGIFY_SEPARATOR)); + private static final Pattern ARRAY_STRINGIFY_SEPARATOR_PAT = + Pattern.compile(Pattern.quote(ARRAY_STRINGIFY_SEPARATOR)); + private static final String[] ID_PROJECTION = new String[] {BaseColumns._ID}; + private static final Uri RAW_CONTACTS_URI_LIMIT_1 = + RawContacts.CONTENT_URI + .buildUpon() + .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY, "1") + .build(); + public final String name; + public final String type; + public final String dataSet; + private final AccountTypeWithDataSet mAccountTypeWithDataSet; + + public AccountWithDataSet(String name, String type, String dataSet) { + this.name = emptyToNull(name); + this.type = emptyToNull(type); + this.dataSet = emptyToNull(dataSet); + mAccountTypeWithDataSet = AccountTypeWithDataSet.get(type, dataSet); + } + + public AccountWithDataSet(Parcel in) { + this.name = in.readString(); + this.type = in.readString(); + this.dataSet = in.readString(); + mAccountTypeWithDataSet = AccountTypeWithDataSet.get(type, dataSet); + } + + private static String emptyToNull(String text) { + return TextUtils.isEmpty(text) ? null : text; + } + + private static StringBuilder addStringified(StringBuilder sb, AccountWithDataSet account) { + if (!TextUtils.isEmpty(account.name)) { + sb.append(account.name); + } + sb.append(STRINGIFY_SEPARATOR); + if (!TextUtils.isEmpty(account.type)) { + sb.append(account.type); + } + sb.append(STRINGIFY_SEPARATOR); + if (!TextUtils.isEmpty(account.dataSet)) { + sb.append(account.dataSet); + } + + return sb; + } + + /** + * Unpack a string created by {@link #stringify}. + * + * @throws IllegalArgumentException if it's an invalid string. + */ + public static AccountWithDataSet unstringify(String s) { + final String[] array = STRINGIFY_SEPARATOR_PAT.split(s, 3); + if (array.length < 3) { + throw new IllegalArgumentException("Invalid string " + s); + } + return new AccountWithDataSet( + array[0], array[1], TextUtils.isEmpty(array[2]) ? null : array[2]); + } + + /** Pack a list of {@link AccountWithDataSet} into a string. */ + public static String stringifyList(List accounts) { + final StringBuilder sb = new StringBuilder(); + + for (AccountWithDataSet account : accounts) { + if (sb.length() > 0) { + sb.append(ARRAY_STRINGIFY_SEPARATOR); + } + addStringified(sb, account); + } + + return sb.toString(); + } + + /** + * Unpack a list of {@link AccountWithDataSet} into a string. + * + * @throws IllegalArgumentException if it's an invalid string. + */ + public static List unstringifyList(String s) { + final ArrayList ret = new ArrayList<>(); + if (TextUtils.isEmpty(s)) { + return ret; + } + + final String[] array = ARRAY_STRINGIFY_SEPARATOR_PAT.split(s); + + for (int i = 0; i < array.length; i++) { + ret.add(unstringify(array[i])); + } + + return ret; + } + + public boolean isLocalAccount() { + return name == null && type == null; + } + + public Account getAccountOrNull() { + if (name != null && type != null) { + return new Account(name, type); + } + return null; + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(name); + dest.writeString(type); + dest.writeString(dataSet); + } + + public AccountTypeWithDataSet getAccountTypeWithDataSet() { + return mAccountTypeWithDataSet; + } + + /** + * Return {@code true} if this account has any contacts in the database. Touches DB. Don't use in + * the UI thread. + */ + public boolean hasData(Context context) { + final String BASE_SELECTION = + RawContacts.ACCOUNT_TYPE + " = ?" + " AND " + RawContacts.ACCOUNT_NAME + " = ?"; + final String selection; + final String[] args; + if (TextUtils.isEmpty(dataSet)) { + selection = BASE_SELECTION + " AND " + RawContacts.DATA_SET + " IS NULL"; + args = new String[] {type, name}; + } else { + selection = BASE_SELECTION + " AND " + RawContacts.DATA_SET + " = ?"; + args = new String[] {type, name, dataSet}; + } + + final Cursor c = + context + .getContentResolver() + .query(RAW_CONTACTS_URI_LIMIT_1, ID_PROJECTION, selection, args, null); + if (c == null) { + return false; + } + try { + return c.moveToFirst(); + } finally { + c.close(); + } + } + + public boolean equals(Object obj) { + if (obj instanceof AccountWithDataSet) { + AccountWithDataSet other = (AccountWithDataSet) obj; + return Objects.equals(name, other.name) + && Objects.equals(type, other.type) + && Objects.equals(dataSet, other.dataSet); + } + return false; + } + + public int hashCode() { + int result = 17; + result = 31 * result + (name != null ? name.hashCode() : 0); + result = 31 * result + (type != null ? type.hashCode() : 0); + result = 31 * result + (dataSet != null ? dataSet.hashCode() : 0); + return result; + } + + public String toString() { + return "AccountWithDataSet {name=" + name + ", type=" + type + ", dataSet=" + dataSet + "}"; + } + + /** Pack the instance into a string. */ + public String stringify() { + return addStringified(new StringBuilder(), this).toString(); + } +} diff --git a/java/com/android/contacts/common/model/account/BaseAccountType.java b/java/com/android/contacts/common/model/account/BaseAccountType.java new file mode 100644 index 000000000..21b555917 --- /dev/null +++ b/java/com/android/contacts/common/model/account/BaseAccountType.java @@ -0,0 +1,1890 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model.account; + +import android.content.ContentValues; +import android.content.Context; +import android.content.res.Resources; +import android.provider.ContactsContract.CommonDataKinds.BaseTypes; +import android.provider.ContactsContract.CommonDataKinds.Email; +import android.provider.ContactsContract.CommonDataKinds.Event; +import android.provider.ContactsContract.CommonDataKinds.GroupMembership; +import android.provider.ContactsContract.CommonDataKinds.Im; +import android.provider.ContactsContract.CommonDataKinds.Nickname; +import android.provider.ContactsContract.CommonDataKinds.Note; +import android.provider.ContactsContract.CommonDataKinds.Organization; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.CommonDataKinds.Photo; +import android.provider.ContactsContract.CommonDataKinds.Relation; +import android.provider.ContactsContract.CommonDataKinds.SipAddress; +import android.provider.ContactsContract.CommonDataKinds.StructuredName; +import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; +import android.provider.ContactsContract.CommonDataKinds.Website; +import android.util.ArrayMap; +import android.util.AttributeSet; +import android.util.Log; +import android.view.inputmethod.EditorInfo; +import com.android.contacts.common.R; +import com.android.contacts.common.model.dataitem.DataKind; +import com.android.contacts.common.util.CommonDateUtils; +import com.android.contacts.common.util.ContactDisplayUtils; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +public abstract class BaseAccountType extends AccountType { + + public static final StringInflater ORGANIZATION_BODY_INFLATER = + new StringInflater() { + @Override + public CharSequence inflateUsing(Context context, ContentValues values) { + final CharSequence companyValue = + values.containsKey(Organization.COMPANY) + ? values.getAsString(Organization.COMPANY) + : null; + final CharSequence titleValue = + values.containsKey(Organization.TITLE) + ? values.getAsString(Organization.TITLE) + : null; + + if (companyValue != null && titleValue != null) { + return companyValue + ": " + titleValue; + } else if (companyValue == null) { + return titleValue; + } else { + return companyValue; + } + } + }; + protected static final int FLAGS_PHONE = EditorInfo.TYPE_CLASS_PHONE; + protected static final int FLAGS_EMAIL = + EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS; + protected static final int FLAGS_PERSON_NAME = + EditorInfo.TYPE_CLASS_TEXT + | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS + | EditorInfo.TYPE_TEXT_VARIATION_PERSON_NAME; + protected static final int FLAGS_PHONETIC = + EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_PHONETIC; + protected static final int FLAGS_GENERIC_NAME = + EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS; + protected static final int FLAGS_NOTE = + EditorInfo.TYPE_CLASS_TEXT + | EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES + | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE; + protected static final int FLAGS_EVENT = EditorInfo.TYPE_CLASS_TEXT; + protected static final int FLAGS_WEBSITE = + EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_URI; + protected static final int FLAGS_POSTAL = + EditorInfo.TYPE_CLASS_TEXT + | EditorInfo.TYPE_TEXT_VARIATION_POSTAL_ADDRESS + | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS + | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE; + protected static final int FLAGS_SIP_ADDRESS = + EditorInfo.TYPE_CLASS_TEXT + | EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS; // since SIP addresses have the same + // basic format as email addresses + protected static final int FLAGS_RELATION = + EditorInfo.TYPE_CLASS_TEXT + | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS + | EditorInfo.TYPE_TEXT_VARIATION_PERSON_NAME; + + // Specify the maximum number of lines that can be used to display various field types. If no + // value is specified for a particular type, we use the default value from {@link DataKind}. + protected static final int MAX_LINES_FOR_POSTAL_ADDRESS = 10; + protected static final int MAX_LINES_FOR_GROUP = 10; + protected static final int MAX_LINES_FOR_NOTE = 100; + private static final String TAG = "BaseAccountType"; + + public BaseAccountType() { + this.accountType = null; + this.dataSet = null; + this.titleRes = R.string.account_phone; + this.iconRes = R.mipmap.ic_contacts_launcher; + } + + protected static EditType buildPhoneType(int type) { + return new EditType(type, Phone.getTypeLabelResource(type)); + } + + protected static EditType buildEmailType(int type) { + return new EditType(type, Email.getTypeLabelResource(type)); + } + + protected static EditType buildPostalType(int type) { + return new EditType(type, StructuredPostal.getTypeLabelResource(type)); + } + + protected static EditType buildImType(int type) { + return new EditType(type, Im.getProtocolLabelResource(type)); + } + + protected static EditType buildEventType(int type, boolean yearOptional) { + return new EventEditType(type, Event.getTypeResource(type)).setYearOptional(yearOptional); + } + + protected static EditType buildRelationType(int type) { + return new EditType(type, Relation.getTypeLabelResource(type)); + } + + // Utility methods to keep code shorter. + private static boolean getAttr(AttributeSet attrs, String attribute, boolean defaultValue) { + return attrs.getAttributeBooleanValue(null, attribute, defaultValue); + } + + private static int getAttr(AttributeSet attrs, String attribute, int defaultValue) { + return attrs.getAttributeIntValue(null, attribute, defaultValue); + } + + private static String getAttr(AttributeSet attrs, String attribute) { + return attrs.getAttributeValue(null, attribute); + } + + protected DataKind addDataKindStructuredName(Context context) throws DefinitionException { + DataKind kind = + addKind( + new DataKind( + StructuredName.CONTENT_ITEM_TYPE, R.string.nameLabelsGroup, Weight.NONE, true)); + kind.actionHeader = new SimpleInflater(R.string.nameLabelsGroup); + kind.actionBody = new SimpleInflater(Nickname.NAME); + kind.typeOverallMax = 1; + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add( + new EditField(StructuredName.DISPLAY_NAME, R.string.full_name, FLAGS_PERSON_NAME)); + kind.fieldList.add( + new EditField(StructuredName.PREFIX, R.string.name_prefix, FLAGS_PERSON_NAME) + .setLongForm(true)); + kind.fieldList.add( + new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME) + .setLongForm(true)); + kind.fieldList.add( + new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME) + .setLongForm(true)); + kind.fieldList.add( + new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME) + .setLongForm(true)); + kind.fieldList.add( + new EditField(StructuredName.SUFFIX, R.string.name_suffix, FLAGS_PERSON_NAME) + .setLongForm(true)); + kind.fieldList.add( + new EditField( + StructuredName.PHONETIC_FAMILY_NAME, R.string.name_phonetic_family, FLAGS_PHONETIC)); + kind.fieldList.add( + new EditField( + StructuredName.PHONETIC_MIDDLE_NAME, R.string.name_phonetic_middle, FLAGS_PHONETIC)); + kind.fieldList.add( + new EditField( + StructuredName.PHONETIC_GIVEN_NAME, R.string.name_phonetic_given, FLAGS_PHONETIC)); + + return kind; + } + + protected DataKind addDataKindDisplayName(Context context) throws DefinitionException { + DataKind kind = + addKind( + new DataKind( + DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME, + R.string.nameLabelsGroup, + Weight.NONE, + true)); + kind.actionHeader = new SimpleInflater(R.string.nameLabelsGroup); + kind.actionBody = new SimpleInflater(Nickname.NAME); + kind.typeOverallMax = 1; + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add( + new EditField(StructuredName.DISPLAY_NAME, R.string.full_name, FLAGS_PERSON_NAME) + .setShortForm(true)); + + boolean displayOrderPrimary = + context.getResources().getBoolean(R.bool.config_editor_field_order_primary); + + if (!displayOrderPrimary) { + kind.fieldList.add( + new EditField(StructuredName.PREFIX, R.string.name_prefix, FLAGS_PERSON_NAME) + .setLongForm(true)); + kind.fieldList.add( + new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME) + .setLongForm(true)); + kind.fieldList.add( + new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME) + .setLongForm(true)); + kind.fieldList.add( + new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME) + .setLongForm(true)); + kind.fieldList.add( + new EditField(StructuredName.SUFFIX, R.string.name_suffix, FLAGS_PERSON_NAME) + .setLongForm(true)); + } else { + kind.fieldList.add( + new EditField(StructuredName.PREFIX, R.string.name_prefix, FLAGS_PERSON_NAME) + .setLongForm(true)); + kind.fieldList.add( + new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME) + .setLongForm(true)); + kind.fieldList.add( + new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME) + .setLongForm(true)); + kind.fieldList.add( + new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME) + .setLongForm(true)); + kind.fieldList.add( + new EditField(StructuredName.SUFFIX, R.string.name_suffix, FLAGS_PERSON_NAME) + .setLongForm(true)); + } + + return kind; + } + + protected DataKind addDataKindPhoneticName(Context context) throws DefinitionException { + DataKind kind = + addKind( + new DataKind( + DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME, + R.string.name_phonetic, + Weight.NONE, + true)); + kind.actionHeader = new SimpleInflater(R.string.nameLabelsGroup); + kind.actionBody = new SimpleInflater(Nickname.NAME); + kind.typeOverallMax = 1; + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add( + new EditField(DataKind.PSEUDO_COLUMN_PHONETIC_NAME, R.string.name_phonetic, FLAGS_PHONETIC) + .setShortForm(true)); + kind.fieldList.add( + new EditField( + StructuredName.PHONETIC_FAMILY_NAME, R.string.name_phonetic_family, FLAGS_PHONETIC) + .setLongForm(true)); + kind.fieldList.add( + new EditField( + StructuredName.PHONETIC_MIDDLE_NAME, R.string.name_phonetic_middle, FLAGS_PHONETIC) + .setLongForm(true)); + kind.fieldList.add( + new EditField( + StructuredName.PHONETIC_GIVEN_NAME, R.string.name_phonetic_given, FLAGS_PHONETIC) + .setLongForm(true)); + + return kind; + } + + protected DataKind addDataKindNickname(Context context) throws DefinitionException { + DataKind kind = + addKind( + new DataKind( + Nickname.CONTENT_ITEM_TYPE, R.string.nicknameLabelsGroup, Weight.NICKNAME, true)); + kind.typeOverallMax = 1; + kind.actionHeader = new SimpleInflater(R.string.nicknameLabelsGroup); + kind.actionBody = new SimpleInflater(Nickname.NAME); + kind.defaultValues = new ContentValues(); + kind.defaultValues.put(Nickname.TYPE, Nickname.TYPE_DEFAULT); + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add( + new EditField(Nickname.NAME, R.string.nicknameLabelsGroup, FLAGS_PERSON_NAME)); + + return kind; + } + + protected DataKind addDataKindPhone(Context context) throws DefinitionException { + DataKind kind = + addKind( + new DataKind(Phone.CONTENT_ITEM_TYPE, R.string.phoneLabelsGroup, Weight.PHONE, true)); + kind.iconAltRes = R.drawable.ic_message_24dp; + kind.iconAltDescriptionRes = R.string.sms; + kind.actionHeader = new PhoneActionInflater(); + kind.actionAltHeader = new PhoneActionAltInflater(); + kind.actionBody = new SimpleInflater(Phone.NUMBER); + kind.typeColumn = Phone.TYPE; + kind.typeList = new ArrayList<>(); + kind.typeList.add(buildPhoneType(Phone.TYPE_MOBILE)); + kind.typeList.add(buildPhoneType(Phone.TYPE_HOME)); + kind.typeList.add(buildPhoneType(Phone.TYPE_WORK)); + kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_WORK).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_HOME).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_PAGER).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_OTHER)); + kind.typeList.add( + buildPhoneType(Phone.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Phone.LABEL)); + kind.typeList.add(buildPhoneType(Phone.TYPE_CALLBACK).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_CAR).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_COMPANY_MAIN).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_ISDN).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_MAIN).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_OTHER_FAX).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_RADIO).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_TELEX).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_TTY_TDD).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_WORK_MOBILE).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_WORK_PAGER).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_ASSISTANT).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_MMS).setSecondary(true)); + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE)); + + return kind; + } + + protected DataKind addDataKindEmail(Context context) throws DefinitionException { + DataKind kind = + addKind( + new DataKind(Email.CONTENT_ITEM_TYPE, R.string.emailLabelsGroup, Weight.EMAIL, true)); + kind.actionHeader = new EmailActionInflater(); + kind.actionBody = new SimpleInflater(Email.DATA); + kind.typeColumn = Email.TYPE; + kind.typeList = new ArrayList<>(); + kind.typeList.add(buildEmailType(Email.TYPE_HOME)); + kind.typeList.add(buildEmailType(Email.TYPE_WORK)); + kind.typeList.add(buildEmailType(Email.TYPE_OTHER)); + kind.typeList.add(buildEmailType(Email.TYPE_MOBILE)); + kind.typeList.add( + buildEmailType(Email.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Email.LABEL)); + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL)); + + return kind; + } + + protected DataKind addDataKindStructuredPostal(Context context) throws DefinitionException { + DataKind kind = + addKind( + new DataKind( + StructuredPostal.CONTENT_ITEM_TYPE, + R.string.postalLabelsGroup, + Weight.STRUCTURED_POSTAL, + true)); + kind.actionHeader = new PostalActionInflater(); + kind.actionBody = new SimpleInflater(StructuredPostal.FORMATTED_ADDRESS); + kind.typeColumn = StructuredPostal.TYPE; + kind.typeList = new ArrayList<>(); + kind.typeList.add(buildPostalType(StructuredPostal.TYPE_HOME)); + kind.typeList.add(buildPostalType(StructuredPostal.TYPE_WORK)); + kind.typeList.add(buildPostalType(StructuredPostal.TYPE_OTHER)); + kind.typeList.add( + buildPostalType(StructuredPostal.TYPE_CUSTOM) + .setSecondary(true) + .setCustomColumn(StructuredPostal.LABEL)); + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add( + new EditField(StructuredPostal.FORMATTED_ADDRESS, R.string.postal_address, FLAGS_POSTAL)); + + kind.maxLinesForDisplay = MAX_LINES_FOR_POSTAL_ADDRESS; + + return kind; + } + + protected DataKind addDataKindIm(Context context) throws DefinitionException { + DataKind kind = + addKind(new DataKind(Im.CONTENT_ITEM_TYPE, R.string.imLabelsGroup, Weight.IM, true)); + kind.actionHeader = new ImActionInflater(); + kind.actionBody = new SimpleInflater(Im.DATA); + + // NOTE: even though a traditional "type" exists, for editing + // purposes we're using the protocol to pick labels + + kind.defaultValues = new ContentValues(); + kind.defaultValues.put(Im.TYPE, Im.TYPE_OTHER); + + kind.typeColumn = Im.PROTOCOL; + kind.typeList = new ArrayList<>(); + kind.typeList.add(buildImType(Im.PROTOCOL_AIM)); + kind.typeList.add(buildImType(Im.PROTOCOL_MSN)); + kind.typeList.add(buildImType(Im.PROTOCOL_YAHOO)); + kind.typeList.add(buildImType(Im.PROTOCOL_SKYPE)); + kind.typeList.add(buildImType(Im.PROTOCOL_QQ)); + kind.typeList.add(buildImType(Im.PROTOCOL_GOOGLE_TALK)); + kind.typeList.add(buildImType(Im.PROTOCOL_ICQ)); + kind.typeList.add(buildImType(Im.PROTOCOL_JABBER)); + kind.typeList.add( + buildImType(Im.PROTOCOL_CUSTOM).setSecondary(true).setCustomColumn(Im.CUSTOM_PROTOCOL)); + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add(new EditField(Im.DATA, R.string.imLabelsGroup, FLAGS_EMAIL)); + + return kind; + } + + protected DataKind addDataKindOrganization(Context context) throws DefinitionException { + DataKind kind = + addKind( + new DataKind( + Organization.CONTENT_ITEM_TYPE, + R.string.organizationLabelsGroup, + Weight.ORGANIZATION, + true)); + kind.actionHeader = new SimpleInflater(R.string.organizationLabelsGroup); + kind.actionBody = ORGANIZATION_BODY_INFLATER; + kind.typeOverallMax = 1; + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add( + new EditField(Organization.COMPANY, R.string.ghostData_company, FLAGS_GENERIC_NAME)); + kind.fieldList.add( + new EditField(Organization.TITLE, R.string.ghostData_title, FLAGS_GENERIC_NAME)); + + return kind; + } + + protected DataKind addDataKindPhoto(Context context) throws DefinitionException { + DataKind kind = addKind(new DataKind(Photo.CONTENT_ITEM_TYPE, -1, Weight.NONE, true)); + kind.typeOverallMax = 1; + kind.fieldList = new ArrayList<>(); + kind.fieldList.add(new EditField(Photo.PHOTO, -1, -1)); + return kind; + } + + protected DataKind addDataKindNote(Context context) throws DefinitionException { + DataKind kind = + addKind(new DataKind(Note.CONTENT_ITEM_TYPE, R.string.label_notes, Weight.NOTE, true)); + kind.typeOverallMax = 1; + kind.actionHeader = new SimpleInflater(R.string.label_notes); + kind.actionBody = new SimpleInflater(Note.NOTE); + kind.fieldList = new ArrayList<>(); + kind.fieldList.add(new EditField(Note.NOTE, R.string.label_notes, FLAGS_NOTE)); + + kind.maxLinesForDisplay = MAX_LINES_FOR_NOTE; + + return kind; + } + + protected DataKind addDataKindWebsite(Context context) throws DefinitionException { + DataKind kind = + addKind( + new DataKind( + Website.CONTENT_ITEM_TYPE, R.string.websiteLabelsGroup, Weight.WEBSITE, true)); + kind.actionHeader = new SimpleInflater(R.string.websiteLabelsGroup); + kind.actionBody = new SimpleInflater(Website.URL); + kind.defaultValues = new ContentValues(); + kind.defaultValues.put(Website.TYPE, Website.TYPE_OTHER); + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add(new EditField(Website.URL, R.string.websiteLabelsGroup, FLAGS_WEBSITE)); + + return kind; + } + + protected DataKind addDataKindSipAddress(Context context) throws DefinitionException { + DataKind kind = + addKind( + new DataKind( + SipAddress.CONTENT_ITEM_TYPE, + R.string.label_sip_address, + Weight.SIP_ADDRESS, + true)); + + kind.actionHeader = new SimpleInflater(R.string.label_sip_address); + kind.actionBody = new SimpleInflater(SipAddress.SIP_ADDRESS); + kind.fieldList = new ArrayList<>(); + kind.fieldList.add( + new EditField(SipAddress.SIP_ADDRESS, R.string.label_sip_address, FLAGS_SIP_ADDRESS)); + kind.typeOverallMax = 1; + + return kind; + } + + protected DataKind addDataKindGroupMembership(Context context) throws DefinitionException { + DataKind kind = + addKind( + new DataKind( + GroupMembership.CONTENT_ITEM_TYPE, + R.string.groupsLabel, + Weight.GROUP_MEMBERSHIP, + true)); + + kind.typeOverallMax = 1; + kind.fieldList = new ArrayList<>(); + kind.fieldList.add(new EditField(GroupMembership.GROUP_ROW_ID, -1, -1)); + + kind.maxLinesForDisplay = MAX_LINES_FOR_GROUP; + + return kind; + } + + @Override + public boolean isGroupMembershipEditable() { + return false; + } + + /** Parses the content of the EditSchema tag in contacts.xml. */ + protected final void parseEditSchema(Context context, XmlPullParser parser, AttributeSet attrs) + throws XmlPullParserException, IOException, DefinitionException { + + final int outerDepth = parser.getDepth(); + int type; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { + final int depth = parser.getDepth(); + if (type != XmlPullParser.START_TAG || depth != outerDepth + 1) { + continue; // Not direct child tag + } + + final String tag = parser.getName(); + + if (Tag.DATA_KIND.equals(tag)) { + for (DataKind kind : KindParser.INSTANCE.parseDataKindTag(context, parser, attrs)) { + addKind(kind); + } + } else { + Log.w(TAG, "Skipping unknown tag " + tag); + } + } + } + + private interface Tag { + + String DATA_KIND = "DataKind"; + String TYPE = "Type"; + } + + private interface Attr { + + String MAX_OCCURRENCE = "maxOccurs"; + String DATE_WITH_TIME = "dateWithTime"; + String YEAR_OPTIONAL = "yearOptional"; + String KIND = "kind"; + String TYPE = "type"; + } + + protected interface Weight { + + int NONE = -1; + int PHONE = 10; + int EMAIL = 15; + int STRUCTURED_POSTAL = 25; + int NICKNAME = 111; + int EVENT = 120; + int ORGANIZATION = 125; + int NOTE = 130; + int IM = 140; + int SIP_ADDRESS = 145; + int GROUP_MEMBERSHIP = 150; + int WEBSITE = 160; + int RELATIONSHIP = 999; + } + + /** + * Simple inflater that assumes a string resource has a "%s" that will be filled from the given + * column. + */ + public static class SimpleInflater implements StringInflater { + + private final int mStringRes; + private final String mColumnName; + + public SimpleInflater(int stringRes) { + this(stringRes, null); + } + + public SimpleInflater(String columnName) { + this(-1, columnName); + } + + public SimpleInflater(int stringRes, String columnName) { + mStringRes = stringRes; + mColumnName = columnName; + } + + @Override + public CharSequence inflateUsing(Context context, ContentValues values) { + final boolean validColumn = values.containsKey(mColumnName); + final boolean validString = mStringRes > 0; + + final CharSequence stringValue = validString ? context.getText(mStringRes) : null; + final CharSequence columnValue = validColumn ? values.getAsString(mColumnName) : null; + + if (validString && validColumn) { + return String.format(stringValue.toString(), columnValue); + } else if (validString) { + return stringValue; + } else if (validColumn) { + return columnValue; + } else { + return null; + } + } + + @Override + public String toString() { + return this.getClass().getSimpleName() + + " mStringRes=" + + mStringRes + + " mColumnName" + + mColumnName; + } + + public String getColumnNameForTest() { + return mColumnName; + } + } + + public abstract static class CommonInflater implements StringInflater { + + protected abstract int getTypeLabelResource(Integer type); + + protected boolean isCustom(Integer type) { + return type == BaseTypes.TYPE_CUSTOM; + } + + protected String getTypeColumn() { + return Phone.TYPE; + } + + protected String getLabelColumn() { + return Phone.LABEL; + } + + protected CharSequence getTypeLabel(Resources res, Integer type, CharSequence label) { + final int labelRes = getTypeLabelResource(type); + if (type == null) { + return res.getText(labelRes); + } else if (isCustom(type)) { + return res.getString(labelRes, label == null ? "" : label); + } else { + return res.getText(labelRes); + } + } + + @Override + public CharSequence inflateUsing(Context context, ContentValues values) { + final Integer type = values.getAsInteger(getTypeColumn()); + final String label = values.getAsString(getLabelColumn()); + return getTypeLabel(context.getResources(), type, label); + } + + @Override + public String toString() { + return this.getClass().getSimpleName(); + } + } + + public static class PhoneActionInflater extends CommonInflater { + + @Override + protected boolean isCustom(Integer type) { + return ContactDisplayUtils.isCustomPhoneType(type); + } + + @Override + protected int getTypeLabelResource(Integer type) { + return ContactDisplayUtils.getPhoneLabelResourceId(type); + } + } + + public static class PhoneActionAltInflater extends CommonInflater { + + @Override + protected boolean isCustom(Integer type) { + return ContactDisplayUtils.isCustomPhoneType(type); + } + + @Override + protected int getTypeLabelResource(Integer type) { + return ContactDisplayUtils.getSmsLabelResourceId(type); + } + } + + public static class EmailActionInflater extends CommonInflater { + + @Override + protected int getTypeLabelResource(Integer type) { + if (type == null) { + return R.string.email; + } + switch (type) { + case Email.TYPE_HOME: + return R.string.email_home; + case Email.TYPE_WORK: + return R.string.email_work; + case Email.TYPE_OTHER: + return R.string.email_other; + case Email.TYPE_MOBILE: + return R.string.email_mobile; + default: + return R.string.email_custom; + } + } + } + + public static class EventActionInflater extends CommonInflater { + + @Override + protected int getTypeLabelResource(Integer type) { + return Event.getTypeResource(type); + } + } + + public static class RelationActionInflater extends CommonInflater { + + @Override + protected int getTypeLabelResource(Integer type) { + return Relation.getTypeLabelResource(type == null ? Relation.TYPE_CUSTOM : type); + } + } + + public static class PostalActionInflater extends CommonInflater { + + @Override + protected int getTypeLabelResource(Integer type) { + if (type == null) { + return R.string.map_other; + } + switch (type) { + case StructuredPostal.TYPE_HOME: + return R.string.map_home; + case StructuredPostal.TYPE_WORK: + return R.string.map_work; + case StructuredPostal.TYPE_OTHER: + return R.string.map_other; + default: + return R.string.map_custom; + } + } + } + + public static class ImActionInflater extends CommonInflater { + + @Override + protected String getTypeColumn() { + return Im.PROTOCOL; + } + + @Override + protected String getLabelColumn() { + return Im.CUSTOM_PROTOCOL; + } + + @Override + protected int getTypeLabelResource(Integer type) { + if (type == null) { + return R.string.chat; + } + switch (type) { + case Im.PROTOCOL_AIM: + return R.string.chat_aim; + case Im.PROTOCOL_MSN: + return R.string.chat_msn; + case Im.PROTOCOL_YAHOO: + return R.string.chat_yahoo; + case Im.PROTOCOL_SKYPE: + return R.string.chat_skype; + case Im.PROTOCOL_QQ: + return R.string.chat_qq; + case Im.PROTOCOL_GOOGLE_TALK: + return R.string.chat_gtalk; + case Im.PROTOCOL_ICQ: + return R.string.chat_icq; + case Im.PROTOCOL_JABBER: + return R.string.chat_jabber; + case Im.PROTOCOL_NETMEETING: + return R.string.chat; + default: + return R.string.chat; + } + } + } + + // TODO Extract it to its own class, and move all KindBuilders to it as well. + private static class KindParser { + + public static final KindParser INSTANCE = new KindParser(); + + private final Map mBuilders = new ArrayMap<>(); + + private KindParser() { + addBuilder(new NameKindBuilder()); + addBuilder(new NicknameKindBuilder()); + addBuilder(new PhoneKindBuilder()); + addBuilder(new EmailKindBuilder()); + addBuilder(new StructuredPostalKindBuilder()); + addBuilder(new ImKindBuilder()); + addBuilder(new OrganizationKindBuilder()); + addBuilder(new PhotoKindBuilder()); + addBuilder(new NoteKindBuilder()); + addBuilder(new WebsiteKindBuilder()); + addBuilder(new SipAddressKindBuilder()); + addBuilder(new GroupMembershipKindBuilder()); + addBuilder(new EventKindBuilder()); + addBuilder(new RelationshipKindBuilder()); + } + + private void addBuilder(KindBuilder builder) { + mBuilders.put(builder.getTagName(), builder); + } + + /** + * Takes a {@link XmlPullParser} at the start of a DataKind tag, parses it and returns {@link + * DataKind}s. (Usually just one, but there are three for the "name" kind.) + * + *

This method returns a list, because we need to add 3 kinds for the name data kind. + * (structured, display and phonetic) + */ + public List parseDataKindTag( + Context context, XmlPullParser parser, AttributeSet attrs) + throws DefinitionException, XmlPullParserException, IOException { + final String kind = getAttr(attrs, Attr.KIND); + final KindBuilder builder = mBuilders.get(kind); + if (builder != null) { + return builder.parseDataKind(context, parser, attrs); + } else { + throw new DefinitionException("Undefined data kind '" + kind + "'"); + } + } + } + + private abstract static class KindBuilder { + + public abstract String getTagName(); + + /** DataKind tag parser specific to each kind. Subclasses must implement it. */ + public abstract List parseDataKind( + Context context, XmlPullParser parser, AttributeSet attrs) + throws DefinitionException, XmlPullParserException, IOException; + + /** Creates a new {@link DataKind}, and also parses the child Type tags in the DataKind tag. */ + protected final DataKind newDataKind( + Context context, + XmlPullParser parser, + AttributeSet attrs, + boolean isPseudo, + String mimeType, + String typeColumn, + int titleRes, + int weight, + StringInflater actionHeader, + StringInflater actionBody) + throws DefinitionException, XmlPullParserException, IOException { + + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Adding DataKind: " + mimeType); + } + + final DataKind kind = new DataKind(mimeType, titleRes, weight, true); + kind.typeColumn = typeColumn; + kind.actionHeader = actionHeader; + kind.actionBody = actionBody; + kind.fieldList = new ArrayList<>(); + + // Get more information from the tag... + // A pseudo data kind doesn't have corresponding tag the XML, so we skip this. + if (!isPseudo) { + kind.typeOverallMax = getAttr(attrs, Attr.MAX_OCCURRENCE, -1); + + // Process "Type" tags. + // If a kind has the type column, contacts.xml must have at least one type + // definition. Otherwise, it mustn't have a type definition. + if (kind.typeColumn != null) { + // Parse and add types. + kind.typeList = new ArrayList<>(); + parseTypes(context, parser, attrs, kind, true); + if (kind.typeList.size() == 0) { + throw new DefinitionException("Kind " + kind.mimeType + " must have at least one type"); + } + } else { + // Make sure it has no types. + parseTypes(context, parser, attrs, kind, false /* can't have types */); + } + } + + return kind; + } + + /** + * Parses Type elements in a DataKind element, and if {@code canHaveTypes} is true adds them to + * the given {@link DataKind}. Otherwise the {@link DataKind} can't have a type, so throws + * {@link DefinitionException}. + */ + private void parseTypes( + Context context, + XmlPullParser parser, + AttributeSet attrs, + DataKind kind, + boolean canHaveTypes) + throws DefinitionException, XmlPullParserException, IOException { + final int outerDepth = parser.getDepth(); + int type; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { + final int depth = parser.getDepth(); + if (type != XmlPullParser.START_TAG || depth != outerDepth + 1) { + continue; // Not direct child tag + } + + final String tag = parser.getName(); + if (Tag.TYPE.equals(tag)) { + if (canHaveTypes) { + kind.typeList.add(parseTypeTag(parser, attrs, kind)); + } else { + throw new DefinitionException("Kind " + kind.mimeType + " can't have types"); + } + } else { + throw new DefinitionException("Unknown tag: " + tag); + } + } + } + + /** + * Parses a single Type element and returns an {@link EditType} built from it. Uses {@link + * #buildEditTypeForTypeTag} defined in subclasses to actually build an {@link EditType}. + */ + private EditType parseTypeTag(XmlPullParser parser, AttributeSet attrs, DataKind kind) + throws DefinitionException { + + final String typeName = getAttr(attrs, Attr.TYPE); + + final EditType et = buildEditTypeForTypeTag(attrs, typeName); + if (et == null) { + throw new DefinitionException( + "Undefined type '" + typeName + "' for data kind '" + kind.mimeType + "'"); + } + et.specificMax = getAttr(attrs, Attr.MAX_OCCURRENCE, -1); + + return et; + } + + /** + * Returns an {@link EditType} for the given "type". Subclasses may optionally use the + * attributes in the tag to set optional values. (e.g. "yearOptional" for the event kind) + */ + protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) { + return null; + } + + protected final void throwIfList(DataKind kind) throws DefinitionException { + if (kind.typeOverallMax != 1) { + throw new DefinitionException("Kind " + kind.mimeType + " must have 'overallMax=\"1\"'"); + } + } + } + + /** DataKind parser for Name. (structured, display, phonetic) */ + private static class NameKindBuilder extends KindBuilder { + + private static void checkAttributeTrue(boolean value, String attrName) + throws DefinitionException { + if (!value) { + throw new DefinitionException(attrName + " must be true"); + } + } + + @Override + public String getTagName() { + return "name"; + } + + @Override + public List parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs) + throws DefinitionException, XmlPullParserException, IOException { + + // Build 3 data kinds: + // - StructuredName.CONTENT_ITEM_TYPE + // - DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME + // - DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME + + final boolean displayOrderPrimary = + context.getResources().getBoolean(R.bool.config_editor_field_order_primary); + + final boolean supportsDisplayName = getAttr(attrs, "supportsDisplayName", false); + final boolean supportsPrefix = getAttr(attrs, "supportsPrefix", false); + final boolean supportsMiddleName = getAttr(attrs, "supportsMiddleName", false); + final boolean supportsSuffix = getAttr(attrs, "supportsSuffix", false); + final boolean supportsPhoneticFamilyName = + getAttr(attrs, "supportsPhoneticFamilyName", false); + final boolean supportsPhoneticMiddleName = + getAttr(attrs, "supportsPhoneticMiddleName", false); + final boolean supportsPhoneticGivenName = getAttr(attrs, "supportsPhoneticGivenName", false); + + // For now, every things must be supported. + checkAttributeTrue(supportsDisplayName, "supportsDisplayName"); + checkAttributeTrue(supportsPrefix, "supportsPrefix"); + checkAttributeTrue(supportsMiddleName, "supportsMiddleName"); + checkAttributeTrue(supportsSuffix, "supportsSuffix"); + checkAttributeTrue(supportsPhoneticFamilyName, "supportsPhoneticFamilyName"); + checkAttributeTrue(supportsPhoneticMiddleName, "supportsPhoneticMiddleName"); + checkAttributeTrue(supportsPhoneticGivenName, "supportsPhoneticGivenName"); + + final List kinds = new ArrayList<>(); + + // Structured name + final DataKind ks = + newDataKind( + context, + parser, + attrs, + false, + StructuredName.CONTENT_ITEM_TYPE, + null, + R.string.nameLabelsGroup, + Weight.NONE, + new SimpleInflater(R.string.nameLabelsGroup), + new SimpleInflater(Nickname.NAME)); + + throwIfList(ks); + kinds.add(ks); + + // Note about setLongForm/setShortForm below. + // We need to set this only when the type supports display name. (=supportsDisplayName) + // Otherwise (i.e. Exchange) we don't set these flags, but instead make some fields + // "optional". + + ks.fieldList.add( + new EditField(StructuredName.DISPLAY_NAME, R.string.full_name, FLAGS_PERSON_NAME)); + ks.fieldList.add( + new EditField(StructuredName.PREFIX, R.string.name_prefix, FLAGS_PERSON_NAME) + .setLongForm(true)); + ks.fieldList.add( + new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME) + .setLongForm(true)); + ks.fieldList.add( + new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME) + .setLongForm(true)); + ks.fieldList.add( + new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME) + .setLongForm(true)); + ks.fieldList.add( + new EditField(StructuredName.SUFFIX, R.string.name_suffix, FLAGS_PERSON_NAME) + .setLongForm(true)); + ks.fieldList.add( + new EditField( + StructuredName.PHONETIC_FAMILY_NAME, R.string.name_phonetic_family, FLAGS_PHONETIC)); + ks.fieldList.add( + new EditField( + StructuredName.PHONETIC_MIDDLE_NAME, R.string.name_phonetic_middle, FLAGS_PHONETIC)); + ks.fieldList.add( + new EditField( + StructuredName.PHONETIC_GIVEN_NAME, R.string.name_phonetic_given, FLAGS_PHONETIC)); + + // Display name + final DataKind kd = + newDataKind( + context, + parser, + attrs, + true, + DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME, + null, + R.string.nameLabelsGroup, + Weight.NONE, + new SimpleInflater(R.string.nameLabelsGroup), + new SimpleInflater(Nickname.NAME)); + kd.typeOverallMax = 1; + kinds.add(kd); + + kd.fieldList.add( + new EditField(StructuredName.DISPLAY_NAME, R.string.full_name, FLAGS_PERSON_NAME) + .setShortForm(true)); + + if (!displayOrderPrimary) { + kd.fieldList.add( + new EditField(StructuredName.PREFIX, R.string.name_prefix, FLAGS_PERSON_NAME) + .setLongForm(true)); + kd.fieldList.add( + new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME) + .setLongForm(true)); + kd.fieldList.add( + new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME) + .setLongForm(true)); + kd.fieldList.add( + new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME) + .setLongForm(true)); + kd.fieldList.add( + new EditField(StructuredName.SUFFIX, R.string.name_suffix, FLAGS_PERSON_NAME) + .setLongForm(true)); + } else { + kd.fieldList.add( + new EditField(StructuredName.PREFIX, R.string.name_prefix, FLAGS_PERSON_NAME) + .setLongForm(true)); + kd.fieldList.add( + new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME) + .setLongForm(true)); + kd.fieldList.add( + new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME) + .setLongForm(true)); + kd.fieldList.add( + new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME) + .setLongForm(true)); + kd.fieldList.add( + new EditField(StructuredName.SUFFIX, R.string.name_suffix, FLAGS_PERSON_NAME) + .setLongForm(true)); + } + + // Phonetic name + final DataKind kp = + newDataKind( + context, + parser, + attrs, + true, + DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME, + null, + R.string.name_phonetic, + Weight.NONE, + new SimpleInflater(R.string.nameLabelsGroup), + new SimpleInflater(Nickname.NAME)); + kp.typeOverallMax = 1; + kinds.add(kp); + + // We may want to change the order depending on displayOrderPrimary too. + kp.fieldList.add( + new EditField( + DataKind.PSEUDO_COLUMN_PHONETIC_NAME, R.string.name_phonetic, FLAGS_PHONETIC) + .setShortForm(true)); + kp.fieldList.add( + new EditField( + StructuredName.PHONETIC_FAMILY_NAME, + R.string.name_phonetic_family, + FLAGS_PHONETIC) + .setLongForm(true)); + kp.fieldList.add( + new EditField( + StructuredName.PHONETIC_MIDDLE_NAME, + R.string.name_phonetic_middle, + FLAGS_PHONETIC) + .setLongForm(true)); + kp.fieldList.add( + new EditField( + StructuredName.PHONETIC_GIVEN_NAME, R.string.name_phonetic_given, FLAGS_PHONETIC) + .setLongForm(true)); + return kinds; + } + } + + private static class NicknameKindBuilder extends KindBuilder { + + @Override + public String getTagName() { + return "nickname"; + } + + @Override + public List parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs) + throws DefinitionException, XmlPullParserException, IOException { + final DataKind kind = + newDataKind( + context, + parser, + attrs, + false, + Nickname.CONTENT_ITEM_TYPE, + null, + R.string.nicknameLabelsGroup, + Weight.NICKNAME, + new SimpleInflater(R.string.nicknameLabelsGroup), + new SimpleInflater(Nickname.NAME)); + + kind.fieldList.add( + new EditField(Nickname.NAME, R.string.nicknameLabelsGroup, FLAGS_PERSON_NAME)); + + kind.defaultValues = new ContentValues(); + kind.defaultValues.put(Nickname.TYPE, Nickname.TYPE_DEFAULT); + + throwIfList(kind); + List result = new ArrayList<>(); + result.add(kind); + return result; + } + } + + private static class PhoneKindBuilder extends KindBuilder { + + /** Just to avoid line-wrapping... */ + protected static EditType build(int type, boolean secondary) { + return new EditType(type, Phone.getTypeLabelResource(type)).setSecondary(secondary); + } + + @Override + public String getTagName() { + return "phone"; + } + + @Override + public List parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs) + throws DefinitionException, XmlPullParserException, IOException { + final DataKind kind = + newDataKind( + context, + parser, + attrs, + false, + Phone.CONTENT_ITEM_TYPE, + Phone.TYPE, + R.string.phoneLabelsGroup, + Weight.PHONE, + new PhoneActionInflater(), + new SimpleInflater(Phone.NUMBER)); + + kind.iconAltRes = R.drawable.ic_message_24dp; + kind.iconAltDescriptionRes = R.string.sms; + kind.actionAltHeader = new PhoneActionAltInflater(); + + kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE)); + + List result = new ArrayList<>(); + result.add(kind); + return result; + } + + @Override + protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) { + if ("home".equals(type)) { + return build(Phone.TYPE_HOME, false); + } + if ("mobile".equals(type)) { + return build(Phone.TYPE_MOBILE, false); + } + if ("work".equals(type)) { + return build(Phone.TYPE_WORK, false); + } + if ("fax_work".equals(type)) { + return build(Phone.TYPE_FAX_WORK, true); + } + if ("fax_home".equals(type)) { + return build(Phone.TYPE_FAX_HOME, true); + } + if ("pager".equals(type)) { + return build(Phone.TYPE_PAGER, true); + } + if ("other".equals(type)) { + return build(Phone.TYPE_OTHER, false); + } + if ("callback".equals(type)) { + return build(Phone.TYPE_CALLBACK, true); + } + if ("car".equals(type)) { + return build(Phone.TYPE_CAR, true); + } + if ("company_main".equals(type)) { + return build(Phone.TYPE_COMPANY_MAIN, true); + } + if ("isdn".equals(type)) { + return build(Phone.TYPE_ISDN, true); + } + if ("main".equals(type)) { + return build(Phone.TYPE_MAIN, true); + } + if ("other_fax".equals(type)) { + return build(Phone.TYPE_OTHER_FAX, true); + } + if ("radio".equals(type)) { + return build(Phone.TYPE_RADIO, true); + } + if ("telex".equals(type)) { + return build(Phone.TYPE_TELEX, true); + } + if ("tty_tdd".equals(type)) { + return build(Phone.TYPE_TTY_TDD, true); + } + if ("work_mobile".equals(type)) { + return build(Phone.TYPE_WORK_MOBILE, true); + } + if ("work_pager".equals(type)) { + return build(Phone.TYPE_WORK_PAGER, true); + } + + // Note "assistant" used to be a custom column for the fallback type, but not anymore. + if ("assistant".equals(type)) { + return build(Phone.TYPE_ASSISTANT, true); + } + if ("mms".equals(type)) { + return build(Phone.TYPE_MMS, true); + } + if ("custom".equals(type)) { + return build(Phone.TYPE_CUSTOM, true).setCustomColumn(Phone.LABEL); + } + return null; + } + } + + private static class EmailKindBuilder extends KindBuilder { + + @Override + public String getTagName() { + return "email"; + } + + @Override + public List parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs) + throws DefinitionException, XmlPullParserException, IOException { + final DataKind kind = + newDataKind( + context, + parser, + attrs, + false, + Email.CONTENT_ITEM_TYPE, + Email.TYPE, + R.string.emailLabelsGroup, + Weight.EMAIL, + new EmailActionInflater(), + new SimpleInflater(Email.DATA)); + kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL)); + + List result = new ArrayList<>(); + result.add(kind); + return result; + } + + @Override + protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) { + // EditType is mutable, so we need to create a new instance every time. + if ("home".equals(type)) { + return buildEmailType(Email.TYPE_HOME); + } + if ("work".equals(type)) { + return buildEmailType(Email.TYPE_WORK); + } + if ("other".equals(type)) { + return buildEmailType(Email.TYPE_OTHER); + } + if ("mobile".equals(type)) { + return buildEmailType(Email.TYPE_MOBILE); + } + if ("custom".equals(type)) { + return buildEmailType(Email.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Email.LABEL); + } + return null; + } + } + + private static class StructuredPostalKindBuilder extends KindBuilder { + + @Override + public String getTagName() { + return "postal"; + } + + @Override + public List parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs) + throws DefinitionException, XmlPullParserException, IOException { + final DataKind kind = + newDataKind( + context, + parser, + attrs, + false, + StructuredPostal.CONTENT_ITEM_TYPE, + StructuredPostal.TYPE, + R.string.postalLabelsGroup, + Weight.STRUCTURED_POSTAL, + new PostalActionInflater(), + new SimpleInflater(StructuredPostal.FORMATTED_ADDRESS)); + + if (getAttr(attrs, "needsStructured", false)) { + if (Locale.JAPANESE.getLanguage().equals(Locale.getDefault().getLanguage())) { + // Japanese order + kind.fieldList.add( + new EditField(StructuredPostal.COUNTRY, R.string.postal_country, FLAGS_POSTAL) + .setOptional(true)); + kind.fieldList.add( + new EditField(StructuredPostal.POSTCODE, R.string.postal_postcode, FLAGS_POSTAL)); + kind.fieldList.add( + new EditField(StructuredPostal.REGION, R.string.postal_region, FLAGS_POSTAL)); + kind.fieldList.add( + new EditField(StructuredPostal.CITY, R.string.postal_city, FLAGS_POSTAL)); + kind.fieldList.add( + new EditField(StructuredPostal.STREET, R.string.postal_street, FLAGS_POSTAL)); + } else { + // Generic order + kind.fieldList.add( + new EditField(StructuredPostal.STREET, R.string.postal_street, FLAGS_POSTAL)); + kind.fieldList.add( + new EditField(StructuredPostal.CITY, R.string.postal_city, FLAGS_POSTAL)); + kind.fieldList.add( + new EditField(StructuredPostal.REGION, R.string.postal_region, FLAGS_POSTAL)); + kind.fieldList.add( + new EditField(StructuredPostal.POSTCODE, R.string.postal_postcode, FLAGS_POSTAL)); + kind.fieldList.add( + new EditField(StructuredPostal.COUNTRY, R.string.postal_country, FLAGS_POSTAL) + .setOptional(true)); + } + } else { + kind.maxLinesForDisplay = MAX_LINES_FOR_POSTAL_ADDRESS; + kind.fieldList.add( + new EditField( + StructuredPostal.FORMATTED_ADDRESS, R.string.postal_address, FLAGS_POSTAL)); + } + + List result = new ArrayList<>(); + result.add(kind); + return result; + } + + @Override + protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) { + // EditType is mutable, so we need to create a new instance every time. + if ("home".equals(type)) { + return buildPostalType(StructuredPostal.TYPE_HOME); + } + if ("work".equals(type)) { + return buildPostalType(StructuredPostal.TYPE_WORK); + } + if ("other".equals(type)) { + return buildPostalType(StructuredPostal.TYPE_OTHER); + } + if ("custom".equals(type)) { + return buildPostalType(StructuredPostal.TYPE_CUSTOM) + .setSecondary(true) + .setCustomColumn(Email.LABEL); + } + return null; + } + } + + private static class ImKindBuilder extends KindBuilder { + + @Override + public String getTagName() { + return "im"; + } + + @Override + public List parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs) + throws DefinitionException, XmlPullParserException, IOException { + + // IM is special: + // - It uses "protocol" as the custom label field + // - Its TYPE is fixed to TYPE_OTHER + + final DataKind kind = + newDataKind( + context, + parser, + attrs, + false, + Im.CONTENT_ITEM_TYPE, + Im.PROTOCOL, + R.string.imLabelsGroup, + Weight.IM, + new ImActionInflater(), + new SimpleInflater(Im.DATA) // header / action + ); + kind.fieldList.add(new EditField(Im.DATA, R.string.imLabelsGroup, FLAGS_EMAIL)); + + kind.defaultValues = new ContentValues(); + kind.defaultValues.put(Im.TYPE, Im.TYPE_OTHER); + + List result = new ArrayList<>(); + result.add(kind); + return result; + } + + @Override + protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) { + if ("aim".equals(type)) { + return buildImType(Im.PROTOCOL_AIM); + } + if ("msn".equals(type)) { + return buildImType(Im.PROTOCOL_MSN); + } + if ("yahoo".equals(type)) { + return buildImType(Im.PROTOCOL_YAHOO); + } + if ("skype".equals(type)) { + return buildImType(Im.PROTOCOL_SKYPE); + } + if ("qq".equals(type)) { + return buildImType(Im.PROTOCOL_QQ); + } + if ("google_talk".equals(type)) { + return buildImType(Im.PROTOCOL_GOOGLE_TALK); + } + if ("icq".equals(type)) { + return buildImType(Im.PROTOCOL_ICQ); + } + if ("jabber".equals(type)) { + return buildImType(Im.PROTOCOL_JABBER); + } + if ("custom".equals(type)) { + return buildImType(Im.PROTOCOL_CUSTOM) + .setSecondary(true) + .setCustomColumn(Im.CUSTOM_PROTOCOL); + } + return null; + } + } + + private static class OrganizationKindBuilder extends KindBuilder { + + @Override + public String getTagName() { + return "organization"; + } + + @Override + public List parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs) + throws DefinitionException, XmlPullParserException, IOException { + final DataKind kind = + newDataKind( + context, + parser, + attrs, + false, + Organization.CONTENT_ITEM_TYPE, + null, + R.string.organizationLabelsGroup, + Weight.ORGANIZATION, + new SimpleInflater(R.string.organizationLabelsGroup), + ORGANIZATION_BODY_INFLATER); + + kind.fieldList.add( + new EditField(Organization.COMPANY, R.string.ghostData_company, FLAGS_GENERIC_NAME)); + kind.fieldList.add( + new EditField(Organization.TITLE, R.string.ghostData_title, FLAGS_GENERIC_NAME)); + + throwIfList(kind); + + List result = new ArrayList<>(); + result.add(kind); + return result; + } + } + + private static class PhotoKindBuilder extends KindBuilder { + + @Override + public String getTagName() { + return "photo"; + } + + @Override + public List parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs) + throws DefinitionException, XmlPullParserException, IOException { + final DataKind kind = + newDataKind( + context, + parser, + attrs, + false, + Photo.CONTENT_ITEM_TYPE, + null /* no type */, + Weight.NONE, + -1, + null, + null // no header, no body + ); + + kind.fieldList.add(new EditField(Photo.PHOTO, -1, -1)); + + throwIfList(kind); + + List result = new ArrayList<>(); + result.add(kind); + return result; + } + } + + private static class NoteKindBuilder extends KindBuilder { + + @Override + public String getTagName() { + return "note"; + } + + @Override + public List parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs) + throws DefinitionException, XmlPullParserException, IOException { + final DataKind kind = + newDataKind( + context, + parser, + attrs, + false, + Note.CONTENT_ITEM_TYPE, + null, + R.string.label_notes, + Weight.NOTE, + new SimpleInflater(R.string.label_notes), + new SimpleInflater(Note.NOTE)); + + kind.fieldList.add(new EditField(Note.NOTE, R.string.label_notes, FLAGS_NOTE)); + kind.maxLinesForDisplay = MAX_LINES_FOR_NOTE; + + throwIfList(kind); + + List result = new ArrayList<>(); + result.add(kind); + return result; + } + } + + private static class WebsiteKindBuilder extends KindBuilder { + + @Override + public String getTagName() { + return "website"; + } + + @Override + public List parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs) + throws DefinitionException, XmlPullParserException, IOException { + final DataKind kind = + newDataKind( + context, + parser, + attrs, + false, + Website.CONTENT_ITEM_TYPE, + null, + R.string.websiteLabelsGroup, + Weight.WEBSITE, + new SimpleInflater(R.string.websiteLabelsGroup), + new SimpleInflater(Website.URL)); + + kind.fieldList.add(new EditField(Website.URL, R.string.websiteLabelsGroup, FLAGS_WEBSITE)); + + kind.defaultValues = new ContentValues(); + kind.defaultValues.put(Website.TYPE, Website.TYPE_OTHER); + + List result = new ArrayList<>(); + result.add(kind); + return result; + } + } + + private static class SipAddressKindBuilder extends KindBuilder { + + @Override + public String getTagName() { + return "sip_address"; + } + + @Override + public List parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs) + throws DefinitionException, XmlPullParserException, IOException { + final DataKind kind = + newDataKind( + context, + parser, + attrs, + false, + SipAddress.CONTENT_ITEM_TYPE, + null, + R.string.label_sip_address, + Weight.SIP_ADDRESS, + new SimpleInflater(R.string.label_sip_address), + new SimpleInflater(SipAddress.SIP_ADDRESS)); + + kind.fieldList.add( + new EditField(SipAddress.SIP_ADDRESS, R.string.label_sip_address, FLAGS_SIP_ADDRESS)); + + throwIfList(kind); + + List result = new ArrayList<>(); + result.add(kind); + return result; + } + } + + private static class GroupMembershipKindBuilder extends KindBuilder { + + @Override + public String getTagName() { + return "group_membership"; + } + + @Override + public List parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs) + throws DefinitionException, XmlPullParserException, IOException { + final DataKind kind = + newDataKind( + context, + parser, + attrs, + false, + GroupMembership.CONTENT_ITEM_TYPE, + null, + R.string.groupsLabel, + Weight.GROUP_MEMBERSHIP, + null, + null); + + kind.fieldList.add(new EditField(GroupMembership.GROUP_ROW_ID, -1, -1)); + kind.maxLinesForDisplay = MAX_LINES_FOR_GROUP; + + throwIfList(kind); + + List result = new ArrayList<>(); + result.add(kind); + return result; + } + } + + /** + * Event DataKind parser. + * + *

Event DataKind is used only for Google/Exchange types, so this parser is not used for now. + */ + private static class EventKindBuilder extends KindBuilder { + + @Override + public String getTagName() { + return "event"; + } + + @Override + public List parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs) + throws DefinitionException, XmlPullParserException, IOException { + final DataKind kind = + newDataKind( + context, + parser, + attrs, + false, + Event.CONTENT_ITEM_TYPE, + Event.TYPE, + R.string.eventLabelsGroup, + Weight.EVENT, + new EventActionInflater(), + new SimpleInflater(Event.START_DATE)); + + kind.fieldList.add(new EditField(Event.DATA, R.string.eventLabelsGroup, FLAGS_EVENT)); + + if (getAttr(attrs, Attr.DATE_WITH_TIME, false)) { + kind.dateFormatWithoutYear = CommonDateUtils.NO_YEAR_DATE_AND_TIME_FORMAT; + kind.dateFormatWithYear = CommonDateUtils.DATE_AND_TIME_FORMAT; + } else { + kind.dateFormatWithoutYear = CommonDateUtils.NO_YEAR_DATE_FORMAT; + kind.dateFormatWithYear = CommonDateUtils.FULL_DATE_FORMAT; + } + + List result = new ArrayList<>(); + result.add(kind); + return result; + } + + @Override + protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) { + final boolean yo = getAttr(attrs, Attr.YEAR_OPTIONAL, false); + + if ("birthday".equals(type)) { + return buildEventType(Event.TYPE_BIRTHDAY, yo).setSpecificMax(1); + } + if ("anniversary".equals(type)) { + return buildEventType(Event.TYPE_ANNIVERSARY, yo); + } + if ("other".equals(type)) { + return buildEventType(Event.TYPE_OTHER, yo); + } + if ("custom".equals(type)) { + return buildEventType(Event.TYPE_CUSTOM, yo) + .setSecondary(true) + .setCustomColumn(Event.LABEL); + } + return null; + } + } + + /** + * Relationship DataKind parser. + * + *

Relationship DataKind is used only for Google/Exchange types, so this parser is not used for + * now. + */ + private static class RelationshipKindBuilder extends KindBuilder { + + @Override + public String getTagName() { + return "relationship"; + } + + @Override + public List parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs) + throws DefinitionException, XmlPullParserException, IOException { + final DataKind kind = + newDataKind( + context, + parser, + attrs, + false, + Relation.CONTENT_ITEM_TYPE, + Relation.TYPE, + R.string.relationLabelsGroup, + Weight.RELATIONSHIP, + new RelationActionInflater(), + new SimpleInflater(Relation.NAME)); + + kind.fieldList.add( + new EditField(Relation.DATA, R.string.relationLabelsGroup, FLAGS_RELATION)); + + kind.defaultValues = new ContentValues(); + kind.defaultValues.put(Relation.TYPE, Relation.TYPE_SPOUSE); + + List result = new ArrayList<>(); + result.add(kind); + return result; + } + + @Override + protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) { + // EditType is mutable, so we need to create a new instance every time. + if ("assistant".equals(type)) { + return buildRelationType(Relation.TYPE_ASSISTANT); + } + if ("brother".equals(type)) { + return buildRelationType(Relation.TYPE_BROTHER); + } + if ("child".equals(type)) { + return buildRelationType(Relation.TYPE_CHILD); + } + if ("domestic_partner".equals(type)) { + return buildRelationType(Relation.TYPE_DOMESTIC_PARTNER); + } + if ("father".equals(type)) { + return buildRelationType(Relation.TYPE_FATHER); + } + if ("friend".equals(type)) { + return buildRelationType(Relation.TYPE_FRIEND); + } + if ("manager".equals(type)) { + return buildRelationType(Relation.TYPE_MANAGER); + } + if ("mother".equals(type)) { + return buildRelationType(Relation.TYPE_MOTHER); + } + if ("parent".equals(type)) { + return buildRelationType(Relation.TYPE_PARENT); + } + if ("partner".equals(type)) { + return buildRelationType(Relation.TYPE_PARTNER); + } + if ("referred_by".equals(type)) { + return buildRelationType(Relation.TYPE_REFERRED_BY); + } + if ("relative".equals(type)) { + return buildRelationType(Relation.TYPE_RELATIVE); + } + if ("sister".equals(type)) { + return buildRelationType(Relation.TYPE_SISTER); + } + if ("spouse".equals(type)) { + return buildRelationType(Relation.TYPE_SPOUSE); + } + if ("custom".equals(type)) { + return buildRelationType(Relation.TYPE_CUSTOM) + .setSecondary(true) + .setCustomColumn(Relation.LABEL); + } + return null; + } + } +} diff --git a/java/com/android/contacts/common/model/account/ExchangeAccountType.java b/java/com/android/contacts/common/model/account/ExchangeAccountType.java new file mode 100644 index 000000000..a27028e80 --- /dev/null +++ b/java/com/android/contacts/common/model/account/ExchangeAccountType.java @@ -0,0 +1,365 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model.account; + +import android.content.ContentValues; +import android.content.Context; +import android.provider.ContactsContract.CommonDataKinds.Email; +import android.provider.ContactsContract.CommonDataKinds.Event; +import android.provider.ContactsContract.CommonDataKinds.Im; +import android.provider.ContactsContract.CommonDataKinds.Nickname; +import android.provider.ContactsContract.CommonDataKinds.Note; +import android.provider.ContactsContract.CommonDataKinds.Organization; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.CommonDataKinds.Photo; +import android.provider.ContactsContract.CommonDataKinds.StructuredName; +import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; +import android.provider.ContactsContract.CommonDataKinds.Website; +import android.util.Log; +import com.android.contacts.common.R; +import com.android.contacts.common.model.dataitem.DataKind; +import com.android.contacts.common.util.CommonDateUtils; +import java.util.ArrayList; +import java.util.Locale; + +public class ExchangeAccountType extends BaseAccountType { + + private static final String TAG = "ExchangeAccountType"; + + private static final String ACCOUNT_TYPE_AOSP = "com.android.exchange"; + private static final String ACCOUNT_TYPE_GOOGLE_1 = "com.google.android.exchange"; + private static final String ACCOUNT_TYPE_GOOGLE_2 = "com.google.android.gm.exchange"; + + public ExchangeAccountType(Context context, String authenticatorPackageName, String type) { + this.accountType = type; + this.resourcePackageName = null; + this.syncAdapterPackageName = authenticatorPackageName; + + try { + addDataKindStructuredName(context); + addDataKindDisplayName(context); + addDataKindPhoneticName(context); + addDataKindNickname(context); + addDataKindPhone(context); + addDataKindEmail(context); + addDataKindStructuredPostal(context); + addDataKindIm(context); + addDataKindOrganization(context); + addDataKindPhoto(context); + addDataKindNote(context); + addDataKindEvent(context); + addDataKindWebsite(context); + addDataKindGroupMembership(context); + + mIsInitialized = true; + } catch (DefinitionException e) { + Log.e(TAG, "Problem building account type", e); + } + } + + public static boolean isExchangeType(String type) { + return ACCOUNT_TYPE_AOSP.equals(type) + || ACCOUNT_TYPE_GOOGLE_1.equals(type) + || ACCOUNT_TYPE_GOOGLE_2.equals(type); + } + + @Override + protected DataKind addDataKindStructuredName(Context context) throws DefinitionException { + DataKind kind = + addKind( + new DataKind( + StructuredName.CONTENT_ITEM_TYPE, R.string.nameLabelsGroup, Weight.NONE, true)); + kind.actionHeader = new SimpleInflater(R.string.nameLabelsGroup); + kind.actionBody = new SimpleInflater(Nickname.NAME); + + kind.typeOverallMax = 1; + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add( + new EditField(StructuredName.PREFIX, R.string.name_prefix, FLAGS_PERSON_NAME) + .setOptional(true)); + kind.fieldList.add( + new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME)); + kind.fieldList.add( + new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME)); + kind.fieldList.add( + new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME)); + kind.fieldList.add( + new EditField(StructuredName.SUFFIX, R.string.name_suffix, FLAGS_PERSON_NAME)); + + kind.fieldList.add( + new EditField( + StructuredName.PHONETIC_FAMILY_NAME, R.string.name_phonetic_family, FLAGS_PHONETIC)); + kind.fieldList.add( + new EditField( + StructuredName.PHONETIC_GIVEN_NAME, R.string.name_phonetic_given, FLAGS_PHONETIC)); + + return kind; + } + + @Override + protected DataKind addDataKindDisplayName(Context context) throws DefinitionException { + DataKind kind = + addKind( + new DataKind( + DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME, + R.string.nameLabelsGroup, + Weight.NONE, + true)); + + boolean displayOrderPrimary = + context.getResources().getBoolean(R.bool.config_editor_field_order_primary); + kind.typeOverallMax = 1; + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add( + new EditField(StructuredName.PREFIX, R.string.name_prefix, FLAGS_PERSON_NAME) + .setOptional(true)); + if (!displayOrderPrimary) { + kind.fieldList.add( + new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME)); + kind.fieldList.add( + new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME) + .setOptional(true)); + kind.fieldList.add( + new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME)); + } else { + kind.fieldList.add( + new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME)); + kind.fieldList.add( + new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME) + .setOptional(true)); + kind.fieldList.add( + new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME)); + } + kind.fieldList.add( + new EditField(StructuredName.SUFFIX, R.string.name_suffix, FLAGS_PERSON_NAME) + .setOptional(true)); + + return kind; + } + + @Override + protected DataKind addDataKindPhoneticName(Context context) throws DefinitionException { + DataKind kind = + addKind( + new DataKind( + DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME, + R.string.name_phonetic, + Weight.NONE, + true)); + kind.actionHeader = new SimpleInflater(R.string.nameLabelsGroup); + kind.actionBody = new SimpleInflater(Nickname.NAME); + + kind.typeOverallMax = 1; + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add( + new EditField( + StructuredName.PHONETIC_FAMILY_NAME, R.string.name_phonetic_family, FLAGS_PHONETIC)); + kind.fieldList.add( + new EditField( + StructuredName.PHONETIC_GIVEN_NAME, R.string.name_phonetic_given, FLAGS_PHONETIC)); + + return kind; + } + + @Override + protected DataKind addDataKindNickname(Context context) throws DefinitionException { + final DataKind kind = super.addDataKindNickname(context); + + kind.typeOverallMax = 1; + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add( + new EditField(Nickname.NAME, R.string.nicknameLabelsGroup, FLAGS_PERSON_NAME)); + + return kind; + } + + @Override + protected DataKind addDataKindPhone(Context context) throws DefinitionException { + final DataKind kind = super.addDataKindPhone(context); + + kind.typeColumn = Phone.TYPE; + kind.typeList = new ArrayList<>(); + kind.typeList.add(buildPhoneType(Phone.TYPE_MOBILE).setSpecificMax(1)); + kind.typeList.add(buildPhoneType(Phone.TYPE_HOME).setSpecificMax(2)); + kind.typeList.add(buildPhoneType(Phone.TYPE_WORK).setSpecificMax(2)); + kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_WORK).setSecondary(true).setSpecificMax(1)); + kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_HOME).setSecondary(true).setSpecificMax(1)); + kind.typeList.add(buildPhoneType(Phone.TYPE_PAGER).setSecondary(true).setSpecificMax(1)); + kind.typeList.add(buildPhoneType(Phone.TYPE_CAR).setSecondary(true).setSpecificMax(1)); + kind.typeList.add(buildPhoneType(Phone.TYPE_COMPANY_MAIN).setSecondary(true).setSpecificMax(1)); + kind.typeList.add(buildPhoneType(Phone.TYPE_MMS).setSecondary(true).setSpecificMax(1)); + kind.typeList.add(buildPhoneType(Phone.TYPE_RADIO).setSecondary(true).setSpecificMax(1)); + kind.typeList.add(buildPhoneType(Phone.TYPE_ASSISTANT).setSecondary(true).setSpecificMax(1)); + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE)); + + return kind; + } + + @Override + protected DataKind addDataKindEmail(Context context) throws DefinitionException { + final DataKind kind = super.addDataKindEmail(context); + + kind.typeOverallMax = 3; + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL)); + + return kind; + } + + @Override + protected DataKind addDataKindStructuredPostal(Context context) throws DefinitionException { + final DataKind kind = super.addDataKindStructuredPostal(context); + + final boolean useJapaneseOrder = + Locale.JAPANESE.getLanguage().equals(Locale.getDefault().getLanguage()); + kind.typeColumn = StructuredPostal.TYPE; + kind.typeList = new ArrayList<>(); + kind.typeList.add(buildPostalType(StructuredPostal.TYPE_WORK).setSpecificMax(1)); + kind.typeList.add(buildPostalType(StructuredPostal.TYPE_HOME).setSpecificMax(1)); + kind.typeList.add(buildPostalType(StructuredPostal.TYPE_OTHER).setSpecificMax(1)); + + kind.fieldList = new ArrayList<>(); + if (useJapaneseOrder) { + kind.fieldList.add( + new EditField(StructuredPostal.COUNTRY, R.string.postal_country, FLAGS_POSTAL) + .setOptional(true)); + kind.fieldList.add( + new EditField(StructuredPostal.POSTCODE, R.string.postal_postcode, FLAGS_POSTAL)); + kind.fieldList.add( + new EditField(StructuredPostal.REGION, R.string.postal_region, FLAGS_POSTAL)); + kind.fieldList.add(new EditField(StructuredPostal.CITY, R.string.postal_city, FLAGS_POSTAL)); + kind.fieldList.add( + new EditField(StructuredPostal.STREET, R.string.postal_street, FLAGS_POSTAL)); + } else { + kind.fieldList.add( + new EditField(StructuredPostal.STREET, R.string.postal_street, FLAGS_POSTAL)); + kind.fieldList.add(new EditField(StructuredPostal.CITY, R.string.postal_city, FLAGS_POSTAL)); + kind.fieldList.add( + new EditField(StructuredPostal.REGION, R.string.postal_region, FLAGS_POSTAL)); + kind.fieldList.add( + new EditField(StructuredPostal.POSTCODE, R.string.postal_postcode, FLAGS_POSTAL)); + kind.fieldList.add( + new EditField(StructuredPostal.COUNTRY, R.string.postal_country, FLAGS_POSTAL) + .setOptional(true)); + } + + return kind; + } + + @Override + protected DataKind addDataKindIm(Context context) throws DefinitionException { + final DataKind kind = super.addDataKindIm(context); + + // Types are not supported for IM. There can be 3 IMs, but OWA only shows only the first + kind.typeOverallMax = 3; + + kind.defaultValues = new ContentValues(); + kind.defaultValues.put(Im.TYPE, Im.TYPE_OTHER); + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add(new EditField(Im.DATA, R.string.imLabelsGroup, FLAGS_EMAIL)); + + return kind; + } + + @Override + protected DataKind addDataKindOrganization(Context context) throws DefinitionException { + final DataKind kind = super.addDataKindOrganization(context); + + kind.typeOverallMax = 1; + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add( + new EditField(Organization.COMPANY, R.string.ghostData_company, FLAGS_GENERIC_NAME)); + kind.fieldList.add( + new EditField(Organization.TITLE, R.string.ghostData_title, FLAGS_GENERIC_NAME)); + + return kind; + } + + @Override + protected DataKind addDataKindPhoto(Context context) throws DefinitionException { + final DataKind kind = super.addDataKindPhoto(context); + + kind.typeOverallMax = 1; + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add(new EditField(Photo.PHOTO, -1, -1)); + + return kind; + } + + @Override + protected DataKind addDataKindNote(Context context) throws DefinitionException { + final DataKind kind = super.addDataKindNote(context); + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add(new EditField(Note.NOTE, R.string.label_notes, FLAGS_NOTE)); + + return kind; + } + + protected DataKind addDataKindEvent(Context context) throws DefinitionException { + DataKind kind = + addKind( + new DataKind(Event.CONTENT_ITEM_TYPE, R.string.eventLabelsGroup, Weight.EVENT, true)); + kind.actionHeader = new EventActionInflater(); + kind.actionBody = new SimpleInflater(Event.START_DATE); + + kind.typeOverallMax = 1; + + kind.typeColumn = Event.TYPE; + kind.typeList = new ArrayList<>(); + kind.typeList.add(buildEventType(Event.TYPE_BIRTHDAY, false).setSpecificMax(1)); + + kind.dateFormatWithYear = CommonDateUtils.DATE_AND_TIME_FORMAT; + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add(new EditField(Event.DATA, R.string.eventLabelsGroup, FLAGS_EVENT)); + + return kind; + } + + @Override + protected DataKind addDataKindWebsite(Context context) throws DefinitionException { + final DataKind kind = super.addDataKindWebsite(context); + + kind.typeOverallMax = 1; + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add(new EditField(Website.URL, R.string.websiteLabelsGroup, FLAGS_WEBSITE)); + + return kind; + } + + @Override + public boolean isGroupMembershipEditable() { + return true; + } + + @Override + public boolean areContactsWritable() { + return true; + } +} diff --git a/java/com/android/contacts/common/model/account/ExternalAccountType.java b/java/com/android/contacts/common/model/account/ExternalAccountType.java new file mode 100644 index 000000000..aca1f70d2 --- /dev/null +++ b/java/com/android/contacts/common/model/account/ExternalAccountType.java @@ -0,0 +1,443 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model.account; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.provider.ContactsContract.CommonDataKinds.Photo; +import android.provider.ContactsContract.CommonDataKinds.StructuredName; +import android.support.annotation.VisibleForTesting; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.Log; +import android.util.Xml; +import com.android.contacts.common.R; +import com.android.contacts.common.model.dataitem.DataKind; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +/** A general contacts account type descriptor. */ +public class ExternalAccountType extends BaseAccountType { + + private static final String TAG = "ExternalAccountType"; + + private static final String SYNC_META_DATA = "android.content.SyncAdapter"; + + /** + * The metadata name for so-called "contacts.xml". + * + *

On LMP and later, we also accept the "alternate" name. This is to allow sync adapters to + * have a contacts.xml without making it visible on older platforms. If you modify this also + * update the corresponding list in ContactsProvider/PhotoPriorityResolver + */ + private static final String[] METADATA_CONTACTS_NAMES = + new String[] { + "android.provider.ALTERNATE_CONTACTS_STRUCTURE", "android.provider.CONTACTS_STRUCTURE" + }; + + private static final String TAG_CONTACTS_SOURCE_LEGACY = "ContactsSource"; + private static final String TAG_CONTACTS_ACCOUNT_TYPE = "ContactsAccountType"; + private static final String TAG_CONTACTS_DATA_KIND = "ContactsDataKind"; + private static final String TAG_EDIT_SCHEMA = "EditSchema"; + + private static final String ATTR_EDIT_CONTACT_ACTIVITY = "editContactActivity"; + private static final String ATTR_CREATE_CONTACT_ACTIVITY = "createContactActivity"; + private static final String ATTR_INVITE_CONTACT_ACTIVITY = "inviteContactActivity"; + private static final String ATTR_INVITE_CONTACT_ACTION_LABEL = "inviteContactActionLabel"; + private static final String ATTR_VIEW_CONTACT_NOTIFY_SERVICE = "viewContactNotifyService"; + private static final String ATTR_VIEW_GROUP_ACTIVITY = "viewGroupActivity"; + private static final String ATTR_VIEW_GROUP_ACTION_LABEL = "viewGroupActionLabel"; + private static final String ATTR_DATA_SET = "dataSet"; + private static final String ATTR_EXTENSION_PACKAGE_NAMES = "extensionPackageNames"; + + // The following attributes should only be set in non-sync-adapter account types. They allow + // for the account type and resource IDs to be specified without an associated authenticator. + private static final String ATTR_ACCOUNT_TYPE = "accountType"; + private static final String ATTR_ACCOUNT_LABEL = "accountTypeLabel"; + private static final String ATTR_ACCOUNT_ICON = "accountTypeIcon"; + + private final boolean mIsExtension; + + private String mEditContactActivityClassName; + private String mCreateContactActivityClassName; + private String mInviteContactActivity; + private String mInviteActionLabelAttribute; + private int mInviteActionLabelResId; + private String mViewContactNotifyService; + private String mViewGroupActivity; + private String mViewGroupLabelAttribute; + private int mViewGroupLabelResId; + private List mExtensionPackageNames; + private String mAccountTypeLabelAttribute; + private String mAccountTypeIconAttribute; + private boolean mHasContactsMetadata; + private boolean mHasEditSchema; + + public ExternalAccountType(Context context, String resPackageName, boolean isExtension) { + this(context, resPackageName, isExtension, null); + } + + /** + * Constructor used for testing to initialize with any arbitrary XML. + * + * @param injectedMetadata If non-null, it'll be used to initialize the type. Only set by tests. + * If null, the metadata is loaded from the specified package. + */ + ExternalAccountType( + Context context, + String packageName, + boolean isExtension, + XmlResourceParser injectedMetadata) { + this.mIsExtension = isExtension; + this.resourcePackageName = packageName; + this.syncAdapterPackageName = packageName; + + final XmlResourceParser parser; + if (injectedMetadata == null) { + parser = loadContactsXml(context, packageName); + } else { + parser = injectedMetadata; + } + boolean needLineNumberInErrorLog = true; + try { + if (parser != null) { + inflate(context, parser); + } + + // Done parsing; line number no longer needed in error log. + needLineNumberInErrorLog = false; + if (mHasEditSchema) { + checkKindExists(StructuredName.CONTENT_ITEM_TYPE); + checkKindExists(DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME); + checkKindExists(DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME); + checkKindExists(Photo.CONTENT_ITEM_TYPE); + } else { + // Bring in name and photo from fallback source, which are non-optional + addDataKindStructuredName(context); + addDataKindDisplayName(context); + addDataKindPhoneticName(context); + addDataKindPhoto(context); + } + } catch (DefinitionException e) { + final StringBuilder error = new StringBuilder(); + error.append("Problem reading XML"); + if (needLineNumberInErrorLog && (parser != null)) { + error.append(" in line "); + error.append(parser.getLineNumber()); + } + error.append(" for external package "); + error.append(packageName); + + Log.e(TAG, error.toString(), e); + return; + } finally { + if (parser != null) { + parser.close(); + } + } + + mExtensionPackageNames = new ArrayList(); + mInviteActionLabelResId = + resolveExternalResId( + context, + mInviteActionLabelAttribute, + syncAdapterPackageName, + ATTR_INVITE_CONTACT_ACTION_LABEL); + mViewGroupLabelResId = + resolveExternalResId( + context, + mViewGroupLabelAttribute, + syncAdapterPackageName, + ATTR_VIEW_GROUP_ACTION_LABEL); + titleRes = + resolveExternalResId( + context, mAccountTypeLabelAttribute, syncAdapterPackageName, ATTR_ACCOUNT_LABEL); + iconRes = + resolveExternalResId( + context, mAccountTypeIconAttribute, syncAdapterPackageName, ATTR_ACCOUNT_ICON); + + // If we reach this point, the account type has been successfully initialized. + mIsInitialized = true; + } + + /** + * Returns the CONTACTS_STRUCTURE metadata (aka "contacts.xml") in the given apk package. + * + *

This method looks through all services in the package that handle sync adapter intents for + * the first one that contains CONTACTS_STRUCTURE metadata. We have to look through all sync + * adapters in the package in case there are contacts and other sync adapters (eg, calendar) in + * the same package. + * + *

Returns {@code null} if the package has no CONTACTS_STRUCTURE metadata. In this case the + * account type *will* be initialized with minimal configuration. + */ + public static XmlResourceParser loadContactsXml(Context context, String resPackageName) { + final PackageManager pm = context.getPackageManager(); + final Intent intent = new Intent(SYNC_META_DATA).setPackage(resPackageName); + final List intentServices = + pm.queryIntentServices(intent, PackageManager.GET_SERVICES | PackageManager.GET_META_DATA); + + if (intentServices != null) { + for (final ResolveInfo resolveInfo : intentServices) { + final ServiceInfo serviceInfo = resolveInfo.serviceInfo; + if (serviceInfo == null) { + continue; + } + for (String metadataName : METADATA_CONTACTS_NAMES) { + final XmlResourceParser parser = serviceInfo.loadXmlMetaData(pm, metadataName); + if (parser != null) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d( + TAG, + String.format( + "Metadata loaded from: %s, %s, %s", + serviceInfo.packageName, serviceInfo.name, metadataName)); + } + return parser; + } + } + } + } + + // Package was found, but that doesn't contain the CONTACTS_STRUCTURE metadata. + return null; + } + + /** Returns {@code TRUE} if the package contains CONTACTS_STRUCTURE metadata. */ + public static boolean hasContactsXml(Context context, String resPackageName) { + return loadContactsXml(context, resPackageName) != null; + } + + /** + * Takes a string in the "@xxx/yyy" format and return the resource ID for the resource in the + * resource package. + * + *

If the argument is in the invalid format or isn't a resource name, it returns -1. + * + * @param context context + * @param resourceName Resource name in the "@xxx/yyy" format, e.g. "@string/invite_lavbel" + * @param packageName name of the package containing the resource. + * @param xmlAttributeName attribute name which the resource came from. Used for logging. + */ + @VisibleForTesting + static int resolveExternalResId( + Context context, String resourceName, String packageName, String xmlAttributeName) { + if (TextUtils.isEmpty(resourceName)) { + return -1; // Empty text is okay. + } + if (resourceName.charAt(0) != '@') { + Log.e(TAG, xmlAttributeName + " must be a resource name beginnig with '@'"); + return -1; + } + final String name = resourceName.substring(1); + final Resources res; + try { + res = context.getPackageManager().getResourcesForApplication(packageName); + } catch (NameNotFoundException e) { + Log.e(TAG, "Unable to load package " + packageName); + return -1; + } + final int resId = res.getIdentifier(name, null, packageName); + if (resId == 0) { + Log.e(TAG, "Unable to load " + resourceName + " from package " + packageName); + return -1; + } + return resId; + } + + private void checkKindExists(String mimeType) throws DefinitionException { + if (getKindForMimetype(mimeType) == null) { + throw new DefinitionException(mimeType + " must be supported"); + } + } + + @Override + public boolean isEmbedded() { + return false; + } + + @Override + public boolean isExtension() { + return mIsExtension; + } + + @Override + public boolean areContactsWritable() { + return mHasEditSchema; + } + + /** Whether this account type has the android.provider.CONTACTS_STRUCTURE metadata xml. */ + public boolean hasContactsMetadata() { + return mHasContactsMetadata; + } + + @Override + public String getEditContactActivityClassName() { + return mEditContactActivityClassName; + } + + @Override + public String getCreateContactActivityClassName() { + return mCreateContactActivityClassName; + } + + @Override + public String getInviteContactActivityClassName() { + return mInviteContactActivity; + } + + @Override + protected int getInviteContactActionResId() { + return mInviteActionLabelResId; + } + + @Override + public String getViewContactNotifyServiceClassName() { + return mViewContactNotifyService; + } + + @Override + public String getViewGroupActivity() { + return mViewGroupActivity; + } + + @Override + protected int getViewGroupLabelResId() { + return mViewGroupLabelResId; + } + + @Override + public List getExtensionPackageNames() { + return mExtensionPackageNames; + } + + /** + * Inflate this {@link AccountType} from the given parser. This may only load details matching the + * publicly-defined schema. + */ + protected void inflate(Context context, XmlPullParser parser) throws DefinitionException { + final AttributeSet attrs = Xml.asAttributeSet(parser); + + try { + int type; + while ((type = parser.next()) != XmlPullParser.START_TAG + && type != XmlPullParser.END_DOCUMENT) { + // Drain comments and whitespace + } + + if (type != XmlPullParser.START_TAG) { + throw new IllegalStateException("No start tag found"); + } + + String rootTag = parser.getName(); + if (!TAG_CONTACTS_ACCOUNT_TYPE.equals(rootTag) + && !TAG_CONTACTS_SOURCE_LEGACY.equals(rootTag)) { + throw new IllegalStateException( + "Top level element must be " + TAG_CONTACTS_ACCOUNT_TYPE + ", not " + rootTag); + } + + mHasContactsMetadata = true; + + int attributeCount = parser.getAttributeCount(); + for (int i = 0; i < attributeCount; i++) { + String attr = parser.getAttributeName(i); + String value = parser.getAttributeValue(i); + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, attr + "=" + value); + } + if (ATTR_EDIT_CONTACT_ACTIVITY.equals(attr)) { + mEditContactActivityClassName = value; + } else if (ATTR_CREATE_CONTACT_ACTIVITY.equals(attr)) { + mCreateContactActivityClassName = value; + } else if (ATTR_INVITE_CONTACT_ACTIVITY.equals(attr)) { + mInviteContactActivity = value; + } else if (ATTR_INVITE_CONTACT_ACTION_LABEL.equals(attr)) { + mInviteActionLabelAttribute = value; + } else if (ATTR_VIEW_CONTACT_NOTIFY_SERVICE.equals(attr)) { + mViewContactNotifyService = value; + } else if (ATTR_VIEW_GROUP_ACTIVITY.equals(attr)) { + mViewGroupActivity = value; + } else if (ATTR_VIEW_GROUP_ACTION_LABEL.equals(attr)) { + mViewGroupLabelAttribute = value; + } else if (ATTR_DATA_SET.equals(attr)) { + dataSet = value; + } else if (ATTR_EXTENSION_PACKAGE_NAMES.equals(attr)) { + mExtensionPackageNames.add(value); + } else if (ATTR_ACCOUNT_TYPE.equals(attr)) { + accountType = value; + } else if (ATTR_ACCOUNT_LABEL.equals(attr)) { + mAccountTypeLabelAttribute = value; + } else if (ATTR_ACCOUNT_ICON.equals(attr)) { + mAccountTypeIconAttribute = value; + } else { + Log.e(TAG, "Unsupported attribute " + attr); + } + } + + // Parse all children kinds + final int startDepth = parser.getDepth(); + while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > startDepth) + && type != XmlPullParser.END_DOCUMENT) { + + if (type != XmlPullParser.START_TAG || parser.getDepth() != startDepth + 1) { + continue; // Not a direct child tag + } + + String tag = parser.getName(); + if (TAG_EDIT_SCHEMA.equals(tag)) { + mHasEditSchema = true; + parseEditSchema(context, parser, attrs); + } else if (TAG_CONTACTS_DATA_KIND.equals(tag)) { + final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ContactsDataKind); + final DataKind kind = new DataKind(); + + kind.mimeType = a.getString(R.styleable.ContactsDataKind_android_mimeType); + final String summaryColumn = + a.getString(R.styleable.ContactsDataKind_android_summaryColumn); + if (summaryColumn != null) { + // Inflate a specific column as summary when requested + kind.actionHeader = new SimpleInflater(summaryColumn); + } + final String detailColumn = + a.getString(R.styleable.ContactsDataKind_android_detailColumn); + if (detailColumn != null) { + // Inflate specific column as summary + kind.actionBody = new SimpleInflater(detailColumn); + } + + a.recycle(); + + addKind(kind); + } + } + } catch (XmlPullParserException e) { + throw new DefinitionException("Problem reading XML", e); + } catch (IOException e) { + throw new DefinitionException("Problem reading XML", e); + } + } +} diff --git a/java/com/android/contacts/common/model/account/FallbackAccountType.java b/java/com/android/contacts/common/model/account/FallbackAccountType.java new file mode 100644 index 000000000..976a7b892 --- /dev/null +++ b/java/com/android/contacts/common/model/account/FallbackAccountType.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model.account; + +import android.content.Context; +import android.util.Log; +import com.android.contacts.common.R; +import com.android.contacts.common.model.dataitem.DataKind; + +public class FallbackAccountType extends BaseAccountType { + + private static final String TAG = "FallbackAccountType"; + + private FallbackAccountType(Context context, String resPackageName) { + this.accountType = null; + this.dataSet = null; + this.titleRes = R.string.account_phone; + this.iconRes = R.mipmap.ic_contacts_launcher; + + // Note those are only set for unit tests. + this.resourcePackageName = resPackageName; + this.syncAdapterPackageName = resPackageName; + + try { + addDataKindStructuredName(context); + addDataKindDisplayName(context); + addDataKindPhoneticName(context); + addDataKindNickname(context); + addDataKindPhone(context); + addDataKindEmail(context); + addDataKindStructuredPostal(context); + addDataKindIm(context); + addDataKindOrganization(context); + addDataKindPhoto(context); + addDataKindNote(context); + addDataKindWebsite(context); + addDataKindSipAddress(context); + addDataKindGroupMembership(context); + + mIsInitialized = true; + } catch (DefinitionException e) { + Log.e(TAG, "Problem building account type", e); + } + } + + public FallbackAccountType(Context context) { + this(context, null); + } + + /** + * Used to compare with an {@link ExternalAccountType} built from a test contacts.xml. In order to + * build {@link DataKind}s with the same resource package name, {@code resPackageName} is + * injectable. + */ + static AccountType createWithPackageNameForTest(Context context, String resPackageName) { + return new FallbackAccountType(context, resPackageName); + } + + @Override + public boolean areContactsWritable() { + return true; + } +} diff --git a/java/com/android/contacts/common/model/account/GoogleAccountType.java b/java/com/android/contacts/common/model/account/GoogleAccountType.java new file mode 100644 index 000000000..2f1fe0ed6 --- /dev/null +++ b/java/com/android/contacts/common/model/account/GoogleAccountType.java @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model.account; + +import android.content.ContentValues; +import android.content.Context; +import android.provider.ContactsContract.CommonDataKinds.Email; +import android.provider.ContactsContract.CommonDataKinds.Event; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.CommonDataKinds.Relation; +import android.util.Log; +import com.android.contacts.common.R; +import com.android.contacts.common.model.dataitem.DataKind; +import com.android.contacts.common.util.CommonDateUtils; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class GoogleAccountType extends BaseAccountType { + + /** + * The package name that we should load contacts.xml from and rely on to handle G+ account + * actions. Even though this points to gms, in some cases gms will still hand off responsibility + * to the G+ app. + */ + public static final String PLUS_EXTENSION_PACKAGE_NAME = "com.google.android.gms"; + + public static final String ACCOUNT_TYPE = "com.google"; + private static final String TAG = "GoogleAccountType"; + private static final List mExtensionPackages = + new ArrayList<>(Collections.singletonList(PLUS_EXTENSION_PACKAGE_NAME)); + + public GoogleAccountType(Context context, String authenticatorPackageName) { + this.accountType = ACCOUNT_TYPE; + this.resourcePackageName = null; + this.syncAdapterPackageName = authenticatorPackageName; + + try { + addDataKindStructuredName(context); + addDataKindDisplayName(context); + addDataKindPhoneticName(context); + addDataKindNickname(context); + addDataKindPhone(context); + addDataKindEmail(context); + addDataKindStructuredPostal(context); + addDataKindIm(context); + addDataKindOrganization(context); + addDataKindPhoto(context); + addDataKindNote(context); + addDataKindWebsite(context); + addDataKindSipAddress(context); + addDataKindGroupMembership(context); + addDataKindRelation(context); + addDataKindEvent(context); + + mIsInitialized = true; + } catch (DefinitionException e) { + Log.e(TAG, "Problem building account type", e); + } + } + + @Override + public List getExtensionPackageNames() { + return mExtensionPackages; + } + + @Override + protected DataKind addDataKindPhone(Context context) throws DefinitionException { + final DataKind kind = super.addDataKindPhone(context); + + kind.typeColumn = Phone.TYPE; + kind.typeList = new ArrayList<>(); + kind.typeList.add(buildPhoneType(Phone.TYPE_MOBILE)); + kind.typeList.add(buildPhoneType(Phone.TYPE_WORK)); + kind.typeList.add(buildPhoneType(Phone.TYPE_HOME)); + kind.typeList.add(buildPhoneType(Phone.TYPE_MAIN)); + kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_WORK).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_HOME).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_PAGER).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_OTHER)); + kind.typeList.add( + buildPhoneType(Phone.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Phone.LABEL)); + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE)); + + return kind; + } + + @Override + protected DataKind addDataKindEmail(Context context) throws DefinitionException { + final DataKind kind = super.addDataKindEmail(context); + + kind.typeColumn = Email.TYPE; + kind.typeList = new ArrayList<>(); + kind.typeList.add(buildEmailType(Email.TYPE_HOME)); + kind.typeList.add(buildEmailType(Email.TYPE_WORK)); + kind.typeList.add(buildEmailType(Email.TYPE_OTHER)); + kind.typeList.add( + buildEmailType(Email.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Email.LABEL)); + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL)); + + return kind; + } + + private DataKind addDataKindRelation(Context context) throws DefinitionException { + DataKind kind = + addKind( + new DataKind( + Relation.CONTENT_ITEM_TYPE, + R.string.relationLabelsGroup, + Weight.RELATIONSHIP, + true)); + kind.actionHeader = new RelationActionInflater(); + kind.actionBody = new SimpleInflater(Relation.NAME); + + kind.typeColumn = Relation.TYPE; + kind.typeList = new ArrayList<>(); + kind.typeList.add(buildRelationType(Relation.TYPE_ASSISTANT)); + kind.typeList.add(buildRelationType(Relation.TYPE_BROTHER)); + kind.typeList.add(buildRelationType(Relation.TYPE_CHILD)); + kind.typeList.add(buildRelationType(Relation.TYPE_DOMESTIC_PARTNER)); + kind.typeList.add(buildRelationType(Relation.TYPE_FATHER)); + kind.typeList.add(buildRelationType(Relation.TYPE_FRIEND)); + kind.typeList.add(buildRelationType(Relation.TYPE_MANAGER)); + kind.typeList.add(buildRelationType(Relation.TYPE_MOTHER)); + kind.typeList.add(buildRelationType(Relation.TYPE_PARENT)); + kind.typeList.add(buildRelationType(Relation.TYPE_PARTNER)); + kind.typeList.add(buildRelationType(Relation.TYPE_REFERRED_BY)); + kind.typeList.add(buildRelationType(Relation.TYPE_RELATIVE)); + kind.typeList.add(buildRelationType(Relation.TYPE_SISTER)); + kind.typeList.add(buildRelationType(Relation.TYPE_SPOUSE)); + kind.typeList.add( + buildRelationType(Relation.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Relation.LABEL)); + + kind.defaultValues = new ContentValues(); + kind.defaultValues.put(Relation.TYPE, Relation.TYPE_SPOUSE); + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add(new EditField(Relation.DATA, R.string.relationLabelsGroup, FLAGS_RELATION)); + + return kind; + } + + private DataKind addDataKindEvent(Context context) throws DefinitionException { + DataKind kind = + addKind( + new DataKind(Event.CONTENT_ITEM_TYPE, R.string.eventLabelsGroup, Weight.EVENT, true)); + kind.actionHeader = new EventActionInflater(); + kind.actionBody = new SimpleInflater(Event.START_DATE); + + kind.typeColumn = Event.TYPE; + kind.typeList = new ArrayList<>(); + kind.dateFormatWithoutYear = CommonDateUtils.NO_YEAR_DATE_FORMAT; + kind.dateFormatWithYear = CommonDateUtils.FULL_DATE_FORMAT; + kind.typeList.add(buildEventType(Event.TYPE_BIRTHDAY, true).setSpecificMax(1)); + kind.typeList.add(buildEventType(Event.TYPE_ANNIVERSARY, false)); + kind.typeList.add(buildEventType(Event.TYPE_OTHER, false)); + kind.typeList.add( + buildEventType(Event.TYPE_CUSTOM, false).setSecondary(true).setCustomColumn(Event.LABEL)); + + kind.defaultValues = new ContentValues(); + kind.defaultValues.put(Event.TYPE, Event.TYPE_BIRTHDAY); + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add(new EditField(Event.DATA, R.string.eventLabelsGroup, FLAGS_EVENT)); + + return kind; + } + + @Override + public boolean isGroupMembershipEditable() { + return true; + } + + @Override + public boolean areContactsWritable() { + return true; + } + + @Override + public String getViewContactNotifyServiceClassName() { + return "com.google.android.syncadapters.contacts." + "SyncHighResPhotoIntentService"; + } + + @Override + public String getViewContactNotifyServicePackageName() { + return "com.google.android.syncadapters.contacts"; + } +} diff --git a/java/com/android/contacts/common/model/account/SamsungAccountType.java b/java/com/android/contacts/common/model/account/SamsungAccountType.java new file mode 100644 index 000000000..45406bc2b --- /dev/null +++ b/java/com/android/contacts/common/model/account/SamsungAccountType.java @@ -0,0 +1,235 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model.account; + +import android.content.ContentValues; +import android.content.Context; +import android.provider.ContactsContract.CommonDataKinds.Email; +import android.provider.ContactsContract.CommonDataKinds.Event; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.CommonDataKinds.Relation; +import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; +import android.util.Log; +import com.android.contacts.common.R; +import com.android.contacts.common.model.dataitem.DataKind; +import com.android.contacts.common.util.CommonDateUtils; +import java.util.ArrayList; +import java.util.Locale; + +/** + * A writable account type that can be used to support samsung contacts. This may not perfectly + * match Samsung's latest intended account schema. + * + *

This is only used to partially support Samsung accounts. The DataKind labels & fields are + * setup to support the values used by Samsung. But, not everything in the Samsung account type is + * supported. The Samsung account type includes a "Message Type" mimetype that we have no intention + * of showing inside the Contact editor. Similarly, we don't handle the "Ringtone" mimetype here + * since managing ringtones is handled in a different flow. + */ +public class SamsungAccountType extends BaseAccountType { + + private static final String TAG = "KnownExternalAccountType"; + private static final String ACCOUNT_TYPE_SAMSUNG = "com.osp.app.signin"; + + public SamsungAccountType(Context context, String authenticatorPackageName, String type) { + this.accountType = type; + this.resourcePackageName = null; + this.syncAdapterPackageName = authenticatorPackageName; + + try { + addDataKindStructuredName(context); + addDataKindDisplayName(context); + addDataKindPhoneticName(context); + addDataKindNickname(context); + addDataKindPhone(context); + addDataKindEmail(context); + addDataKindStructuredPostal(context); + addDataKindIm(context); + addDataKindOrganization(context); + addDataKindPhoto(context); + addDataKindNote(context); + addDataKindWebsite(context); + addDataKindGroupMembership(context); + addDataKindRelation(context); + addDataKindEvent(context); + + mIsInitialized = true; + } catch (DefinitionException e) { + Log.e(TAG, "Problem building account type", e); + } + } + + /** + * Returns {@code TRUE} if this is samsung's account type and Samsung hasn't bothered to define a + * contacts.xml to provide a more accurate definition than ours. + */ + public static boolean isSamsungAccountType(Context context, String type, String packageName) { + return ACCOUNT_TYPE_SAMSUNG.equals(type) + && !ExternalAccountType.hasContactsXml(context, packageName); + } + + @Override + protected DataKind addDataKindStructuredPostal(Context context) throws DefinitionException { + final DataKind kind = super.addDataKindStructuredPostal(context); + + final boolean useJapaneseOrder = + Locale.JAPANESE.getLanguage().equals(Locale.getDefault().getLanguage()); + kind.typeColumn = StructuredPostal.TYPE; + kind.typeList = new ArrayList<>(); + kind.typeList.add(buildPostalType(StructuredPostal.TYPE_WORK).setSpecificMax(1)); + kind.typeList.add(buildPostalType(StructuredPostal.TYPE_HOME).setSpecificMax(1)); + kind.typeList.add(buildPostalType(StructuredPostal.TYPE_OTHER).setSpecificMax(1)); + + kind.fieldList = new ArrayList<>(); + if (useJapaneseOrder) { + kind.fieldList.add( + new EditField(StructuredPostal.COUNTRY, R.string.postal_country, FLAGS_POSTAL) + .setOptional(true)); + kind.fieldList.add( + new EditField(StructuredPostal.POSTCODE, R.string.postal_postcode, FLAGS_POSTAL)); + kind.fieldList.add( + new EditField(StructuredPostal.REGION, R.string.postal_region, FLAGS_POSTAL)); + kind.fieldList.add(new EditField(StructuredPostal.CITY, R.string.postal_city, FLAGS_POSTAL)); + kind.fieldList.add( + new EditField(StructuredPostal.STREET, R.string.postal_street, FLAGS_POSTAL)); + } else { + kind.fieldList.add( + new EditField(StructuredPostal.STREET, R.string.postal_street, FLAGS_POSTAL)); + kind.fieldList.add(new EditField(StructuredPostal.CITY, R.string.postal_city, FLAGS_POSTAL)); + kind.fieldList.add( + new EditField(StructuredPostal.REGION, R.string.postal_region, FLAGS_POSTAL)); + kind.fieldList.add( + new EditField(StructuredPostal.POSTCODE, R.string.postal_postcode, FLAGS_POSTAL)); + kind.fieldList.add( + new EditField(StructuredPostal.COUNTRY, R.string.postal_country, FLAGS_POSTAL) + .setOptional(true)); + } + + return kind; + } + + @Override + protected DataKind addDataKindPhone(Context context) throws DefinitionException { + final DataKind kind = super.addDataKindPhone(context); + + kind.typeColumn = Phone.TYPE; + kind.typeList = new ArrayList<>(); + kind.typeList.add(buildPhoneType(Phone.TYPE_MOBILE)); + kind.typeList.add(buildPhoneType(Phone.TYPE_HOME)); + kind.typeList.add(buildPhoneType(Phone.TYPE_WORK)); + kind.typeList.add(buildPhoneType(Phone.TYPE_MAIN)); + kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_WORK).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_HOME).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_PAGER).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_RADIO).setSecondary(true)); + kind.typeList.add(buildPhoneType(Phone.TYPE_OTHER)); + kind.typeList.add( + buildPhoneType(Phone.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Phone.LABEL)); + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE)); + + return kind; + } + + @Override + protected DataKind addDataKindEmail(Context context) throws DefinitionException { + final DataKind kind = super.addDataKindEmail(context); + + kind.typeColumn = Email.TYPE; + kind.typeList = new ArrayList<>(); + kind.typeList.add(buildEmailType(Email.TYPE_HOME)); + kind.typeList.add(buildEmailType(Email.TYPE_WORK)); + kind.typeList.add(buildEmailType(Email.TYPE_OTHER)); + kind.typeList.add( + buildEmailType(Email.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Email.LABEL)); + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL)); + + return kind; + } + + private DataKind addDataKindRelation(Context context) throws DefinitionException { + DataKind kind = + addKind(new DataKind(Relation.CONTENT_ITEM_TYPE, R.string.relationLabelsGroup, 160, true)); + kind.actionHeader = new RelationActionInflater(); + kind.actionBody = new SimpleInflater(Relation.NAME); + + kind.typeColumn = Relation.TYPE; + kind.typeList = new ArrayList<>(); + kind.typeList.add(buildRelationType(Relation.TYPE_ASSISTANT)); + kind.typeList.add(buildRelationType(Relation.TYPE_BROTHER)); + kind.typeList.add(buildRelationType(Relation.TYPE_CHILD)); + kind.typeList.add(buildRelationType(Relation.TYPE_DOMESTIC_PARTNER)); + kind.typeList.add(buildRelationType(Relation.TYPE_FATHER)); + kind.typeList.add(buildRelationType(Relation.TYPE_FRIEND)); + kind.typeList.add(buildRelationType(Relation.TYPE_MANAGER)); + kind.typeList.add(buildRelationType(Relation.TYPE_MOTHER)); + kind.typeList.add(buildRelationType(Relation.TYPE_PARENT)); + kind.typeList.add(buildRelationType(Relation.TYPE_PARTNER)); + kind.typeList.add(buildRelationType(Relation.TYPE_REFERRED_BY)); + kind.typeList.add(buildRelationType(Relation.TYPE_RELATIVE)); + kind.typeList.add(buildRelationType(Relation.TYPE_SISTER)); + kind.typeList.add(buildRelationType(Relation.TYPE_SPOUSE)); + kind.typeList.add( + buildRelationType(Relation.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Relation.LABEL)); + + kind.defaultValues = new ContentValues(); + kind.defaultValues.put(Relation.TYPE, Relation.TYPE_SPOUSE); + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add(new EditField(Relation.DATA, R.string.relationLabelsGroup, FLAGS_RELATION)); + + return kind; + } + + private DataKind addDataKindEvent(Context context) throws DefinitionException { + DataKind kind = + addKind(new DataKind(Event.CONTENT_ITEM_TYPE, R.string.eventLabelsGroup, 150, true)); + kind.actionHeader = new EventActionInflater(); + kind.actionBody = new SimpleInflater(Event.START_DATE); + + kind.typeColumn = Event.TYPE; + kind.typeList = new ArrayList<>(); + kind.dateFormatWithoutYear = CommonDateUtils.NO_YEAR_DATE_FORMAT; + kind.dateFormatWithYear = CommonDateUtils.FULL_DATE_FORMAT; + kind.typeList.add(buildEventType(Event.TYPE_BIRTHDAY, true).setSpecificMax(1)); + kind.typeList.add(buildEventType(Event.TYPE_ANNIVERSARY, false)); + kind.typeList.add(buildEventType(Event.TYPE_OTHER, false)); + kind.typeList.add( + buildEventType(Event.TYPE_CUSTOM, false).setSecondary(true).setCustomColumn(Event.LABEL)); + + kind.defaultValues = new ContentValues(); + kind.defaultValues.put(Event.TYPE, Event.TYPE_BIRTHDAY); + + kind.fieldList = new ArrayList<>(); + kind.fieldList.add(new EditField(Event.DATA, R.string.eventLabelsGroup, FLAGS_EVENT)); + + return kind; + } + + @Override + public boolean isGroupMembershipEditable() { + return true; + } + + @Override + public boolean areContactsWritable() { + return true; + } +} diff --git a/java/com/android/contacts/common/model/dataitem/DataItem.java b/java/com/android/contacts/common/model/dataitem/DataItem.java new file mode 100644 index 000000000..dc746055b --- /dev/null +++ b/java/com/android/contacts/common/model/dataitem/DataItem.java @@ -0,0 +1,258 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model.dataitem; + +import android.content.ContentValues; +import android.content.Context; +import android.provider.ContactsContract.CommonDataKinds.Email; +import android.provider.ContactsContract.CommonDataKinds.Event; +import android.provider.ContactsContract.CommonDataKinds.GroupMembership; +import android.provider.ContactsContract.CommonDataKinds.Identity; +import android.provider.ContactsContract.CommonDataKinds.Im; +import android.provider.ContactsContract.CommonDataKinds.Nickname; +import android.provider.ContactsContract.CommonDataKinds.Note; +import android.provider.ContactsContract.CommonDataKinds.Organization; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.CommonDataKinds.Photo; +import android.provider.ContactsContract.CommonDataKinds.Relation; +import android.provider.ContactsContract.CommonDataKinds.SipAddress; +import android.provider.ContactsContract.CommonDataKinds.StructuredName; +import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; +import android.provider.ContactsContract.CommonDataKinds.Website; +import android.provider.ContactsContract.Contacts.Data; +import android.provider.ContactsContract.Contacts.Entity; +import com.android.contacts.common.Collapser; +import com.android.contacts.common.MoreContactUtils; +import com.android.contacts.common.model.account.AccountType.EditType; + +/** This is the base class for data items, which represents a row from the Data table. */ +public class DataItem implements Collapser.Collapsible { + + private final ContentValues mContentValues; + protected DataKind mKind; + + protected DataItem(ContentValues values) { + mContentValues = values; + } + + /** + * Factory for creating subclasses of DataItem objects based on the mimetype in the content + * values. Raw contact is the raw contact that this data item is associated with. + */ + public static DataItem createFrom(ContentValues values) { + final String mimeType = values.getAsString(Data.MIMETYPE); + if (GroupMembership.CONTENT_ITEM_TYPE.equals(mimeType)) { + return new GroupMembershipDataItem(values); + } else if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) { + return new StructuredNameDataItem(values); + } else if (Phone.CONTENT_ITEM_TYPE.equals(mimeType)) { + return new PhoneDataItem(values); + } else if (Email.CONTENT_ITEM_TYPE.equals(mimeType)) { + return new EmailDataItem(values); + } else if (StructuredPostal.CONTENT_ITEM_TYPE.equals(mimeType)) { + return new StructuredPostalDataItem(values); + } else if (Im.CONTENT_ITEM_TYPE.equals(mimeType)) { + return new ImDataItem(values); + } else if (Organization.CONTENT_ITEM_TYPE.equals(mimeType)) { + return new OrganizationDataItem(values); + } else if (Nickname.CONTENT_ITEM_TYPE.equals(mimeType)) { + return new NicknameDataItem(values); + } else if (Note.CONTENT_ITEM_TYPE.equals(mimeType)) { + return new NoteDataItem(values); + } else if (Website.CONTENT_ITEM_TYPE.equals(mimeType)) { + return new WebsiteDataItem(values); + } else if (SipAddress.CONTENT_ITEM_TYPE.equals(mimeType)) { + return new SipAddressDataItem(values); + } else if (Event.CONTENT_ITEM_TYPE.equals(mimeType)) { + return new EventDataItem(values); + } else if (Relation.CONTENT_ITEM_TYPE.equals(mimeType)) { + return new RelationDataItem(values); + } else if (Identity.CONTENT_ITEM_TYPE.equals(mimeType)) { + return new IdentityDataItem(values); + } else if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) { + return new PhotoDataItem(values); + } + + // generic + return new DataItem(values); + } + + public ContentValues getContentValues() { + return mContentValues; + } + + public Long getRawContactId() { + return mContentValues.getAsLong(Data.RAW_CONTACT_ID); + } + + public void setRawContactId(long rawContactId) { + mContentValues.put(Data.RAW_CONTACT_ID, rawContactId); + } + + /** Returns the data id. */ + public long getId() { + return mContentValues.getAsLong(Data._ID); + } + + /** Returns the mimetype of the data. */ + public String getMimeType() { + return mContentValues.getAsString(Data.MIMETYPE); + } + + public void setMimeType(String mimeType) { + mContentValues.put(Data.MIMETYPE, mimeType); + } + + public boolean isPrimary() { + Integer primary = mContentValues.getAsInteger(Data.IS_PRIMARY); + return primary != null && primary != 0; + } + + public boolean isSuperPrimary() { + Integer superPrimary = mContentValues.getAsInteger(Data.IS_SUPER_PRIMARY); + return superPrimary != null && superPrimary != 0; + } + + public boolean hasKindTypeColumn(DataKind kind) { + final String key = kind.typeColumn; + return key != null + && mContentValues.containsKey(key) + && mContentValues.getAsInteger(key) != null; + } + + public int getKindTypeColumn(DataKind kind) { + final String key = kind.typeColumn; + return mContentValues.getAsInteger(key); + } + + /** + * Indicates the carrier presence value for the current {@link DataItem}. + * + * @return {@link Data#CARRIER_PRESENCE_VT_CAPABLE} if the {@link DataItem} supports carrier video + * calling, {@code 0} otherwise. + */ + public int getCarrierPresence() { + return mContentValues.getAsInteger(Data.CARRIER_PRESENCE); + } + + /** + * This builds the data string depending on the type of data item by using the generic DataKind + * object underneath. + */ + public String buildDataString(Context context, DataKind kind) { + if (kind.actionBody == null) { + return null; + } + CharSequence actionBody = kind.actionBody.inflateUsing(context, mContentValues); + return actionBody == null ? null : actionBody.toString(); + } + + /** + * This builds the data string(intended for display) depending on the type of data item. It + * returns the same value as {@link #buildDataString} by default, but certain data items can + * override it to provide their version of formatted data strings. + * + * @return Data string representing the data item, possibly formatted for display + */ + public String buildDataStringForDisplay(Context context, DataKind kind) { + return buildDataString(context, kind); + } + + public DataKind getDataKind() { + return mKind; + } + + public void setDataKind(DataKind kind) { + mKind = kind; + } + + public Integer getTimesUsed() { + return mContentValues.getAsInteger(Entity.TIMES_USED); + } + + public Long getLastTimeUsed() { + return mContentValues.getAsLong(Entity.LAST_TIME_USED); + } + + @Override + public void collapseWith(DataItem that) { + DataKind thisKind = getDataKind(); + DataKind thatKind = that.getDataKind(); + // If this does not have a type and that does, or if that's type is higher precedence, + // use that's type + if ((!hasKindTypeColumn(thisKind) && that.hasKindTypeColumn(thatKind)) + || (that.hasKindTypeColumn(thatKind) + && getTypePrecedence(thisKind, getKindTypeColumn(thisKind)) + > getTypePrecedence(thatKind, that.getKindTypeColumn(thatKind)))) { + mContentValues.put(thatKind.typeColumn, that.getKindTypeColumn(thatKind)); + mKind = thatKind; + } + + // Choose the max of the maxLines and maxLabelLines values. + mKind.maxLinesForDisplay = Math.max(thisKind.maxLinesForDisplay, thatKind.maxLinesForDisplay); + + // If any of the collapsed entries are super primary make the whole thing super primary. + if (isSuperPrimary() || that.isSuperPrimary()) { + mContentValues.put(Data.IS_SUPER_PRIMARY, 1); + mContentValues.put(Data.IS_PRIMARY, 1); + } + + // If any of the collapsed entries are primary make the whole thing primary. + if (isPrimary() || that.isPrimary()) { + mContentValues.put(Data.IS_PRIMARY, 1); + } + + // Add up the times used + mContentValues.put( + Entity.TIMES_USED, + (getTimesUsed() == null ? 0 : getTimesUsed()) + + (that.getTimesUsed() == null ? 0 : that.getTimesUsed())); + + // Use the most recent time + mContentValues.put( + Entity.LAST_TIME_USED, + Math.max( + getLastTimeUsed() == null ? 0 : getLastTimeUsed(), + that.getLastTimeUsed() == null ? 0 : that.getLastTimeUsed())); + } + + @Override + public boolean shouldCollapseWith(DataItem t, Context context) { + if (mKind == null || t.getDataKind() == null) { + return false; + } + return MoreContactUtils.shouldCollapse( + getMimeType(), + buildDataString(context, mKind), + t.getMimeType(), + t.buildDataString(context, t.getDataKind())); + } + + /** + * Return the precedence for the the given {@link EditType#rawValue}, where lower numbers are + * higher precedence. + */ + private static int getTypePrecedence(DataKind kind, int rawValue) { + for (int i = 0; i < kind.typeList.size(); i++) { + final EditType type = kind.typeList.get(i); + if (type.rawValue == rawValue) { + return i; + } + } + return Integer.MAX_VALUE; + } +} diff --git a/java/com/android/contacts/common/model/dataitem/DataKind.java b/java/com/android/contacts/common/model/dataitem/DataKind.java new file mode 100644 index 000000000..3b470a2ae --- /dev/null +++ b/java/com/android/contacts/common/model/dataitem/DataKind.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model.dataitem; + +import android.content.ContentValues; +import android.content.Context; +import android.provider.ContactsContract.Data; +import com.android.contacts.common.model.account.AccountType.EditField; +import com.android.contacts.common.model.account.AccountType.EditType; +import com.android.contacts.common.model.account.AccountType.StringInflater; +import com.google.common.collect.Iterators; +import java.text.SimpleDateFormat; +import java.util.List; + +/** + * Description of a specific data type, usually marked by a unique {@link Data#MIMETYPE}. Includes + * details about how to view and edit {@link Data} rows of this kind, including the possible {@link + * EditType} labels and editable {@link EditField}. + */ +public final class DataKind { + + public static final String PSEUDO_MIME_TYPE_DISPLAY_NAME = "#displayName"; + public static final String PSEUDO_MIME_TYPE_PHONETIC_NAME = "#phoneticName"; + public static final String PSEUDO_COLUMN_PHONETIC_NAME = "#phoneticName"; + + public String resourcePackageName; + public String mimeType; + public int titleRes; + public int iconAltRes; + public int iconAltDescriptionRes; + public int weight; + public boolean editable; + + public StringInflater actionHeader; + public StringInflater actionAltHeader; + public StringInflater actionBody; + + public String typeColumn; + + /** Maximum number of values allowed in the list. -1 represents infinity. */ + public int typeOverallMax; + + public List typeList; + public List fieldList; + + public ContentValues defaultValues; + + /** + * If this is a date field, this specifies the format of the date when saving. The date includes + * year, month and day. If this is not a date field or the date field is not editable, this value + * should be ignored. + */ + public SimpleDateFormat dateFormatWithoutYear; + + /** + * If this is a date field, this specifies the format of the date when saving. The date includes + * month and day. If this is not a date field, the field is not editable or dates without year are + * not supported, this value should be ignored. + */ + public SimpleDateFormat dateFormatWithYear; + + /** The number of lines available for displaying this kind of data. Defaults to 1. */ + public int maxLinesForDisplay; + + public DataKind() { + maxLinesForDisplay = 1; + } + + public DataKind(String mimeType, int titleRes, int weight, boolean editable) { + this.mimeType = mimeType; + this.titleRes = titleRes; + this.weight = weight; + this.editable = editable; + this.typeOverallMax = -1; + maxLinesForDisplay = 1; + } + + public static String toString(SimpleDateFormat format) { + return format == null ? "(null)" : format.toPattern(); + } + + public static String toString(Iterable list) { + if (list == null) { + return "(null)"; + } else { + return Iterators.toString(list.iterator()); + } + } + + public String getKindString(Context context) { + return (titleRes == -1 || titleRes == 0) ? "" : context.getString(titleRes); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append("DataKind:"); + sb.append(" resPackageName=").append(resourcePackageName); + sb.append(" mimeType=").append(mimeType); + sb.append(" titleRes=").append(titleRes); + sb.append(" iconAltRes=").append(iconAltRes); + sb.append(" iconAltDescriptionRes=").append(iconAltDescriptionRes); + sb.append(" weight=").append(weight); + sb.append(" editable=").append(editable); + sb.append(" actionHeader=").append(actionHeader); + sb.append(" actionAltHeader=").append(actionAltHeader); + sb.append(" actionBody=").append(actionBody); + sb.append(" typeColumn=").append(typeColumn); + sb.append(" typeOverallMax=").append(typeOverallMax); + sb.append(" typeList=").append(toString(typeList)); + sb.append(" fieldList=").append(toString(fieldList)); + sb.append(" defaultValues=").append(defaultValues); + sb.append(" dateFormatWithoutYear=").append(toString(dateFormatWithoutYear)); + sb.append(" dateFormatWithYear=").append(toString(dateFormatWithYear)); + + return sb.toString(); + } +} diff --git a/java/com/android/contacts/common/model/dataitem/EmailDataItem.java b/java/com/android/contacts/common/model/dataitem/EmailDataItem.java new file mode 100644 index 000000000..2fe297816 --- /dev/null +++ b/java/com/android/contacts/common/model/dataitem/EmailDataItem.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model.dataitem; + +import android.content.ContentValues; +import android.provider.ContactsContract.CommonDataKinds.Email; + +/** + * Represents an email data item, wrapping the columns in {@link + * ContactsContract.CommonDataKinds.Email}. + */ +public class EmailDataItem extends DataItem { + + /* package */ EmailDataItem(ContentValues values) { + super(values); + } + + public String getAddress() { + return getContentValues().getAsString(Email.ADDRESS); + } + + public String getDisplayName() { + return getContentValues().getAsString(Email.DISPLAY_NAME); + } + + public String getData() { + return getContentValues().getAsString(Email.DATA); + } + + public String getLabel() { + return getContentValues().getAsString(Email.LABEL); + } +} diff --git a/java/com/android/contacts/common/model/dataitem/EventDataItem.java b/java/com/android/contacts/common/model/dataitem/EventDataItem.java new file mode 100644 index 000000000..15d9880b1 --- /dev/null +++ b/java/com/android/contacts/common/model/dataitem/EventDataItem.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model.dataitem; + +import android.content.ContentValues; +import android.content.Context; +import android.provider.ContactsContract.CommonDataKinds.Event; +import android.text.TextUtils; + +/** + * Represents an event data item, wrapping the columns in {@link + * ContactsContract.CommonDataKinds.Event}. + */ +public class EventDataItem extends DataItem { + + /* package */ EventDataItem(ContentValues values) { + super(values); + } + + public String getStartDate() { + return getContentValues().getAsString(Event.START_DATE); + } + + public String getLabel() { + return getContentValues().getAsString(Event.LABEL); + } + + @Override + public boolean shouldCollapseWith(DataItem t, Context context) { + if (!(t instanceof EventDataItem) || mKind == null || t.getDataKind() == null) { + return false; + } + final EventDataItem that = (EventDataItem) t; + // Events can be different (anniversary, birthday) but have the same start date + if (!TextUtils.equals(getStartDate(), that.getStartDate())) { + return false; + } else if (!hasKindTypeColumn(mKind) || !that.hasKindTypeColumn(that.getDataKind())) { + return hasKindTypeColumn(mKind) == that.hasKindTypeColumn(that.getDataKind()); + } else if (getKindTypeColumn(mKind) != that.getKindTypeColumn(that.getDataKind())) { + return false; + } else if (getKindTypeColumn(mKind) == Event.TYPE_CUSTOM + && !TextUtils.equals(getLabel(), that.getLabel())) { + // Check if custom types are not the same + return false; + } + return true; + } +} diff --git a/java/com/android/contacts/common/model/dataitem/GroupMembershipDataItem.java b/java/com/android/contacts/common/model/dataitem/GroupMembershipDataItem.java new file mode 100644 index 000000000..f921b3c9d --- /dev/null +++ b/java/com/android/contacts/common/model/dataitem/GroupMembershipDataItem.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model.dataitem; + +import android.content.ContentValues; +import android.provider.ContactsContract; +import android.provider.ContactsContract.CommonDataKinds.GroupMembership; + +/** + * Represents a group memebership data item, wrapping the columns in {@link + * ContactsContract.CommonDataKinds.GroupMembership}. + */ +public class GroupMembershipDataItem extends DataItem { + + /* package */ GroupMembershipDataItem(ContentValues values) { + super(values); + } + + public Long getGroupRowId() { + return getContentValues().getAsLong(GroupMembership.GROUP_ROW_ID); + } + + public String getGroupSourceId() { + return getContentValues().getAsString(GroupMembership.GROUP_SOURCE_ID); + } +} diff --git a/java/com/android/contacts/common/model/dataitem/IdentityDataItem.java b/java/com/android/contacts/common/model/dataitem/IdentityDataItem.java new file mode 100644 index 000000000..2badf92f7 --- /dev/null +++ b/java/com/android/contacts/common/model/dataitem/IdentityDataItem.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model.dataitem; + +import android.content.ContentValues; +import android.provider.ContactsContract.CommonDataKinds.Identity; + +/** + * Represents an identity data item, wrapping the columns in {@link + * ContactsContract.CommonDataKinds.Identity}. + */ +public class IdentityDataItem extends DataItem { + + /* package */ IdentityDataItem(ContentValues values) { + super(values); + } + + public String getIdentity() { + return getContentValues().getAsString(Identity.IDENTITY); + } + + public String getNamespace() { + return getContentValues().getAsString(Identity.NAMESPACE); + } +} diff --git a/java/com/android/contacts/common/model/dataitem/ImDataItem.java b/java/com/android/contacts/common/model/dataitem/ImDataItem.java new file mode 100644 index 000000000..16b9fd094 --- /dev/null +++ b/java/com/android/contacts/common/model/dataitem/ImDataItem.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model.dataitem; + +import android.content.ContentValues; +import android.content.Context; +import android.provider.ContactsContract.CommonDataKinds.Email; +import android.provider.ContactsContract.CommonDataKinds.Im; +import android.text.TextUtils; + +/** + * Represents an IM data item, wrapping the columns in {@link ContactsContract.CommonDataKinds.Im}. + */ +public class ImDataItem extends DataItem { + + private final boolean mCreatedFromEmail; + + /* package */ ImDataItem(ContentValues values) { + super(values); + mCreatedFromEmail = false; + } + + private ImDataItem(ContentValues values, boolean createdFromEmail) { + super(values); + mCreatedFromEmail = createdFromEmail; + } + + public static ImDataItem createFromEmail(EmailDataItem item) { + final ImDataItem im = new ImDataItem(new ContentValues(item.getContentValues()), true); + im.setMimeType(Im.CONTENT_ITEM_TYPE); + return im; + } + + public String getData() { + if (mCreatedFromEmail) { + return getContentValues().getAsString(Email.DATA); + } else { + return getContentValues().getAsString(Im.DATA); + } + } + + public String getLabel() { + return getContentValues().getAsString(Im.LABEL); + } + + /** Values are one of Im.PROTOCOL_ */ + public Integer getProtocol() { + return getContentValues().getAsInteger(Im.PROTOCOL); + } + + public boolean isProtocolValid() { + return getProtocol() != null; + } + + public String getCustomProtocol() { + return getContentValues().getAsString(Im.CUSTOM_PROTOCOL); + } + + public int getChatCapability() { + Integer result = getContentValues().getAsInteger(Im.CHAT_CAPABILITY); + return result == null ? 0 : result; + } + + public boolean isCreatedFromEmail() { + return mCreatedFromEmail; + } + + @Override + public boolean shouldCollapseWith(DataItem t, Context context) { + if (!(t instanceof ImDataItem) || mKind == null || t.getDataKind() == null) { + return false; + } + final ImDataItem that = (ImDataItem) t; + // IM can have the same data put different protocol. These should not collapse. + if (!getData().equals(that.getData())) { + return false; + } else if (!isProtocolValid() || !that.isProtocolValid()) { + // Deal with invalid protocol as if it was custom. If either has a non valid + // protocol, check to see if the other has a valid that is not custom + if (isProtocolValid()) { + return getProtocol() == Im.PROTOCOL_CUSTOM; + } else if (that.isProtocolValid()) { + return that.getProtocol() == Im.PROTOCOL_CUSTOM; + } + return true; + } else if (getProtocol() != that.getProtocol()) { + return false; + } else if (getProtocol() == Im.PROTOCOL_CUSTOM + && !TextUtils.equals(getCustomProtocol(), that.getCustomProtocol())) { + // Check if custom protocols are not the same + return false; + } + return true; + } +} diff --git a/java/com/android/contacts/common/model/dataitem/NicknameDataItem.java b/java/com/android/contacts/common/model/dataitem/NicknameDataItem.java new file mode 100644 index 000000000..a448be786 --- /dev/null +++ b/java/com/android/contacts/common/model/dataitem/NicknameDataItem.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model.dataitem; + +import android.content.ContentValues; +import android.provider.ContactsContract.CommonDataKinds.Nickname; + +/** + * Represents a nickname data item, wrapping the columns in {@link + * ContactsContract.CommonDataKinds.Nickname}. + */ +public class NicknameDataItem extends DataItem { + + public NicknameDataItem(ContentValues values) { + super(values); + } + + public String getName() { + return getContentValues().getAsString(Nickname.NAME); + } + + public String getLabel() { + return getContentValues().getAsString(Nickname.LABEL); + } +} diff --git a/java/com/android/contacts/common/model/dataitem/NoteDataItem.java b/java/com/android/contacts/common/model/dataitem/NoteDataItem.java new file mode 100644 index 000000000..b55ecc3e5 --- /dev/null +++ b/java/com/android/contacts/common/model/dataitem/NoteDataItem.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model.dataitem; + +import android.content.ContentValues; +import android.provider.ContactsContract.CommonDataKinds.Note; + +/** + * Represents a note data item, wrapping the columns in {@link + * ContactsContract.CommonDataKinds.Note}. + */ +public class NoteDataItem extends DataItem { + + /* package */ NoteDataItem(ContentValues values) { + super(values); + } + + public String getNote() { + return getContentValues().getAsString(Note.NOTE); + } +} diff --git a/java/com/android/contacts/common/model/dataitem/OrganizationDataItem.java b/java/com/android/contacts/common/model/dataitem/OrganizationDataItem.java new file mode 100644 index 000000000..b33124838 --- /dev/null +++ b/java/com/android/contacts/common/model/dataitem/OrganizationDataItem.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model.dataitem; + +import android.content.ContentValues; +import android.provider.ContactsContract; +import android.provider.ContactsContract.CommonDataKinds.Organization; + +/** + * Represents an organization data item, wrapping the columns in {@link + * ContactsContract.CommonDataKinds.Organization}. + */ +public class OrganizationDataItem extends DataItem { + + /* package */ OrganizationDataItem(ContentValues values) { + super(values); + } + + public String getCompany() { + return getContentValues().getAsString(Organization.COMPANY); + } + + public String getLabel() { + return getContentValues().getAsString(Organization.LABEL); + } + + public String getTitle() { + return getContentValues().getAsString(Organization.TITLE); + } + + public String getDepartment() { + return getContentValues().getAsString(Organization.DEPARTMENT); + } + + public String getJobDescription() { + return getContentValues().getAsString(Organization.JOB_DESCRIPTION); + } + + public String getSymbol() { + return getContentValues().getAsString(Organization.SYMBOL); + } + + public String getPhoneticName() { + return getContentValues().getAsString(Organization.PHONETIC_NAME); + } + + public String getOfficeLocation() { + return getContentValues().getAsString(Organization.OFFICE_LOCATION); + } +} diff --git a/java/com/android/contacts/common/model/dataitem/PhoneDataItem.java b/java/com/android/contacts/common/model/dataitem/PhoneDataItem.java new file mode 100644 index 000000000..e1f56456a --- /dev/null +++ b/java/com/android/contacts/common/model/dataitem/PhoneDataItem.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model.dataitem; + +import android.content.ContentValues; +import android.content.Context; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import com.android.contacts.common.compat.PhoneNumberUtilsCompat; + +/** + * Represents a phone data item, wrapping the columns in {@link + * ContactsContract.CommonDataKinds.Phone}. + */ +public class PhoneDataItem extends DataItem { + + public static final String KEY_FORMATTED_PHONE_NUMBER = "formattedPhoneNumber"; + + /* package */ PhoneDataItem(ContentValues values) { + super(values); + } + + public String getNumber() { + return getContentValues().getAsString(Phone.NUMBER); + } + + /** Returns the normalized phone number in E164 format. */ + public String getNormalizedNumber() { + return getContentValues().getAsString(Phone.NORMALIZED_NUMBER); + } + + public String getFormattedPhoneNumber() { + return getContentValues().getAsString(KEY_FORMATTED_PHONE_NUMBER); + } + + public String getLabel() { + return getContentValues().getAsString(Phone.LABEL); + } + + public void computeFormattedPhoneNumber(String defaultCountryIso) { + final String phoneNumber = getNumber(); + if (phoneNumber != null) { + final String formattedPhoneNumber = + PhoneNumberUtilsCompat.formatNumber( + phoneNumber, getNormalizedNumber(), defaultCountryIso); + getContentValues().put(KEY_FORMATTED_PHONE_NUMBER, formattedPhoneNumber); + } + } + + /** + * Returns the formatted phone number (if already computed using {@link + * #computeFormattedPhoneNumber}). Otherwise this method returns the unformatted phone number. + */ + @Override + public String buildDataStringForDisplay(Context context, DataKind kind) { + final String formatted = getFormattedPhoneNumber(); + if (formatted != null) { + return formatted; + } else { + return getNumber(); + } + } +} diff --git a/java/com/android/contacts/common/model/dataitem/PhotoDataItem.java b/java/com/android/contacts/common/model/dataitem/PhotoDataItem.java new file mode 100644 index 000000000..0bf7a318b --- /dev/null +++ b/java/com/android/contacts/common/model/dataitem/PhotoDataItem.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model.dataitem; + +import android.content.ContentValues; +import android.provider.ContactsContract; +import android.provider.ContactsContract.Contacts.Photo; + +/** + * Represents a photo data item, wrapping the columns in {@link ContactsContract.Contacts.Photo}. + */ +public class PhotoDataItem extends DataItem { + + /* package */ PhotoDataItem(ContentValues values) { + super(values); + } + + public Long getPhotoFileId() { + return getContentValues().getAsLong(Photo.PHOTO_FILE_ID); + } + + public byte[] getPhoto() { + return getContentValues().getAsByteArray(Photo.PHOTO); + } +} diff --git a/java/com/android/contacts/common/model/dataitem/RelationDataItem.java b/java/com/android/contacts/common/model/dataitem/RelationDataItem.java new file mode 100644 index 000000000..fdbcbb313 --- /dev/null +++ b/java/com/android/contacts/common/model/dataitem/RelationDataItem.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model.dataitem; + +import android.content.ContentValues; +import android.content.Context; +import android.provider.ContactsContract.CommonDataKinds.Relation; +import android.text.TextUtils; + +/** + * Represents a relation data item, wrapping the columns in {@link + * ContactsContract.CommonDataKinds.Relation}. + */ +public class RelationDataItem extends DataItem { + + /* package */ RelationDataItem(ContentValues values) { + super(values); + } + + public String getName() { + return getContentValues().getAsString(Relation.NAME); + } + + public String getLabel() { + return getContentValues().getAsString(Relation.LABEL); + } + + @Override + public boolean shouldCollapseWith(DataItem t, Context context) { + if (!(t instanceof RelationDataItem) || mKind == null || t.getDataKind() == null) { + return false; + } + final RelationDataItem that = (RelationDataItem) t; + // Relations can have different types (assistant, father) but have the same name + if (!TextUtils.equals(getName(), that.getName())) { + return false; + } else if (!hasKindTypeColumn(mKind) || !that.hasKindTypeColumn(that.getDataKind())) { + return hasKindTypeColumn(mKind) == that.hasKindTypeColumn(that.getDataKind()); + } else if (getKindTypeColumn(mKind) != that.getKindTypeColumn(that.getDataKind())) { + return false; + } else if (getKindTypeColumn(mKind) == Relation.TYPE_CUSTOM + && !TextUtils.equals(getLabel(), that.getLabel())) { + // Check if custom types are not the same + return false; + } + return true; + } +} diff --git a/java/com/android/contacts/common/model/dataitem/SipAddressDataItem.java b/java/com/android/contacts/common/model/dataitem/SipAddressDataItem.java new file mode 100644 index 000000000..0ca9eae6d --- /dev/null +++ b/java/com/android/contacts/common/model/dataitem/SipAddressDataItem.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model.dataitem; + +import android.content.ContentValues; +import android.provider.ContactsContract; +import android.provider.ContactsContract.CommonDataKinds.SipAddress; + +/** + * Represents a sip address data item, wrapping the columns in {@link + * ContactsContract.CommonDataKinds.SipAddress}. + */ +public class SipAddressDataItem extends DataItem { + + /* package */ SipAddressDataItem(ContentValues values) { + super(values); + } + + public String getSipAddress() { + return getContentValues().getAsString(SipAddress.SIP_ADDRESS); + } + + public String getLabel() { + return getContentValues().getAsString(SipAddress.LABEL); + } +} diff --git a/java/com/android/contacts/common/model/dataitem/StructuredNameDataItem.java b/java/com/android/contacts/common/model/dataitem/StructuredNameDataItem.java new file mode 100644 index 000000000..22bf037f1 --- /dev/null +++ b/java/com/android/contacts/common/model/dataitem/StructuredNameDataItem.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model.dataitem; + +import android.content.ContentValues; +import android.provider.ContactsContract.CommonDataKinds.StructuredName; +import android.provider.ContactsContract.Contacts.Data; + +/** + * Represents a structured name data item, wrapping the columns in {@link + * ContactsContract.CommonDataKinds.StructuredName}. + */ +public class StructuredNameDataItem extends DataItem { + + public StructuredNameDataItem() { + super(new ContentValues()); + getContentValues().put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE); + } + + /* package */ StructuredNameDataItem(ContentValues values) { + super(values); + } + + public String getDisplayName() { + return getContentValues().getAsString(StructuredName.DISPLAY_NAME); + } + + public void setDisplayName(String name) { + getContentValues().put(StructuredName.DISPLAY_NAME, name); + } + + public String getGivenName() { + return getContentValues().getAsString(StructuredName.GIVEN_NAME); + } + + public String getFamilyName() { + return getContentValues().getAsString(StructuredName.FAMILY_NAME); + } + + public String getPrefix() { + return getContentValues().getAsString(StructuredName.PREFIX); + } + + public String getMiddleName() { + return getContentValues().getAsString(StructuredName.MIDDLE_NAME); + } + + public String getSuffix() { + return getContentValues().getAsString(StructuredName.SUFFIX); + } + + public String getPhoneticGivenName() { + return getContentValues().getAsString(StructuredName.PHONETIC_GIVEN_NAME); + } + + public void setPhoneticGivenName(String name) { + getContentValues().put(StructuredName.PHONETIC_GIVEN_NAME, name); + } + + public String getPhoneticMiddleName() { + return getContentValues().getAsString(StructuredName.PHONETIC_MIDDLE_NAME); + } + + public void setPhoneticMiddleName(String name) { + getContentValues().put(StructuredName.PHONETIC_MIDDLE_NAME, name); + } + + public String getPhoneticFamilyName() { + return getContentValues().getAsString(StructuredName.PHONETIC_FAMILY_NAME); + } + + public void setPhoneticFamilyName(String name) { + getContentValues().put(StructuredName.PHONETIC_FAMILY_NAME, name); + } + + public String getFullNameStyle() { + return getContentValues().getAsString(StructuredName.FULL_NAME_STYLE); + } + + public boolean isSuperPrimary() { + final ContentValues contentValues = getContentValues(); + return contentValues == null || !contentValues.containsKey(StructuredName.IS_SUPER_PRIMARY) + ? false + : contentValues.getAsBoolean(StructuredName.IS_SUPER_PRIMARY); + } +} diff --git a/java/com/android/contacts/common/model/dataitem/StructuredPostalDataItem.java b/java/com/android/contacts/common/model/dataitem/StructuredPostalDataItem.java new file mode 100644 index 000000000..18aae282c --- /dev/null +++ b/java/com/android/contacts/common/model/dataitem/StructuredPostalDataItem.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model.dataitem; + +import android.content.ContentValues; +import android.provider.ContactsContract; +import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; + +/** + * Represents a structured postal data item, wrapping the columns in {@link + * ContactsContract.CommonDataKinds.StructuredPostal}. + */ +public class StructuredPostalDataItem extends DataItem { + + /* package */ StructuredPostalDataItem(ContentValues values) { + super(values); + } + + public String getFormattedAddress() { + return getContentValues().getAsString(StructuredPostal.FORMATTED_ADDRESS); + } + + public String getLabel() { + return getContentValues().getAsString(StructuredPostal.LABEL); + } + + public String getStreet() { + return getContentValues().getAsString(StructuredPostal.STREET); + } + + public String getPOBox() { + return getContentValues().getAsString(StructuredPostal.POBOX); + } + + public String getNeighborhood() { + return getContentValues().getAsString(StructuredPostal.NEIGHBORHOOD); + } + + public String getCity() { + return getContentValues().getAsString(StructuredPostal.CITY); + } + + public String getRegion() { + return getContentValues().getAsString(StructuredPostal.REGION); + } + + public String getPostcode() { + return getContentValues().getAsString(StructuredPostal.POSTCODE); + } + + public String getCountry() { + return getContentValues().getAsString(StructuredPostal.COUNTRY); + } +} diff --git a/java/com/android/contacts/common/model/dataitem/WebsiteDataItem.java b/java/com/android/contacts/common/model/dataitem/WebsiteDataItem.java new file mode 100644 index 000000000..b8400ecd1 --- /dev/null +++ b/java/com/android/contacts/common/model/dataitem/WebsiteDataItem.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.model.dataitem; + +import android.content.ContentValues; +import android.provider.ContactsContract.CommonDataKinds.Website; + +/** + * Represents a website data item, wrapping the columns in {@link + * ContactsContract.CommonDataKinds.Website}. + */ +public class WebsiteDataItem extends DataItem { + + /* package */ WebsiteDataItem(ContentValues values) { + super(values); + } + + public String getUrl() { + return getContentValues().getAsString(Website.URL); + } + + public String getLabel() { + return getContentValues().getAsString(Website.LABEL); + } +} diff --git a/java/com/android/contacts/common/preference/ContactsPreferences.java b/java/com/android/contacts/common/preference/ContactsPreferences.java new file mode 100644 index 000000000..7f0d99acd --- /dev/null +++ b/java/com/android/contacts/common/preference/ContactsPreferences.java @@ -0,0 +1,269 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.preference; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; +import android.content.SharedPreferences.OnSharedPreferenceChangeListener; +import android.os.Handler; +import android.preference.PreferenceManager; +import android.provider.Settings; +import android.provider.Settings.SettingNotFoundException; +import android.text.TextUtils; +import com.android.contacts.common.R; +import com.android.contacts.common.model.account.AccountWithDataSet; + +/** Manages user preferences for contacts. */ +public class ContactsPreferences implements OnSharedPreferenceChangeListener { + + /** The value for the DISPLAY_ORDER key to show the given name first. */ + public static final int DISPLAY_ORDER_PRIMARY = 1; + + /** The value for the DISPLAY_ORDER key to show the family name first. */ + public static final int DISPLAY_ORDER_ALTERNATIVE = 2; + + public static final String DISPLAY_ORDER_KEY = "android.contacts.DISPLAY_ORDER"; + + /** The value for the SORT_ORDER key corresponding to sort by given name first. */ + public static final int SORT_ORDER_PRIMARY = 1; + + public static final String SORT_ORDER_KEY = "android.contacts.SORT_ORDER"; + + /** The value for the SORT_ORDER key corresponding to sort by family name first. */ + public static final int SORT_ORDER_ALTERNATIVE = 2; + + public static final String PREF_DISPLAY_ONLY_PHONES = "only_phones"; + + public static final boolean PREF_DISPLAY_ONLY_PHONES_DEFAULT = false; + + /** + * Value to use when a preference is unassigned and needs to be read from the shared preferences + */ + private static final int PREFERENCE_UNASSIGNED = -1; + + private final Context mContext; + private final SharedPreferences mPreferences; + private int mSortOrder = PREFERENCE_UNASSIGNED; + private int mDisplayOrder = PREFERENCE_UNASSIGNED; + private String mDefaultAccount = null; + private ChangeListener mListener = null; + private Handler mHandler; + private String mDefaultAccountKey; + private String mDefaultAccountSavedKey; + + public ContactsPreferences(Context context) { + mContext = context; + mHandler = new Handler(); + mPreferences = mContext.getSharedPreferences(context.getPackageName(), Context.MODE_PRIVATE); + mDefaultAccountKey = + mContext.getResources().getString(R.string.contact_editor_default_account_key); + mDefaultAccountSavedKey = + mContext.getResources().getString(R.string.contact_editor_anything_saved_key); + maybeMigrateSystemSettings(); + } + + public boolean isSortOrderUserChangeable() { + return mContext.getResources().getBoolean(R.bool.config_sort_order_user_changeable); + } + + public int getDefaultSortOrder() { + if (mContext.getResources().getBoolean(R.bool.config_default_sort_order_primary)) { + return SORT_ORDER_PRIMARY; + } else { + return SORT_ORDER_ALTERNATIVE; + } + } + + public int getSortOrder() { + if (!isSortOrderUserChangeable()) { + return getDefaultSortOrder(); + } + if (mSortOrder == PREFERENCE_UNASSIGNED) { + mSortOrder = mPreferences.getInt(SORT_ORDER_KEY, getDefaultSortOrder()); + } + return mSortOrder; + } + + public void setSortOrder(int sortOrder) { + mSortOrder = sortOrder; + final Editor editor = mPreferences.edit(); + editor.putInt(SORT_ORDER_KEY, sortOrder); + editor.commit(); + } + + public boolean isDisplayOrderUserChangeable() { + return mContext.getResources().getBoolean(R.bool.config_display_order_user_changeable); + } + + public int getDefaultDisplayOrder() { + if (mContext.getResources().getBoolean(R.bool.config_default_display_order_primary)) { + return DISPLAY_ORDER_PRIMARY; + } else { + return DISPLAY_ORDER_ALTERNATIVE; + } + } + + public int getDisplayOrder() { + if (!isDisplayOrderUserChangeable()) { + return getDefaultDisplayOrder(); + } + if (mDisplayOrder == PREFERENCE_UNASSIGNED) { + mDisplayOrder = mPreferences.getInt(DISPLAY_ORDER_KEY, getDefaultDisplayOrder()); + } + return mDisplayOrder; + } + + public void setDisplayOrder(int displayOrder) { + mDisplayOrder = displayOrder; + final Editor editor = mPreferences.edit(); + editor.putInt(DISPLAY_ORDER_KEY, displayOrder); + editor.commit(); + } + + public boolean isDefaultAccountUserChangeable() { + return mContext.getResources().getBoolean(R.bool.config_default_account_user_changeable); + } + + public String getDefaultAccount() { + if (!isDefaultAccountUserChangeable()) { + return mDefaultAccount; + } + if (TextUtils.isEmpty(mDefaultAccount)) { + final String accountString = mPreferences.getString(mDefaultAccountKey, mDefaultAccount); + if (!TextUtils.isEmpty(accountString)) { + final AccountWithDataSet accountWithDataSet = AccountWithDataSet.unstringify(accountString); + mDefaultAccount = accountWithDataSet.name; + } + } + return mDefaultAccount; + } + + public void setDefaultAccount(AccountWithDataSet accountWithDataSet) { + mDefaultAccount = accountWithDataSet == null ? null : accountWithDataSet.name; + final Editor editor = mPreferences.edit(); + if (TextUtils.isEmpty(mDefaultAccount)) { + editor.remove(mDefaultAccountKey); + } else { + editor.putString(mDefaultAccountKey, accountWithDataSet.stringify()); + } + editor.putBoolean(mDefaultAccountSavedKey, true); + editor.commit(); + } + + public void registerChangeListener(ChangeListener listener) { + if (mListener != null) { + unregisterChangeListener(); + } + + mListener = listener; + + // Reset preferences to "unknown" because they may have changed while the + // listener was unregistered. + mDisplayOrder = PREFERENCE_UNASSIGNED; + mSortOrder = PREFERENCE_UNASSIGNED; + mDefaultAccount = null; + + mPreferences.registerOnSharedPreferenceChangeListener(this); + } + + public void unregisterChangeListener() { + if (mListener != null) { + mListener = null; + } + + mPreferences.unregisterOnSharedPreferenceChangeListener(this); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, final String key) { + // This notification is not sent on the Ui thread. Use the previously created Handler + // to switch to the Ui thread + mHandler.post( + new Runnable() { + @Override + public void run() { + refreshValue(key); + } + }); + } + + /** + * Forces the value for the given key to be looked up from shared preferences and notifies the + * registered {@link ChangeListener} + * + * @param key the {@link SharedPreferences} key to look up + */ + public void refreshValue(String key) { + if (DISPLAY_ORDER_KEY.equals(key)) { + mDisplayOrder = PREFERENCE_UNASSIGNED; + mDisplayOrder = getDisplayOrder(); + } else if (SORT_ORDER_KEY.equals(key)) { + mSortOrder = PREFERENCE_UNASSIGNED; + mSortOrder = getSortOrder(); + } else if (mDefaultAccountKey.equals(key)) { + mDefaultAccount = null; + mDefaultAccount = getDefaultAccount(); + } + if (mListener != null) { + mListener.onChange(); + } + } + + /** + * If there are currently no preferences (which means this is the first time we are run), For sort + * order and display order, check to see if there are any preferences stored in system settings + * (pre-L) which can be copied into our own SharedPreferences. For default account setting, check + * to see if there are any preferences stored in the previous SharedPreferences which can be + * copied into current SharedPreferences. + */ + private void maybeMigrateSystemSettings() { + if (!mPreferences.contains(SORT_ORDER_KEY)) { + int sortOrder = getDefaultSortOrder(); + try { + sortOrder = Settings.System.getInt(mContext.getContentResolver(), SORT_ORDER_KEY); + } catch (SettingNotFoundException e) { + } + setSortOrder(sortOrder); + } + + if (!mPreferences.contains(DISPLAY_ORDER_KEY)) { + int displayOrder = getDefaultDisplayOrder(); + try { + displayOrder = Settings.System.getInt(mContext.getContentResolver(), DISPLAY_ORDER_KEY); + } catch (SettingNotFoundException e) { + } + setDisplayOrder(displayOrder); + } + + if (!mPreferences.contains(mDefaultAccountKey)) { + final SharedPreferences previousPrefs = + PreferenceManager.getDefaultSharedPreferences(mContext); + final String defaultAccount = previousPrefs.getString(mDefaultAccountKey, null); + if (!TextUtils.isEmpty(defaultAccount)) { + final AccountWithDataSet accountWithDataSet = + AccountWithDataSet.unstringify(defaultAccount); + setDefaultAccount(accountWithDataSet); + } + } + } + + public interface ChangeListener { + + void onChange(); + } +} diff --git a/java/com/android/contacts/common/preference/DisplayOrderPreference.java b/java/com/android/contacts/common/preference/DisplayOrderPreference.java new file mode 100644 index 000000000..8dda57f9f --- /dev/null +++ b/java/com/android/contacts/common/preference/DisplayOrderPreference.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.preference; + +import android.app.AlertDialog.Builder; +import android.content.Context; +import android.preference.ListPreference; +import android.util.AttributeSet; +import com.android.contacts.common.R; + +/** Custom preference: view-name-as (first name first or last name first). */ +public final class DisplayOrderPreference extends ListPreference { + + private ContactsPreferences mPreferences; + private Context mContext; + + public DisplayOrderPreference(Context context) { + super(context); + prepare(); + } + + public DisplayOrderPreference(Context context, AttributeSet attrs) { + super(context, attrs); + prepare(); + } + + private void prepare() { + mContext = getContext(); + mPreferences = new ContactsPreferences(mContext); + setEntries( + new String[] { + mContext.getString(R.string.display_options_view_given_name_first), + mContext.getString(R.string.display_options_view_family_name_first), + }); + setEntryValues( + new String[] { + String.valueOf(ContactsPreferences.DISPLAY_ORDER_PRIMARY), + String.valueOf(ContactsPreferences.DISPLAY_ORDER_ALTERNATIVE), + }); + setValue(String.valueOf(mPreferences.getDisplayOrder())); + } + + @Override + protected boolean shouldPersist() { + return false; // This preference takes care of its own storage + } + + @Override + public CharSequence getSummary() { + switch (mPreferences.getDisplayOrder()) { + case ContactsPreferences.DISPLAY_ORDER_PRIMARY: + return mContext.getString(R.string.display_options_view_given_name_first); + case ContactsPreferences.DISPLAY_ORDER_ALTERNATIVE: + return mContext.getString(R.string.display_options_view_family_name_first); + } + return null; + } + + @Override + protected boolean persistString(String value) { + int newValue = Integer.parseInt(value); + if (newValue != mPreferences.getDisplayOrder()) { + mPreferences.setDisplayOrder(newValue); + notifyChanged(); + } + return true; + } + + @Override + // UX recommendation is not to show cancel button on such lists. + protected void onPrepareDialogBuilder(Builder builder) { + super.onPrepareDialogBuilder(builder); + builder.setNegativeButton(null, null); + } +} diff --git a/java/com/android/contacts/common/preference/SortOrderPreference.java b/java/com/android/contacts/common/preference/SortOrderPreference.java new file mode 100644 index 000000000..9b6f57860 --- /dev/null +++ b/java/com/android/contacts/common/preference/SortOrderPreference.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.preference; + +import android.app.AlertDialog.Builder; +import android.content.Context; +import android.preference.ListPreference; +import android.util.AttributeSet; +import com.android.contacts.common.R; + +/** Custom preference: sort-by. */ +public final class SortOrderPreference extends ListPreference { + + private ContactsPreferences mPreferences; + private Context mContext; + + public SortOrderPreference(Context context) { + super(context); + prepare(); + } + + public SortOrderPreference(Context context, AttributeSet attrs) { + super(context, attrs); + prepare(); + } + + private void prepare() { + mContext = getContext(); + mPreferences = new ContactsPreferences(mContext); + setEntries( + new String[] { + mContext.getString(R.string.display_options_sort_by_given_name), + mContext.getString(R.string.display_options_sort_by_family_name), + }); + setEntryValues( + new String[] { + String.valueOf(ContactsPreferences.SORT_ORDER_PRIMARY), + String.valueOf(ContactsPreferences.SORT_ORDER_ALTERNATIVE), + }); + setValue(String.valueOf(mPreferences.getSortOrder())); + } + + @Override + protected boolean shouldPersist() { + return false; // This preference takes care of its own storage + } + + @Override + public CharSequence getSummary() { + switch (mPreferences.getSortOrder()) { + case ContactsPreferences.SORT_ORDER_PRIMARY: + return mContext.getString(R.string.display_options_sort_by_given_name); + case ContactsPreferences.SORT_ORDER_ALTERNATIVE: + return mContext.getString(R.string.display_options_sort_by_family_name); + } + return null; + } + + @Override + protected boolean persistString(String value) { + int newValue = Integer.parseInt(value); + if (newValue != mPreferences.getSortOrder()) { + mPreferences.setSortOrder(newValue); + notifyChanged(); + } + return true; + } + + @Override + // UX recommendation is not to show cancel button on such lists. + protected void onPrepareDialogBuilder(Builder builder) { + super.onPrepareDialogBuilder(builder); + builder.setNegativeButton(null, null); + } +} diff --git a/java/com/android/contacts/common/res/color/popup_menu_color.xml b/java/com/android/contacts/common/res/color/popup_menu_color.xml new file mode 100644 index 000000000..c52bd5b50 --- /dev/null +++ b/java/com/android/contacts/common/res/color/popup_menu_color.xml @@ -0,0 +1,20 @@ + + + + + + + \ No newline at end of file diff --git a/java/com/android/contacts/common/res/color/tab_text_color.xml b/java/com/android/contacts/common/res/color/tab_text_color.xml new file mode 100644 index 000000000..71ef3e903 --- /dev/null +++ b/java/com/android/contacts/common/res/color/tab_text_color.xml @@ -0,0 +1,21 @@ + + + + + + + \ No newline at end of file diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_ab_search.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_ab_search.png new file mode 100644 index 000000000..d86b2195a Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_ab_search.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_arrow_back_24dp.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_arrow_back_24dp.png new file mode 100644 index 000000000..ddbb2c459 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_arrow_back_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_business_white_120dp.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_business_white_120dp.png new file mode 100644 index 000000000..d5942dcad Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_business_white_120dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_call_24dp.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_call_24dp.png new file mode 100644 index 000000000..4dc506515 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_call_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_call_note_white_24dp.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_call_note_white_24dp.png new file mode 100644 index 000000000..503e58e22 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_call_note_white_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_close_dk.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_close_dk.png new file mode 100644 index 000000000..969552935 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_close_dk.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_create_24dp.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_create_24dp.png new file mode 100644 index 000000000..540ab4dee Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_create_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_group_white_24dp.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_group_white_24dp.png new file mode 100644 index 000000000..017e4bbf7 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_group_white_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_history_white_drawable_24dp.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_history_white_drawable_24dp.png new file mode 100644 index 000000000..703d30b92 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_history_white_drawable_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_info_outline_24dp.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_info_outline_24dp.png new file mode 100644 index 000000000..c7b1113cf Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_info_outline_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_back.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_back.png new file mode 100644 index 000000000..deb3a6dc1 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_back.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_group_dk.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_group_dk.png new file mode 100644 index 000000000..06bd18fbb Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_group_dk.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_group_lt.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_group_lt.png new file mode 100644 index 000000000..d829d11e2 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_group_lt.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_overflow_lt.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_overflow_lt.png new file mode 100644 index 000000000..1ba12950c Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_overflow_lt.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_person_dk.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_person_dk.png new file mode 100644 index 000000000..5ff3ac574 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_person_dk.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_person_lt.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_person_lt.png new file mode 100644 index 000000000..b4ebfc7b2 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_person_lt.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_remove_field_holo_light.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_remove_field_holo_light.png new file mode 100644 index 000000000..03fd2fb10 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_remove_field_holo_light.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_dk.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_dk.png new file mode 100644 index 000000000..e8cb0f5fe Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_dk.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_holo_light.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_holo_light.png new file mode 100644 index 000000000..45137967c Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_holo_light.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_lt.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_lt.png new file mode 100644 index 000000000..1c9bb81fa Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_lt.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_message_24dp.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_message_24dp.png new file mode 100644 index 000000000..57177b7c6 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_message_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_person_24dp.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_person_24dp.png new file mode 100644 index 000000000..56708b0ba Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_person_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_person_add_24dp.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_person_add_24dp.png new file mode 100644 index 000000000..10ae5a70c Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_person_add_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_phone_attach.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_phone_attach.png new file mode 100644 index 000000000..84b1227bd Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_phone_attach.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_rx_videocam.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_rx_videocam.png new file mode 100644 index 000000000..ccdda6701 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_rx_videocam.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_scroll_handle.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_scroll_handle.png new file mode 100644 index 000000000..3aa29b852 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_scroll_handle.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_tx_videocam.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_tx_videocam.png new file mode 100644 index 000000000..603ddc895 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_tx_videocam.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_videocam.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_videocam.png new file mode 100644 index 000000000..97905c9f5 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_videocam.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_voicemail_avatar.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_voicemail_avatar.png new file mode 100644 index 000000000..c74bfab13 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_voicemail_avatar.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/list_activated_holo.9.png b/java/com/android/contacts/common/res/drawable-hdpi/list_activated_holo.9.png new file mode 100644 index 000000000..4ea7afa00 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/list_activated_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/list_background_holo.9.png b/java/com/android/contacts/common/res/drawable-hdpi/list_background_holo.9.png new file mode 100644 index 000000000..cddf9be75 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/list_background_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/list_focused_holo.9.png b/java/com/android/contacts/common/res/drawable-hdpi/list_focused_holo.9.png new file mode 100644 index 000000000..86578be45 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/list_focused_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/list_longpressed_holo_light.9.png b/java/com/android/contacts/common/res/drawable-hdpi/list_longpressed_holo_light.9.png new file mode 100644 index 000000000..e9afcc924 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/list_longpressed_holo_light.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/list_pressed_holo_light.9.png b/java/com/android/contacts/common/res/drawable-hdpi/list_pressed_holo_light.9.png new file mode 100644 index 000000000..2054530ed Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/list_pressed_holo_light.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/list_section_divider_holo_custom.9.png b/java/com/android/contacts/common/res/drawable-hdpi/list_section_divider_holo_custom.9.png new file mode 100644 index 000000000..a0f17568e Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/list_section_divider_holo_custom.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-hdpi/list_title_holo.9.png b/java/com/android/contacts/common/res/drawable-hdpi/list_title_holo.9.png new file mode 100644 index 000000000..ae937176e Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/list_title_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_background_holo.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_background_holo.9.png new file mode 100644 index 000000000..0d80482a9 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_background_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_focused_holo.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_focused_holo.9.png new file mode 100644 index 000000000..4139942d6 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_focused_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_section_divider_holo_custom.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_section_divider_holo_custom.9.png new file mode 100644 index 000000000..569d28f54 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_section_divider_holo_custom.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_title_holo.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_title_holo.9.png new file mode 100644 index 000000000..5ec4c96a7 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_title_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_background_holo.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_background_holo.9.png new file mode 100644 index 000000000..d86d61164 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_background_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_focused_holo.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_focused_holo.9.png new file mode 100644 index 000000000..4139942d6 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_focused_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_section_divider_holo_custom.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_section_divider_holo_custom.9.png new file mode 100644 index 000000000..065ff62ce Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_section_divider_holo_custom.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_title_holo.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_title_holo.9.png new file mode 100644 index 000000000..013d5e711 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_title_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-hdpi/list_activated_holo.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-hdpi/list_activated_holo.9.png new file mode 100644 index 000000000..947f03cec Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-hdpi/list_activated_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-mdpi/list_activated_holo.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-mdpi/list_activated_holo.9.png new file mode 100644 index 000000000..6d09d7278 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-mdpi/list_activated_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-xhdpi/list_activated_holo.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-xhdpi/list_activated_holo.9.png new file mode 100644 index 000000000..63c7456f0 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-xhdpi/list_activated_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_background_holo.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_background_holo.9.png new file mode 100644 index 000000000..f709f2ce4 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_background_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_focused_holo.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_focused_holo.9.png new file mode 100644 index 000000000..4139942d6 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_focused_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_section_divider_holo_custom.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_section_divider_holo_custom.9.png new file mode 100644 index 000000000..af5855420 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_section_divider_holo_custom.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_title_holo.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_title_holo.9.png new file mode 100644 index 000000000..cb801ac1b Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_title_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_ab_search.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_ab_search.png new file mode 100644 index 000000000..2b23b1ec5 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_ab_search.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_arrow_back_24dp.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_arrow_back_24dp.png new file mode 100644 index 000000000..1a21fb400 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_arrow_back_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_business_white_120dp.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_business_white_120dp.png new file mode 100644 index 000000000..3dddca516 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_business_white_120dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_call_24dp.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_call_24dp.png new file mode 100644 index 000000000..77f9de5e3 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_call_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_call_note_white_24dp.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_call_note_white_24dp.png new file mode 100644 index 000000000..9d359db9f Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_call_note_white_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_close_dk.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_close_dk.png new file mode 100644 index 000000000..590a728ad Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_close_dk.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_create_24dp.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_create_24dp.png new file mode 100644 index 000000000..8a2df3992 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_create_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_group_white_24dp.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_group_white_24dp.png new file mode 100644 index 000000000..ad268bf2f Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_group_white_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_history_white_drawable_24dp.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_history_white_drawable_24dp.png new file mode 100644 index 000000000..b3000d31e Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_history_white_drawable_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_info_outline_24dp.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_info_outline_24dp.png new file mode 100644 index 000000000..353e06495 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_info_outline_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_back.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_back.png new file mode 100644 index 000000000..201ad40cb Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_back.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_group_dk.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_group_dk.png new file mode 100644 index 000000000..9aac6d79b Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_group_dk.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_group_lt.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_group_lt.png new file mode 100644 index 000000000..39c16ed2d Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_group_lt.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_overflow_lt.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_overflow_lt.png new file mode 100644 index 000000000..841509682 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_overflow_lt.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_person_dk.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_person_dk.png new file mode 100644 index 000000000..b8fc39aee Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_person_dk.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_person_lt.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_person_lt.png new file mode 100644 index 000000000..736dfd6fa Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_person_lt.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_remove_field_holo_light.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_remove_field_holo_light.png new file mode 100644 index 000000000..8c44e7015 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_remove_field_holo_light.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_star_dk.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_star_dk.png new file mode 100644 index 000000000..c16c6c5de Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_star_dk.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_star_lt.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_star_lt.png new file mode 100644 index 000000000..c67170e31 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_star_lt.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_message_24dp.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_message_24dp.png new file mode 100644 index 000000000..3072b7569 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_message_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_person_24dp.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_person_24dp.png new file mode 100644 index 000000000..f0b1c725d Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_person_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_person_add_24dp.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_person_add_24dp.png new file mode 100644 index 000000000..38e0a2882 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_person_add_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_phone_attach.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_phone_attach.png new file mode 100644 index 000000000..fc4ddd32c Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_phone_attach.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_rx_videocam.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_rx_videocam.png new file mode 100644 index 000000000..1b43a07d0 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_rx_videocam.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_scroll_handle.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_scroll_handle.png new file mode 100644 index 000000000..af75db4b4 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_scroll_handle.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_tx_videocam.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_tx_videocam.png new file mode 100644 index 000000000..64995d147 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_tx_videocam.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_videocam.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_videocam.png new file mode 100644 index 000000000..dc9655b6d Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_videocam.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_voicemail_avatar.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_voicemail_avatar.png new file mode 100644 index 000000000..a5a30213d Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_voicemail_avatar.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/list_activated_holo.9.png b/java/com/android/contacts/common/res/drawable-mdpi/list_activated_holo.9.png new file mode 100644 index 000000000..3bf8e0362 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/list_activated_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/list_background_holo.9.png b/java/com/android/contacts/common/res/drawable-mdpi/list_background_holo.9.png new file mode 100644 index 000000000..7d5d66de3 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/list_background_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/list_focused_holo.9.png b/java/com/android/contacts/common/res/drawable-mdpi/list_focused_holo.9.png new file mode 100644 index 000000000..86578be45 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/list_focused_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/list_longpressed_holo_light.9.png b/java/com/android/contacts/common/res/drawable-mdpi/list_longpressed_holo_light.9.png new file mode 100644 index 000000000..3226ab760 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/list_longpressed_holo_light.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/list_pressed_holo_light.9.png b/java/com/android/contacts/common/res/drawable-mdpi/list_pressed_holo_light.9.png new file mode 100644 index 000000000..061904c42 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/list_pressed_holo_light.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/list_section_divider_holo_custom.9.png b/java/com/android/contacts/common/res/drawable-mdpi/list_section_divider_holo_custom.9.png new file mode 100644 index 000000000..1d9371de0 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/list_section_divider_holo_custom.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-mdpi/list_title_holo.9.png b/java/com/android/contacts/common/res/drawable-mdpi/list_title_holo.9.png new file mode 100644 index 000000000..64bd6912c Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/list_title_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-sw600dp-hdpi/list_activated_holo.9.png b/java/com/android/contacts/common/res/drawable-sw600dp-hdpi/list_activated_holo.9.png new file mode 100644 index 000000000..046b24a96 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-sw600dp-hdpi/list_activated_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-sw600dp-mdpi/list_activated_holo.9.png b/java/com/android/contacts/common/res/drawable-sw600dp-mdpi/list_activated_holo.9.png new file mode 100644 index 000000000..1ff337370 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-sw600dp-mdpi/list_activated_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-sw600dp-xhdpi/list_activated_holo.9.png b/java/com/android/contacts/common/res/drawable-sw600dp-xhdpi/list_activated_holo.9.png new file mode 100644 index 000000000..2eb7c7ebc Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-sw600dp-xhdpi/list_activated_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_ab_search.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_ab_search.png new file mode 100644 index 000000000..71f782701 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_ab_search.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_arrow_back_24dp.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_arrow_back_24dp.png new file mode 100644 index 000000000..bb7327251 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_arrow_back_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_business_white_120dp.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_business_white_120dp.png new file mode 100644 index 000000000..6256300b4 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_business_white_120dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_call_24dp.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_call_24dp.png new file mode 100644 index 000000000..ef45e933a Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_call_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_call_note_white_24dp.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_call_note_white_24dp.png new file mode 100644 index 000000000..40eed1d12 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_call_note_white_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_close_dk.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_close_dk.png new file mode 100644 index 000000000..5769f1178 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_close_dk.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_create_24dp.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_create_24dp.png new file mode 100644 index 000000000..48e75beee Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_create_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_group_white_24dp.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_group_white_24dp.png new file mode 100644 index 000000000..09c0e3efd Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_group_white_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_history_white_drawable_24dp.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_history_white_drawable_24dp.png new file mode 100644 index 000000000..e188d4a37 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_history_white_drawable_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_info_outline_24dp.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_info_outline_24dp.png new file mode 100644 index 000000000..c571b2e3e Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_info_outline_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_back.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_back.png new file mode 100644 index 000000000..d2f709942 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_back.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_group_dk.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_group_dk.png new file mode 100644 index 000000000..ce5f704ec Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_group_dk.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_group_lt.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_group_lt.png new file mode 100644 index 000000000..3d0580f93 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_group_lt.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_overflow_lt.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_overflow_lt.png new file mode 100644 index 000000000..f91b71847 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_overflow_lt.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_person_dk.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_person_dk.png new file mode 100644 index 000000000..2fbd458e9 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_person_dk.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_person_lt.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_person_lt.png new file mode 100644 index 000000000..2cdb2d7a1 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_person_lt.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_remove_field_holo_light.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_remove_field_holo_light.png new file mode 100644 index 000000000..65a6b7bbb Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_remove_field_holo_light.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_dk.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_dk.png new file mode 100644 index 000000000..48483a0b6 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_dk.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_holo_light.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_holo_light.png new file mode 100644 index 000000000..906791177 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_holo_light.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_lt.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_lt.png new file mode 100644 index 000000000..e053c757a Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_lt.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_message_24dp.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_message_24dp.png new file mode 100644 index 000000000..763767b4f Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_message_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_person_24dp.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_person_24dp.png new file mode 100644 index 000000000..aea15f0be Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_person_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_person_add_24dp.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_person_add_24dp.png new file mode 100644 index 000000000..7e7c289d4 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_person_add_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_phone_attach.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_phone_attach.png new file mode 100644 index 000000000..fdfafed9a Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_phone_attach.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_rx_videocam.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_rx_videocam.png new file mode 100644 index 000000000..43319dc92 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_rx_videocam.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_scroll_handle.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_scroll_handle.png new file mode 100644 index 000000000..2d43c4d5b Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_scroll_handle.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_tx_videocam.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_tx_videocam.png new file mode 100644 index 000000000..d2671edf7 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_tx_videocam.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_videocam.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_videocam.png new file mode 100644 index 000000000..c1783de67 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_videocam.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_voicemail_avatar.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_voicemail_avatar.png new file mode 100644 index 000000000..ca9d7d66b Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_voicemail_avatar.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/list_activated_holo.9.png b/java/com/android/contacts/common/res/drawable-xhdpi/list_activated_holo.9.png new file mode 100644 index 000000000..eda10e612 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/list_activated_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/list_background_holo.9.png b/java/com/android/contacts/common/res/drawable-xhdpi/list_background_holo.9.png new file mode 100644 index 000000000..b65272542 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/list_background_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/list_focused_holo.9.png b/java/com/android/contacts/common/res/drawable-xhdpi/list_focused_holo.9.png new file mode 100644 index 000000000..86578be45 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/list_focused_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/list_longpressed_holo_light.9.png b/java/com/android/contacts/common/res/drawable-xhdpi/list_longpressed_holo_light.9.png new file mode 100644 index 000000000..5532e88c2 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/list_longpressed_holo_light.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/list_pressed_holo_light.9.png b/java/com/android/contacts/common/res/drawable-xhdpi/list_pressed_holo_light.9.png new file mode 100644 index 000000000..f4af92657 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/list_pressed_holo_light.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/list_section_divider_holo_custom.9.png b/java/com/android/contacts/common/res/drawable-xhdpi/list_section_divider_holo_custom.9.png new file mode 100644 index 000000000..8fb0636cf Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/list_section_divider_holo_custom.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/list_title_holo.9.png b/java/com/android/contacts/common/res/drawable-xhdpi/list_title_holo.9.png new file mode 100644 index 000000000..f4f00ca0f Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/list_title_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_ab_search.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_ab_search.png new file mode 100644 index 000000000..142c5457d Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_ab_search.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_arrow_back_24dp.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_arrow_back_24dp.png new file mode 100644 index 000000000..72c51b0d5 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_arrow_back_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_business_white_120dp.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_business_white_120dp.png new file mode 100644 index 000000000..8d67e448f Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_business_white_120dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_call_24dp.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_call_24dp.png new file mode 100644 index 000000000..90ead2e45 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_call_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_call_note_white_24dp.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_call_note_white_24dp.png new file mode 100644 index 000000000..2656cad18 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_call_note_white_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_close_dk.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_close_dk.png new file mode 100644 index 000000000..670bf796c Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_close_dk.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_create_24dp.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_create_24dp.png new file mode 100644 index 000000000..24142c729 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_create_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_group_white_24dp.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_group_white_24dp.png new file mode 100644 index 000000000..03cad4c90 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_group_white_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_history_white_drawable_24dp.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_history_white_drawable_24dp.png new file mode 100644 index 000000000..f44df1afd Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_history_white_drawable_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_info_outline_24dp.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_info_outline_24dp.png new file mode 100644 index 000000000..c41a5fcff Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_info_outline_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_back.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_back.png new file mode 100644 index 000000000..436a82da6 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_back.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_group_dk.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_group_dk.png new file mode 100644 index 000000000..a70c60c03 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_group_dk.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_group_lt.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_group_lt.png new file mode 100644 index 000000000..c64b9defe Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_group_lt.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_overflow_lt.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_overflow_lt.png new file mode 100644 index 000000000..ff1759b8f Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_overflow_lt.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_person_dk.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_person_dk.png new file mode 100644 index 000000000..878e694ad Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_person_dk.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_person_lt.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_person_lt.png new file mode 100644 index 000000000..ed4138f15 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_person_lt.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_remove_field_holo_light.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_remove_field_holo_light.png new file mode 100644 index 000000000..0fec2f2b5 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_remove_field_holo_light.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_dk.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_dk.png new file mode 100644 index 000000000..fa682b11b Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_dk.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_holo_light.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_holo_light.png new file mode 100644 index 000000000..6c45bc8e6 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_holo_light.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_lt.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_lt.png new file mode 100644 index 000000000..955f7383b Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_lt.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_message_24dp.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_message_24dp.png new file mode 100644 index 000000000..0a79824b8 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_message_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_person_24dp.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_person_24dp.png new file mode 100644 index 000000000..184f7418d Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_person_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_person_add_24dp.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_person_add_24dp.png new file mode 100644 index 000000000..8f744f039 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_person_add_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_phone_attach.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_phone_attach.png new file mode 100644 index 000000000..6a6cdeeaa Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_phone_attach.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_rx_videocam.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_rx_videocam.png new file mode 100644 index 000000000..89d29b7f5 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_rx_videocam.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_scroll_handle.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_scroll_handle.png new file mode 100644 index 000000000..55f1d1369 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_scroll_handle.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_tx_videocam.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_tx_videocam.png new file mode 100644 index 000000000..8d897ba5a Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_tx_videocam.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_videocam.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_videocam.png new file mode 100644 index 000000000..4ab5ad0ee Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_videocam.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_voicemail_avatar.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_voicemail_avatar.png new file mode 100644 index 000000000..d0979e9eb Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_voicemail_avatar.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/list_activated_holo.9.png b/java/com/android/contacts/common/res/drawable-xxhdpi/list_activated_holo.9.png new file mode 100644 index 000000000..52c00ddcd Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/list_activated_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/list_focused_holo.9.png b/java/com/android/contacts/common/res/drawable-xxhdpi/list_focused_holo.9.png new file mode 100644 index 000000000..3e4ca684e Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/list_focused_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/list_longpressed_holo_light.9.png b/java/com/android/contacts/common/res/drawable-xxhdpi/list_longpressed_holo_light.9.png new file mode 100644 index 000000000..230d649bf Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/list_longpressed_holo_light.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/list_pressed_holo_light.9.png b/java/com/android/contacts/common/res/drawable-xxhdpi/list_pressed_holo_light.9.png new file mode 100644 index 000000000..1352a1702 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/list_pressed_holo_light.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/list_title_holo.9.png b/java/com/android/contacts/common/res/drawable-xxhdpi/list_title_holo.9.png new file mode 100644 index 000000000..7ddf14a0d Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/list_title_holo.9.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_ab_search.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_ab_search.png new file mode 100644 index 000000000..2ffb2ecae Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_ab_search.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_arrow_back_24dp.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_arrow_back_24dp.png new file mode 100644 index 000000000..ae01a04ae Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_arrow_back_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_business_white_120dp.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_business_white_120dp.png new file mode 100644 index 000000000..1741675de Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_business_white_120dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_call_24dp.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_call_24dp.png new file mode 100644 index 000000000..b0e020573 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_call_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_call_note_white_24dp.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_call_note_white_24dp.png new file mode 100644 index 000000000..903c1623d Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_call_note_white_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_close_dk.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_close_dk.png new file mode 100644 index 000000000..3a5540ff6 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_close_dk.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_create_24dp.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_create_24dp.png new file mode 100644 index 000000000..d3ff0ecb6 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_create_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_history_white_drawable_24dp.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_history_white_drawable_24dp.png new file mode 100644 index 000000000..5b96af5b7 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_history_white_drawable_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_info_outline_24dp.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_info_outline_24dp.png new file mode 100644 index 000000000..3a82cab3b Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_info_outline_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_message_24dp.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_message_24dp.png new file mode 100644 index 000000000..fa7c17ac4 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_message_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_person_24dp.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_person_24dp.png new file mode 100644 index 000000000..33d40d8b6 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_person_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_person_add_24dp.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_person_add_24dp.png new file mode 100644 index 000000000..2fa2cca80 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_person_add_24dp.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_phone_attach.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_phone_attach.png new file mode 100644 index 000000000..b072ad11f Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_phone_attach.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_scroll_handle.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_scroll_handle.png new file mode 100644 index 000000000..d90782a32 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_scroll_handle.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_videocam.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_videocam.png new file mode 100644 index 000000000..0643ea55f Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_videocam.png differ diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_voicemail_avatar.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_voicemail_avatar.png new file mode 100644 index 000000000..1d6e1aa0f Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_voicemail_avatar.png differ diff --git a/java/com/android/contacts/common/res/drawable/dialog_background_material.xml b/java/com/android/contacts/common/res/drawable/dialog_background_material.xml new file mode 100644 index 000000000..1b71cd63a --- /dev/null +++ b/java/com/android/contacts/common/res/drawable/dialog_background_material.xml @@ -0,0 +1,23 @@ + + + + + + + + + diff --git a/java/com/android/contacts/common/res/drawable/fastscroll_thumb.xml b/java/com/android/contacts/common/res/drawable/fastscroll_thumb.xml new file mode 100644 index 000000000..67645ff91 --- /dev/null +++ b/java/com/android/contacts/common/res/drawable/fastscroll_thumb.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/java/com/android/contacts/common/res/drawable/ic_back_arrow.xml b/java/com/android/contacts/common/res/drawable/ic_back_arrow.xml new file mode 100644 index 000000000..56fab8f6f --- /dev/null +++ b/java/com/android/contacts/common/res/drawable/ic_back_arrow.xml @@ -0,0 +1,20 @@ + + + \ No newline at end of file diff --git a/java/com/android/contacts/common/res/drawable/ic_call.xml b/java/com/android/contacts/common/res/drawable/ic_call.xml new file mode 100644 index 000000000..0fedd452f --- /dev/null +++ b/java/com/android/contacts/common/res/drawable/ic_call.xml @@ -0,0 +1,19 @@ + + + diff --git a/java/com/android/contacts/common/res/drawable/ic_message_24dp.xml b/java/com/android/contacts/common/res/drawable/ic_message_24dp.xml new file mode 100644 index 000000000..3c6c8b534 --- /dev/null +++ b/java/com/android/contacts/common/res/drawable/ic_message_24dp.xml @@ -0,0 +1,19 @@ + + + diff --git a/java/com/android/contacts/common/res/drawable/ic_more_vert.xml b/java/com/android/contacts/common/res/drawable/ic_more_vert.xml new file mode 100644 index 000000000..fcc3d9e4f --- /dev/null +++ b/java/com/android/contacts/common/res/drawable/ic_more_vert.xml @@ -0,0 +1,9 @@ + + + diff --git a/java/com/android/contacts/common/res/drawable/ic_person_add_tinted_24dp.xml b/java/com/android/contacts/common/res/drawable/ic_person_add_tinted_24dp.xml new file mode 100644 index 000000000..0af90edb3 --- /dev/null +++ b/java/com/android/contacts/common/res/drawable/ic_person_add_tinted_24dp.xml @@ -0,0 +1,20 @@ + + + diff --git a/java/com/android/contacts/common/res/drawable/ic_scroll_handle_default.xml b/java/com/android/contacts/common/res/drawable/ic_scroll_handle_default.xml new file mode 100644 index 000000000..ac932f87c --- /dev/null +++ b/java/com/android/contacts/common/res/drawable/ic_scroll_handle_default.xml @@ -0,0 +1,20 @@ + + + + diff --git a/java/com/android/contacts/common/res/drawable/ic_scroll_handle_pressed.xml b/java/com/android/contacts/common/res/drawable/ic_scroll_handle_pressed.xml new file mode 100644 index 000000000..4838de58a --- /dev/null +++ b/java/com/android/contacts/common/res/drawable/ic_scroll_handle_pressed.xml @@ -0,0 +1,20 @@ + + + + \ No newline at end of file diff --git a/java/com/android/contacts/common/res/drawable/ic_search_add_contact.xml b/java/com/android/contacts/common/res/drawable/ic_search_add_contact.xml new file mode 100644 index 000000000..801806044 --- /dev/null +++ b/java/com/android/contacts/common/res/drawable/ic_search_add_contact.xml @@ -0,0 +1,20 @@ + + + + diff --git a/java/com/android/contacts/common/res/drawable/ic_search_video_call.xml b/java/com/android/contacts/common/res/drawable/ic_search_video_call.xml new file mode 100644 index 000000000..f1b5cba43 --- /dev/null +++ b/java/com/android/contacts/common/res/drawable/ic_search_video_call.xml @@ -0,0 +1,21 @@ + + + + diff --git a/java/com/android/contacts/common/res/drawable/ic_tab_all.xml b/java/com/android/contacts/common/res/drawable/ic_tab_all.xml new file mode 100644 index 000000000..9cc6fbc96 --- /dev/null +++ b/java/com/android/contacts/common/res/drawable/ic_tab_all.xml @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/java/com/android/contacts/common/res/drawable/ic_tab_groups.xml b/java/com/android/contacts/common/res/drawable/ic_tab_groups.xml new file mode 100644 index 000000000..6b3e7a415 --- /dev/null +++ b/java/com/android/contacts/common/res/drawable/ic_tab_groups.xml @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/java/com/android/contacts/common/res/drawable/ic_tab_starred.xml b/java/com/android/contacts/common/res/drawable/ic_tab_starred.xml new file mode 100644 index 000000000..a12e0993e --- /dev/null +++ b/java/com/android/contacts/common/res/drawable/ic_tab_starred.xml @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/java/com/android/contacts/common/res/drawable/ic_work_profile.xml b/java/com/android/contacts/common/res/drawable/ic_work_profile.xml new file mode 100644 index 000000000..fc21100c0 --- /dev/null +++ b/java/com/android/contacts/common/res/drawable/ic_work_profile.xml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/java/com/android/contacts/common/res/drawable/item_background_material_borderless_dark.xml b/java/com/android/contacts/common/res/drawable/item_background_material_borderless_dark.xml new file mode 100644 index 000000000..94e309507 --- /dev/null +++ b/java/com/android/contacts/common/res/drawable/item_background_material_borderless_dark.xml @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/java/com/android/contacts/common/res/drawable/item_background_material_dark.xml b/java/com/android/contacts/common/res/drawable/item_background_material_dark.xml new file mode 100644 index 000000000..91ab763a5 --- /dev/null +++ b/java/com/android/contacts/common/res/drawable/item_background_material_dark.xml @@ -0,0 +1,23 @@ + + + + + + + + + \ No newline at end of file diff --git a/java/com/android/contacts/common/res/drawable/item_background_material_light.xml b/java/com/android/contacts/common/res/drawable/item_background_material_light.xml new file mode 100644 index 000000000..d41accb02 --- /dev/null +++ b/java/com/android/contacts/common/res/drawable/item_background_material_light.xml @@ -0,0 +1,23 @@ + + + + + + + + + \ No newline at end of file diff --git a/java/com/android/contacts/common/res/drawable/list_item_activated_background.xml b/java/com/android/contacts/common/res/drawable/list_item_activated_background.xml new file mode 100644 index 000000000..5b774fd20 --- /dev/null +++ b/java/com/android/contacts/common/res/drawable/list_item_activated_background.xml @@ -0,0 +1,20 @@ + + + + + + + diff --git a/java/com/android/contacts/common/res/drawable/list_selector_background_transition_holo_light.xml b/java/com/android/contacts/common/res/drawable/list_selector_background_transition_holo_light.xml new file mode 100644 index 000000000..35fff99c2 --- /dev/null +++ b/java/com/android/contacts/common/res/drawable/list_selector_background_transition_holo_light.xml @@ -0,0 +1,20 @@ + + + + + + + diff --git a/java/com/android/contacts/common/res/drawable/searchedittext_custom_cursor.xml b/java/com/android/contacts/common/res/drawable/searchedittext_custom_cursor.xml new file mode 100644 index 000000000..27614a1ac --- /dev/null +++ b/java/com/android/contacts/common/res/drawable/searchedittext_custom_cursor.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/java/com/android/contacts/common/res/drawable/unread_count_background.xml b/java/com/android/contacts/common/res/drawable/unread_count_background.xml new file mode 100644 index 000000000..4fc6b9b60 --- /dev/null +++ b/java/com/android/contacts/common/res/drawable/unread_count_background.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/java/com/android/contacts/common/res/drawable/view_pager_tab_background.xml b/java/com/android/contacts/common/res/drawable/view_pager_tab_background.xml new file mode 100644 index 000000000..bef30a434 --- /dev/null +++ b/java/com/android/contacts/common/res/drawable/view_pager_tab_background.xml @@ -0,0 +1,22 @@ + + + + + + + \ No newline at end of file diff --git a/java/com/android/contacts/common/res/layout-ldrtl/unread_count_tab.xml b/java/com/android/contacts/common/res/layout-ldrtl/unread_count_tab.xml new file mode 100644 index 000000000..2aa97722d --- /dev/null +++ b/java/com/android/contacts/common/res/layout-ldrtl/unread_count_tab.xml @@ -0,0 +1,48 @@ + + + + + + + + + diff --git a/java/com/android/contacts/common/res/layout/account_filter_header.xml b/java/com/android/contacts/common/res/layout/account_filter_header.xml new file mode 100644 index 000000000..a12ab08fd --- /dev/null +++ b/java/com/android/contacts/common/res/layout/account_filter_header.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + diff --git a/java/com/android/contacts/common/res/layout/account_selector_list_item.xml b/java/com/android/contacts/common/res/layout/account_selector_list_item.xml new file mode 100644 index 000000000..587626e8d --- /dev/null +++ b/java/com/android/contacts/common/res/layout/account_selector_list_item.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + diff --git a/java/com/android/contacts/common/res/layout/account_selector_list_item_condensed.xml b/java/com/android/contacts/common/res/layout/account_selector_list_item_condensed.xml new file mode 100644 index 000000000..33821166e --- /dev/null +++ b/java/com/android/contacts/common/res/layout/account_selector_list_item_condensed.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + diff --git a/java/com/android/contacts/common/res/layout/call_subject_history.xml b/java/com/android/contacts/common/res/layout/call_subject_history.xml new file mode 100644 index 000000000..733f1d8b6 --- /dev/null +++ b/java/com/android/contacts/common/res/layout/call_subject_history.xml @@ -0,0 +1,33 @@ + + + + + + + + \ No newline at end of file diff --git a/java/com/android/contacts/common/res/layout/call_subject_history_list_item.xml b/java/com/android/contacts/common/res/layout/call_subject_history_list_item.xml new file mode 100644 index 000000000..c378f24b2 --- /dev/null +++ b/java/com/android/contacts/common/res/layout/call_subject_history_list_item.xml @@ -0,0 +1,29 @@ + + + + diff --git a/java/com/android/contacts/common/res/layout/contact_detail_list_padding.xml b/java/com/android/contacts/common/res/layout/contact_detail_list_padding.xml new file mode 100644 index 000000000..02a5c809c --- /dev/null +++ b/java/com/android/contacts/common/res/layout/contact_detail_list_padding.xml @@ -0,0 +1,27 @@ + + + + + + + diff --git a/java/com/android/contacts/common/res/layout/contact_list_card.xml b/java/com/android/contacts/common/res/layout/contact_list_card.xml new file mode 100644 index 000000000..a04f4cad9 --- /dev/null +++ b/java/com/android/contacts/common/res/layout/contact_list_card.xml @@ -0,0 +1,39 @@ + + + + + + + diff --git a/java/com/android/contacts/common/res/layout/contact_list_content.xml b/java/com/android/contacts/common/res/layout/contact_list_content.xml new file mode 100644 index 000000000..3ee27a0ad --- /dev/null +++ b/java/com/android/contacts/common/res/layout/contact_list_content.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + diff --git a/java/com/android/contacts/common/res/layout/default_account_checkbox.xml b/java/com/android/contacts/common/res/layout/default_account_checkbox.xml new file mode 100644 index 000000000..b7c0cf644 --- /dev/null +++ b/java/com/android/contacts/common/res/layout/default_account_checkbox.xml @@ -0,0 +1,36 @@ + + + + + + diff --git a/java/com/android/contacts/common/res/layout/dialog_call_subject.xml b/java/com/android/contacts/common/res/layout/dialog_call_subject.xml new file mode 100644 index 000000000..709bb50cb --- /dev/null +++ b/java/com/android/contacts/common/res/layout/dialog_call_subject.xml @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com/android/contacts/common/res/layout/directory_header.xml b/java/com/android/contacts/common/res/layout/directory_header.xml new file mode 100644 index 000000000..b8f5163c0 --- /dev/null +++ b/java/com/android/contacts/common/res/layout/directory_header.xml @@ -0,0 +1,55 @@ + + + + + + + + + diff --git a/java/com/android/contacts/common/res/layout/list_separator.xml b/java/com/android/contacts/common/res/layout/list_separator.xml new file mode 100644 index 000000000..ab60605c5 --- /dev/null +++ b/java/com/android/contacts/common/res/layout/list_separator.xml @@ -0,0 +1,27 @@ + + + diff --git a/java/com/android/contacts/common/res/layout/search_bar_expanded.xml b/java/com/android/contacts/common/res/layout/search_bar_expanded.xml new file mode 100644 index 000000000..8a3bd6088 --- /dev/null +++ b/java/com/android/contacts/common/res/layout/search_bar_expanded.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + diff --git a/java/com/android/contacts/common/res/layout/select_account_list_item.xml b/java/com/android/contacts/common/res/layout/select_account_list_item.xml new file mode 100644 index 000000000..fbd31e573 --- /dev/null +++ b/java/com/android/contacts/common/res/layout/select_account_list_item.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + diff --git a/java/com/android/contacts/common/res/layout/unread_count_tab.xml b/java/com/android/contacts/common/res/layout/unread_count_tab.xml new file mode 100644 index 000000000..83481ee2d --- /dev/null +++ b/java/com/android/contacts/common/res/layout/unread_count_tab.xml @@ -0,0 +1,43 @@ + + + + + + + diff --git a/java/com/android/contacts/common/res/mipmap-hdpi/ic_contacts_launcher.png b/java/com/android/contacts/common/res/mipmap-hdpi/ic_contacts_launcher.png new file mode 100644 index 000000000..64eff002f Binary files /dev/null and b/java/com/android/contacts/common/res/mipmap-hdpi/ic_contacts_launcher.png differ diff --git a/java/com/android/contacts/common/res/mipmap-mdpi/ic_contacts_launcher.png b/java/com/android/contacts/common/res/mipmap-mdpi/ic_contacts_launcher.png new file mode 100644 index 000000000..b4ee8215a Binary files /dev/null and b/java/com/android/contacts/common/res/mipmap-mdpi/ic_contacts_launcher.png differ diff --git a/java/com/android/contacts/common/res/mipmap-xhdpi/ic_contacts_launcher.png b/java/com/android/contacts/common/res/mipmap-xhdpi/ic_contacts_launcher.png new file mode 100644 index 000000000..6feeadfbe Binary files /dev/null and b/java/com/android/contacts/common/res/mipmap-xhdpi/ic_contacts_launcher.png differ diff --git a/java/com/android/contacts/common/res/mipmap-xxhdpi/ic_contacts_launcher.png b/java/com/android/contacts/common/res/mipmap-xxhdpi/ic_contacts_launcher.png new file mode 100644 index 000000000..01a3fde9d Binary files /dev/null and b/java/com/android/contacts/common/res/mipmap-xxhdpi/ic_contacts_launcher.png differ diff --git a/java/com/android/contacts/common/res/mipmap-xxxhdpi/ic_contacts_launcher.png b/java/com/android/contacts/common/res/mipmap-xxxhdpi/ic_contacts_launcher.png new file mode 100644 index 000000000..328e067ee Binary files /dev/null and b/java/com/android/contacts/common/res/mipmap-xxxhdpi/ic_contacts_launcher.png differ diff --git a/java/com/android/contacts/common/res/values-ja/donottranslate_config.xml b/java/com/android/contacts/common/res/values-ja/donottranslate_config.xml new file mode 100644 index 000000000..e05c6d658 --- /dev/null +++ b/java/com/android/contacts/common/res/values-ja/donottranslate_config.xml @@ -0,0 +1,20 @@ + + + + false + + + true + + + false + + + true + + + false + + + true + \ No newline at end of file diff --git a/java/com/android/contacts/common/res/values-ko/donottranslate_config.xml b/java/com/android/contacts/common/res/values-ko/donottranslate_config.xml new file mode 100644 index 000000000..8def55498 --- /dev/null +++ b/java/com/android/contacts/common/res/values-ko/donottranslate_config.xml @@ -0,0 +1,17 @@ + + + + false + + + false + + + false + + + false + + + false + \ No newline at end of file diff --git a/java/com/android/contacts/common/res/values-land/integers.xml b/java/com/android/contacts/common/res/values-land/integers.xml new file mode 100644 index 000000000..26bac6222 --- /dev/null +++ b/java/com/android/contacts/common/res/values-land/integers.xml @@ -0,0 +1,22 @@ + + + + 3 + + + 60 + diff --git a/java/com/android/contacts/common/res/values-sw600dp-land/integers.xml b/java/com/android/contacts/common/res/values-sw600dp-land/integers.xml new file mode 100644 index 000000000..be4eb0bb0 --- /dev/null +++ b/java/com/android/contacts/common/res/values-sw600dp-land/integers.xml @@ -0,0 +1,22 @@ + + + + 3 + + + 20 + diff --git a/java/com/android/contacts/common/res/values-sw600dp/dimens.xml b/java/com/android/contacts/common/res/values-sw600dp/dimens.xml new file mode 100644 index 000000000..cf67a1e72 --- /dev/null +++ b/java/com/android/contacts/common/res/values-sw600dp/dimens.xml @@ -0,0 +1,29 @@ + + + + 0dip + + @dimen/list_visible_scrollbar_padding + + 24dip + 16dip + + + 32dp + + 32dp + diff --git a/java/com/android/contacts/common/res/values-sw600dp/integers.xml b/java/com/android/contacts/common/res/values-sw600dp/integers.xml new file mode 100644 index 000000000..31aeee995 --- /dev/null +++ b/java/com/android/contacts/common/res/values-sw600dp/integers.xml @@ -0,0 +1,24 @@ + + + + 3 + + + + 15 + diff --git a/java/com/android/contacts/common/res/values-sw720dp-land/integers.xml b/java/com/android/contacts/common/res/values-sw720dp-land/integers.xml new file mode 100644 index 000000000..577716d24 --- /dev/null +++ b/java/com/android/contacts/common/res/values-sw720dp-land/integers.xml @@ -0,0 +1,22 @@ + + + + 4 + + + 30 + diff --git a/java/com/android/contacts/common/res/values-sw720dp/integers.xml b/java/com/android/contacts/common/res/values-sw720dp/integers.xml new file mode 100644 index 000000000..05e309351 --- /dev/null +++ b/java/com/android/contacts/common/res/values-sw720dp/integers.xml @@ -0,0 +1,22 @@ + + + + 2 + + + 20 + diff --git a/java/com/android/contacts/common/res/values-zh-rCN/donottranslate_config.xml b/java/com/android/contacts/common/res/values-zh-rCN/donottranslate_config.xml new file mode 100644 index 000000000..08023992b --- /dev/null +++ b/java/com/android/contacts/common/res/values-zh-rCN/donottranslate_config.xml @@ -0,0 +1,17 @@ + + + + false + + + true + + + false + + + true + + + false + \ No newline at end of file diff --git a/java/com/android/contacts/common/res/values-zh-rTW/donottranslate_config.xml b/java/com/android/contacts/common/res/values-zh-rTW/donottranslate_config.xml new file mode 100644 index 000000000..08023992b --- /dev/null +++ b/java/com/android/contacts/common/res/values-zh-rTW/donottranslate_config.xml @@ -0,0 +1,17 @@ + + + + false + + + true + + + false + + + true + + + false + \ No newline at end of file diff --git a/java/com/android/contacts/common/res/values/animation_constants.xml b/java/com/android/contacts/common/res/values/animation_constants.xml new file mode 100644 index 000000000..9eec7d6c8 --- /dev/null +++ b/java/com/android/contacts/common/res/values/animation_constants.xml @@ -0,0 +1,19 @@ + + + + 250 + diff --git a/java/com/android/contacts/common/res/values/attrs.xml b/java/com/android/contacts/common/res/values/attrs.xml new file mode 100644 index 000000000..44d04f025 --- /dev/null +++ b/java/com/android/contacts/common/res/values/attrs.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com/android/contacts/common/res/values/colors.xml b/java/com/android/contacts/common/res/values/colors.xml new file mode 100644 index 000000000..7524eff58 --- /dev/null +++ b/java/com/android/contacts/common/res/values/colors.xml @@ -0,0 +1,158 @@ + + + + + #eeeeee + + #44ff0000 + + + #a0ffffff + + + #30000000 + + + #363636 + + @color/dialer_secondary_text_color + + + #2A56C6 + + + #AAAAAA + + + #D0D0D0 + + + #363636 + + + #DDDDDD + + + #7F000000 + + + #CCCCCC + + #7f000000 + + #fff + #000 + + + + #DB4437 + #E91E63 + #9C27B0 + #673AB7 + #3F51B5 + #4285F4 + #039BE5 + #0097A7 + #009688 + #0F9D58 + #689F38 + #EF6C00 + #FF5722 + #757575 + + + + + #C53929 + #C2185B + #7B1FA2 + #512DA8 + #303F9F + #3367D6 + #0277BD + #006064 + #00796B + #0B8043 + #33691E + #E65100 + #E64A19 + #424242 + + + + #607D8B + + #455A64 + + + #cccccc + + #ffffff + + @color/dialer_theme_color + + #ffffff + + #008aa1 + + #ffffff + @color/tab_ripple_color + #f50057 + #1C3AA9 + + + @color/contactscommon_actionbar_background_color + + + + #ffffff + #a6ffffff + + + #000000 + + #ffffff + + #737373 + @color/searchbox_hint_text_color + + @color/dialtacts_theme_color + + + #f9f9f9 + #FFFFFF + + + #d1041c + + + #000000 + + + #d8d8d8 + + + #00c853 + + + #ffffff + @color/searchbox_hint_text_color + diff --git a/java/com/android/contacts/common/res/values/dimens.xml b/java/com/android/contacts/common/res/values/dimens.xml new file mode 100644 index 000000000..642eb31a4 --- /dev/null +++ b/java/com/android/contacts/common/res/values/dimens.xml @@ -0,0 +1,161 @@ + + + + + + 0dip + + + 32dip + + 18dp + 8dp + + + 23dip + + 16dip + + + 16dip + + + + 48sp + + + 0dip + + + 32dip + + 16dip + @dimen/list_visible_scrollbar_padding + + 8dip + + 56dp + + + 0dip + + + 12dp + + + 1dp + + + 32dip + + + 48dip + + + 16sp + 40dp + 15dp + 12dp + + + 20sp + 10dip + + + 40dp + 20dp + 1dp + 67% + + + 56dp + + 56dp + + 8dp + + 88dp + + 16dp + + 16dp + + + 2dp + + 14sp + 2dp + 16dp + 2dp + 0dp + 2dp + 12sp + 2dp + + + 4dp + + 48dp + + 56dp + + 16dp + + 14dp + + 15dp + + 20sp + + + 16dp + + 57dp + + 24sp + + + 40dp + + 2dp + + + 20dp + + 8dp + + 50dp + + 60dp + + 16sp + + 14sp + + 15dp + diff --git a/java/com/android/contacts/common/res/values/donottranslate_config.xml b/java/com/android/contacts/common/res/values/donottranslate_config.xml new file mode 100644 index 000000000..324437928 --- /dev/null +++ b/java/com/android/contacts/common/res/values/donottranslate_config.xml @@ -0,0 +1,95 @@ + + + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + ContactEditorUtils_default_account + + + ContactEditorUtils_anything_saved + + + default + + + default + + + + + + + + + vcf + + + contacts.vcf + + + 1 + + + 99999 + + + + + + true + + + true + + + true + + pref_build_version + pref_open_source_licenses + pref_privacy_policy + pref_terms_of_service + + + diff --git a/java/com/android/contacts/common/res/values/ids.xml b/java/com/android/contacts/common/res/values/ids.xml new file mode 100644 index 000000000..871f5a636 --- /dev/null +++ b/java/com/android/contacts/common/res/values/ids.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + diff --git a/java/com/android/contacts/common/res/values/integers.xml b/java/com/android/contacts/common/res/values/integers.xml new file mode 100644 index 000000000..d38ad1da0 --- /dev/null +++ b/java/com/android/contacts/common/res/values/integers.xml @@ -0,0 +1,39 @@ + + + + + + + 2 + 3 + + + 30 + + + 0 + + 0 + + + 250 + + + 100 + diff --git a/java/com/android/contacts/common/res/values/strings.xml b/java/com/android/contacts/common/res/values/strings.xml new file mode 100644 index 000000000..15e1f15d9 --- /dev/null +++ b/java/com/android/contacts/common/res/values/strings.xml @@ -0,0 +1,798 @@ + + + + + + Text copied + + Copy to clipboard + + + Call + %s + + + Call home + + Call mobile + + Call work + + Call work fax + + Call home fax + + Call pager + + Call + + Call callback + + Call car + + Call company main + + Call ISDN + + Call main + + Call fax + + Call radio + + Call telex + + Call TTY/TDD + + Call work mobile + + Call work pager + + Call + %s + + + Call MMS + + %s (Call) + + + Text + %s + + + Text home + + Text mobile + + Text work + + Text work fax + + Text home fax + + Text pager + + Text + + Text callback + + Text car + + Text company main + + Text ISDN + + Text main + + Text fax + + Text radio + + Text telex + + Text TTY/TDD + + Text work mobile + + Text work pager + + Text + %s + + + Text MMS + + %s (Message) + + + Clear frequently contacted? + + + You\'ll clear the frequently contacted list in the + Contacts and Phone apps, and force email apps to learn your addressing preferences from + scratch. + + + + Clearing frequently contacted\u2026 + + + Available + + + Away + + + Busy + + + Contacts + + + Other + + + Directory + + + Work directory + + + All contacts + + + Me + + + Searching\u2026 + + + More than %d found. + + + No contacts + + + + 1 found + %d found + + + + Quick contact for %1$s + + + (No name) + + + Frequently called + + + Frequently contacted + + + View contact + + + All contacts with phone numbers + + + Work profile contacts + + + View updates + + + Device-only, unsynced + + + Name + + + Nickname + + + Name + + First name + + Last name + + Name prefix + + Middle name + + Name suffix + + + Phonetic name + + + Phonetic first name + + Phonetic middle name + + Phonetic last name + + + Phone + + + Email + + + Address + + + IM + + + Organization + + + Relationship + + + Special date + + + Text message + + + Address + + + Company + + + Title + + + Notes + + + SIP + + + Website + + + Groups + + + Email home + + Email mobile + + Email work + + Email + + Email %s + + + Email + + + Street + + PO box + + Neighborhood + + City + + State + + ZIP code + + Country + + + View home address + + View work address + + View address + + View %s address + + + Chat using AIM + + Chat using Windows Live + + Chat using Yahoo + + Chat using Skype + + Chat using QQ + + Chat using Google Talk + + Chat using ICQ + + Chat using Jabber + + + Chat + + + delete + + + Expand or collapse name fields + + + Expand or collapse phonetic + name fields + + + All contacts + + + Done + + + Cancel + + + Contacts in %s + + + Contacts in custom view + + + Single contact + + + Save imported contacts to: + + + Import from SIM card + + + Import from SIM ^1 - ^2 + + + Import from SIM %1$s + + + Import from .vcf file + + + Cancel import of %s? + + + Cancel export of %s? + + + Couldn\'t cancel vCard import/export + + + Unknown error. + + + Couldn\'t open \"%s\": %s. + + + Couldn\'t start the exporter: \"%s\". + + + There is no exportable contact. + + + You have disabled a required permission. + + + An error occurred during export: \"%s\". + + + Required filename is too long (\"%s\"). + + + I/O error + + + Not enough memory. The file may be too large. + + + Couldn\'t parse vCard for an unexpected reason. + + + The format isn\'t supported. + + + Couldn\'t collect meta information of given vCard file(s). + + + One or more files couldn\'t be imported (%s). + + + Finished exporting %s. + + + Finished exporting contacts. + + + Finished exporting contacts, click the notification to share contacts. + + + Tap to share contacts. + + + Exporting %s canceled. + + + Exporting contact data + + + Contact data is being exported. + + + Couldn\'t get database information. + + + There are no exportable contacts. If you do have contacts on your device, some data providers may not allow the contacts to be exported from the device. + + + The vCard composer didn\'t start properly. + + + Couldn\'t export + + + The contact data wasn\'t exported.\nReason: \"%s\" + + + Importing %s + + + Couldn\'t read vCard data + + + Reading vCard data canceled + + + Finished importing vCard %s + + + Importing %s canceled + + + %s will be imported shortly. + + The file will be imported shortly. + + vCard import request was rejected. Try again later. + + %s will be exported shortly. + + + The file will be exported shortly. + + + Contacts will be exported shortly. + + + vCard export request was rejected. Try again later. + + contact + + + Caching vCard(s) to local temporary storage. The actual import will start soon. + + + Couldn\'t import vCard. + + + Contact received over NFC + + + Export contacts? + + + Caching + + + Importing %s/%s: %s + + + Export to .vcf file + + + + + Sort by + + + First name + + + Last name + + + Name format + + + First name first + + + Last name first + + + Default account for new contacts + + + Sync contact metadata [DOGFOOD] + + + Sync contact metadata + + + About Contacts + + + Settings + + + Share visible contacts + + + Failed to share visible contacts. + + + Share favorite contacts + + + Share all contacts + + + Failed to share contacts. + + + Import/export contacts + + + Import contacts + + + This contact can\'t be shared. + + + There are no contacts to share. + + + Search + + + Find contacts + + + Favorites + + + No contacts. + + + No visible contacts. + + + No favorites + + + No contacts in %s + + + Clear frequents + + + Select SIM card + + + Manage accounts + + + Import/export + + + sans-serif + + + via %1$s + + + %1$s via %2$s + + + sans-serif-medium + + + stop searching + + + Clear search + + + sans-serif + + + Contact display options + + + Account + + + Always use this for calls + + + Call with + + + Call with a note + + + Type a note to send with call ... + + + SEND & CALL + + + %1$s / %2$s + + + %1$s%2$s + + + %1$s tab. + + + + + %1$s tab. %2$d unread item. + + + %1$s tab. %2$d unread items. + + + + + Build version + + + Open source licenses + + + License details for open source software + + + Privacy policy + + + Terms of service + + + Open source licenses + + + Failed to open the url. + + + Place video call + diff --git a/java/com/android/contacts/common/res/values/styles.xml b/java/com/android/contacts/common/res/values/styles.xml new file mode 100644 index 000000000..07d4a0225 --- /dev/null +++ b/java/com/android/contacts/common/res/values/styles.xml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com/android/contacts/common/testing/InjectedServices.java b/java/com/android/contacts/common/testing/InjectedServices.java new file mode 100644 index 000000000..5ab5e5feb --- /dev/null +++ b/java/com/android/contacts/common/testing/InjectedServices.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.testing; + +import android.content.ContentResolver; +import android.content.SharedPreferences; +import android.util.ArrayMap; +import java.util.Map; + +/** + * A mechanism for providing alternative (mock) services to the application while running tests. + * Activities, Services and the Application should check with this class to see if a particular + * service has been overridden. + */ +public class InjectedServices { + + private ContentResolver mContentResolver; + private SharedPreferences mSharedPreferences; + private Map mSystemServices; + + public ContentResolver getContentResolver() { + return mContentResolver; + } + + public void setContentResolver(ContentResolver contentResolver) { + this.mContentResolver = contentResolver; + } + + public SharedPreferences getSharedPreferences() { + return mSharedPreferences; + } + + public void setSharedPreferences(SharedPreferences sharedPreferences) { + this.mSharedPreferences = sharedPreferences; + } + + public void setSystemService(String name, Object service) { + if (mSystemServices == null) { + mSystemServices = new ArrayMap<>(); + } + + mSystemServices.put(name, service); + } + + public Object getSystemService(String name) { + if (mSystemServices != null) { + return mSystemServices.get(name); + } + return null; + } +} diff --git a/java/com/android/contacts/common/util/AccountFilterUtil.java b/java/com/android/contacts/common/util/AccountFilterUtil.java new file mode 100644 index 000000000..18743c65e --- /dev/null +++ b/java/com/android/contacts/common/util/AccountFilterUtil.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.util; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.util.Log; +import android.view.View; +import android.widget.TextView; +import com.android.contacts.common.R; +import com.android.contacts.common.list.ContactListFilter; +import com.android.contacts.common.list.ContactListFilterController; + +/** Utility class for account filter manipulation. */ +public class AccountFilterUtil { + + public static final String EXTRA_CONTACT_LIST_FILTER = "contactListFilter"; + private static final String TAG = AccountFilterUtil.class.getSimpleName(); + + /** + * Find TextView with the id "account_filter_header" and set correct text for the account filter + * header. + * + * @param filterContainer View containing TextView with id "account_filter_header" + * @return true when header text is set in the call. You may use this for conditionally showing or + * hiding this entire view. + */ + public static boolean updateAccountFilterTitleForPeople( + View filterContainer, ContactListFilter filter, boolean showTitleForAllAccounts) { + return updateAccountFilterTitle(filterContainer, filter, showTitleForAllAccounts, false); + } + + /** + * Similar to {@link #updateAccountFilterTitleForPeople(View, ContactListFilter, boolean, + * boolean)}, but for Phone UI. + */ + public static boolean updateAccountFilterTitleForPhone( + View filterContainer, ContactListFilter filter, boolean showTitleForAllAccounts) { + return updateAccountFilterTitle(filterContainer, filter, showTitleForAllAccounts, true); + } + + private static boolean updateAccountFilterTitle( + View filterContainer, + ContactListFilter filter, + boolean showTitleForAllAccounts, + boolean forPhone) { + final Context context = filterContainer.getContext(); + final TextView headerTextView = + (TextView) filterContainer.findViewById(R.id.account_filter_header); + + boolean textWasSet = false; + if (filter != null) { + if (forPhone) { + if (filter.filterType == ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS) { + if (showTitleForAllAccounts) { + headerTextView.setText(R.string.list_filter_phones); + textWasSet = true; + } + } else if (filter.filterType == ContactListFilter.FILTER_TYPE_ACCOUNT) { + headerTextView.setText( + context.getString(R.string.listAllContactsInAccount, filter.accountName)); + textWasSet = true; + } else if (filter.filterType == ContactListFilter.FILTER_TYPE_CUSTOM) { + headerTextView.setText(R.string.listCustomView); + textWasSet = true; + } else { + Log.w(TAG, "Filter type \"" + filter.filterType + "\" isn't expected."); + } + } else { + if (filter.filterType == ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS) { + if (showTitleForAllAccounts) { + headerTextView.setText(R.string.list_filter_all_accounts); + textWasSet = true; + } + } else if (filter.filterType == ContactListFilter.FILTER_TYPE_ACCOUNT) { + headerTextView.setText( + context.getString(R.string.listAllContactsInAccount, filter.accountName)); + textWasSet = true; + } else if (filter.filterType == ContactListFilter.FILTER_TYPE_CUSTOM) { + headerTextView.setText(R.string.listCustomView); + textWasSet = true; + } else if (filter.filterType == ContactListFilter.FILTER_TYPE_SINGLE_CONTACT) { + headerTextView.setText(R.string.listSingleContact); + textWasSet = true; + } else { + Log.w(TAG, "Filter type \"" + filter.filterType + "\" isn't expected."); + } + } + } else { + Log.w(TAG, "Filter is null."); + } + return textWasSet; + } + + /** This will update filter via a given ContactListFilterController. */ + public static void handleAccountFilterResult( + ContactListFilterController filterController, int resultCode, Intent data) { + if (resultCode == Activity.RESULT_OK) { + final ContactListFilter filter = data.getParcelableExtra(EXTRA_CONTACT_LIST_FILTER); + if (filter == null) { + return; + } + if (filter.filterType == ContactListFilter.FILTER_TYPE_CUSTOM) { + filterController.selectCustomFilter(); + } else { + filterController.setContactListFilter(filter, true); + } + } + } +} diff --git a/java/com/android/contacts/common/util/BitmapUtil.java b/java/com/android/contacts/common/util/BitmapUtil.java new file mode 100644 index 000000000..20f916a3f --- /dev/null +++ b/java/com/android/contacts/common/util/BitmapUtil.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.util; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PorterDuff.Mode; +import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; + +/** Provides static functions to decode bitmaps at the optimal size */ +public class BitmapUtil { + + private BitmapUtil() {} + + /** + * Returns Width or Height of the picture, depending on which size is smaller. Doesn't actually + * decode the picture, so it is pretty efficient to run. + */ + public static int getSmallerExtentFromBytes(byte[] bytes) { + final BitmapFactory.Options options = new BitmapFactory.Options(); + + // don't actually decode the picture, just return its bounds + options.inJustDecodeBounds = true; + BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options); + + // test what the best sample size is + return Math.min(options.outWidth, options.outHeight); + } + + /** + * Finds the optimal sampleSize for loading the picture + * + * @param originalSmallerExtent Width or height of the picture, whichever is smaller + * @param targetExtent Width or height of the target view, whichever is bigger. + *

If either one of the parameters is 0 or smaller, no sampling is applied + */ + public static int findOptimalSampleSize(int originalSmallerExtent, int targetExtent) { + // If we don't know sizes, we can't do sampling. + if (targetExtent < 1) { + return 1; + } + if (originalSmallerExtent < 1) { + return 1; + } + + // Test what the best sample size is. To do that, we find the sample size that gives us + // the best trade-off between resulting image size and memory requirement. We allow + // the down-sampled image to be 20% smaller than the target size. That way we can get around + // unfortunate cases where e.g. a 720 picture is requested for 362 and not down-sampled at + // all. Why 20%? Why not. Prove me wrong. + int extent = originalSmallerExtent; + int sampleSize = 1; + while ((extent >> 1) >= targetExtent * 0.8f) { + sampleSize <<= 1; + extent >>= 1; + } + + return sampleSize; + } + + /** Decodes the bitmap with the given sample size */ + public static Bitmap decodeBitmapFromBytes(byte[] bytes, int sampleSize) { + final BitmapFactory.Options options; + if (sampleSize <= 1) { + options = null; + } else { + options = new BitmapFactory.Options(); + options.inSampleSize = sampleSize; + } + return BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options); + } + + /** + * Retrieves a copy of the specified drawable resource, rotated by a specified angle. + * + * @param resources The current resources. + * @param resourceId The resource ID of the drawable to rotate. + * @param angle The angle of rotation. + * @return Rotated drawable. + */ + public static Drawable getRotatedDrawable( + android.content.res.Resources resources, int resourceId, float angle) { + + // Get the original drawable and make a copy which will be rotated. + Bitmap original = BitmapFactory.decodeResource(resources, resourceId); + Bitmap rotated = + Bitmap.createBitmap(original.getWidth(), original.getHeight(), Bitmap.Config.ARGB_8888); + + // Perform the rotation. + Canvas tempCanvas = new Canvas(rotated); + tempCanvas.rotate(angle, original.getWidth() / 2, original.getHeight() / 2); + tempCanvas.drawBitmap(original, 0, 0, null); + + return new BitmapDrawable(resources, rotated); + } + + /** + * Given an input bitmap, scales it to the given width/height and makes it round. + * + * @param input {@link Bitmap} to scale and crop + * @param targetWidth desired output width + * @param targetHeight desired output height + * @return output bitmap scaled to the target width/height and cropped to an oval. The cropping + * algorithm will try to fit as much of the input into the output as possible, while + * preserving the target width/height ratio. + */ + public static Bitmap getRoundedBitmap(Bitmap input, int targetWidth, int targetHeight) { + if (input == null) { + return null; + } + final Bitmap.Config inputConfig = input.getConfig(); + final Bitmap result = + Bitmap.createBitmap( + targetWidth, targetHeight, inputConfig != null ? inputConfig : Bitmap.Config.ARGB_8888); + final Canvas canvas = new Canvas(result); + final Paint paint = new Paint(); + canvas.drawARGB(0, 0, 0, 0); + paint.setAntiAlias(true); + final RectF dst = new RectF(0, 0, targetWidth, targetHeight); + canvas.drawOval(dst, paint); + + // Specifies that only pixels present in the destination (i.e. the drawn oval) should + // be overwritten with pixels from the input bitmap. + paint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN)); + + final int inputWidth = input.getWidth(); + final int inputHeight = input.getHeight(); + + // Choose the largest scale factor that will fit inside the dimensions of the + // input bitmap. + final float scaleBy = + Math.min((float) inputWidth / targetWidth, (float) inputHeight / targetHeight); + + final int xCropAmountHalved = (int) (scaleBy * targetWidth / 2); + final int yCropAmountHalved = (int) (scaleBy * targetHeight / 2); + + final Rect src = + new Rect( + inputWidth / 2 - xCropAmountHalved, + inputHeight / 2 - yCropAmountHalved, + inputWidth / 2 + xCropAmountHalved, + inputHeight / 2 + yCropAmountHalved); + + canvas.drawBitmap(input, src, dst, paint); + return result; + } +} diff --git a/java/com/android/contacts/common/util/CommonDateUtils.java b/java/com/android/contacts/common/util/CommonDateUtils.java new file mode 100644 index 000000000..312e691f8 --- /dev/null +++ b/java/com/android/contacts/common/util/CommonDateUtils.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.contacts.common.util; + +import java.text.SimpleDateFormat; +import java.util.Locale; + +/** Common date utilities. */ +public class CommonDateUtils { + + // All the SimpleDateFormats in this class use the UTC timezone + public static final SimpleDateFormat NO_YEAR_DATE_FORMAT = + new SimpleDateFormat("--MM-dd", Locale.US); + public static final SimpleDateFormat FULL_DATE_FORMAT = + new SimpleDateFormat("yyyy-MM-dd", Locale.US); + public static final SimpleDateFormat DATE_AND_TIME_FORMAT = + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US); + public static final SimpleDateFormat NO_YEAR_DATE_AND_TIME_FORMAT = + new SimpleDateFormat("--MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US); + + /** Exchange requires 8:00 for birthdays */ + public static final int DEFAULT_HOUR = 8; +} diff --git a/java/com/android/contacts/common/util/Constants.java b/java/com/android/contacts/common/util/Constants.java new file mode 100644 index 000000000..172e8c348 --- /dev/null +++ b/java/com/android/contacts/common/util/Constants.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.util; + +public class Constants { + + /** + * Log tag for performance measurement. To enable: adb shell setprop log.tag.ContactsPerf VERBOSE + */ + public static final String PERFORMANCE_TAG = "ContactsPerf"; + + // Used for lookup URI that contains an encoded JSON string. + public static final String LOOKUP_URI_ENCODED = "encoded"; +} diff --git a/java/com/android/contacts/common/util/ContactDisplayUtils.java b/java/com/android/contacts/common/util/ContactDisplayUtils.java new file mode 100644 index 000000000..1586784db --- /dev/null +++ b/java/com/android/contacts/common/util/ContactDisplayUtils.java @@ -0,0 +1,307 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.contacts.common.util; + +import static android.provider.ContactsContract.CommonDataKinds.Phone; + +import android.content.Context; +import android.content.res.Resources; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.TextUtils; +import android.text.style.TtsSpan; +import android.util.Log; +import android.util.Patterns; +import com.android.contacts.common.R; +import com.android.contacts.common.compat.PhoneNumberUtilsCompat; +import com.android.contacts.common.preference.ContactsPreferences; +import java.util.Objects; + +/** Methods for handling various contact data labels. */ +public class ContactDisplayUtils { + + public static final int INTERACTION_CALL = 1; + public static final int INTERACTION_SMS = 2; + private static final String TAG = ContactDisplayUtils.class.getSimpleName(); + + /** + * Checks if the given data type is a custom type. + * + * @param type Phone data type. + * @return {@literal true} if the type is custom. {@literal false} if not. + */ + public static boolean isCustomPhoneType(Integer type) { + return type == Phone.TYPE_CUSTOM || type == Phone.TYPE_ASSISTANT; + } + + /** + * Gets a display label for a given phone type. + * + * @param type The type of number. + * @param customLabel A custom label to use if the phone is determined to be of custom type + * determined by {@link #isCustomPhoneType(Integer))} + * @param interactionType whether this is a call or sms. Either {@link #INTERACTION_CALL} or + * {@link #INTERACTION_SMS}. + * @param context The application context. + * @return An appropriate string label + */ + public static CharSequence getLabelForCallOrSms( + Integer type, CharSequence customLabel, int interactionType, @NonNull Context context) { + Objects.requireNonNull(context); + + if (isCustomPhoneType(type)) { + return (customLabel == null) ? "" : customLabel; + } else { + int resId; + if (interactionType == INTERACTION_SMS) { + resId = getSmsLabelResourceId(type); + } else { + resId = getPhoneLabelResourceId(type); + if (interactionType != INTERACTION_CALL) { + Log.e( + TAG, + "Un-recognized interaction type: " + + interactionType + + ". Defaulting to ContactDisplayUtils.INTERACTION_CALL."); + } + } + + return context.getResources().getText(resId); + } + } + + /** + * Find a label for calling. + * + * @param type The type of number. + * @return An appropriate string label. + */ + public static int getPhoneLabelResourceId(Integer type) { + if (type == null) { + return R.string.call_other; + } + switch (type) { + case Phone.TYPE_HOME: + return R.string.call_home; + case Phone.TYPE_MOBILE: + return R.string.call_mobile; + case Phone.TYPE_WORK: + return R.string.call_work; + case Phone.TYPE_FAX_WORK: + return R.string.call_fax_work; + case Phone.TYPE_FAX_HOME: + return R.string.call_fax_home; + case Phone.TYPE_PAGER: + return R.string.call_pager; + case Phone.TYPE_OTHER: + return R.string.call_other; + case Phone.TYPE_CALLBACK: + return R.string.call_callback; + case Phone.TYPE_CAR: + return R.string.call_car; + case Phone.TYPE_COMPANY_MAIN: + return R.string.call_company_main; + case Phone.TYPE_ISDN: + return R.string.call_isdn; + case Phone.TYPE_MAIN: + return R.string.call_main; + case Phone.TYPE_OTHER_FAX: + return R.string.call_other_fax; + case Phone.TYPE_RADIO: + return R.string.call_radio; + case Phone.TYPE_TELEX: + return R.string.call_telex; + case Phone.TYPE_TTY_TDD: + return R.string.call_tty_tdd; + case Phone.TYPE_WORK_MOBILE: + return R.string.call_work_mobile; + case Phone.TYPE_WORK_PAGER: + return R.string.call_work_pager; + case Phone.TYPE_ASSISTANT: + return R.string.call_assistant; + case Phone.TYPE_MMS: + return R.string.call_mms; + default: + return R.string.call_custom; + } + } + + /** + * Find a label for sending an sms. + * + * @param type The type of number. + * @return An appropriate string label. + */ + public static int getSmsLabelResourceId(Integer type) { + if (type == null) { + return R.string.sms_other; + } + switch (type) { + case Phone.TYPE_HOME: + return R.string.sms_home; + case Phone.TYPE_MOBILE: + return R.string.sms_mobile; + case Phone.TYPE_WORK: + return R.string.sms_work; + case Phone.TYPE_FAX_WORK: + return R.string.sms_fax_work; + case Phone.TYPE_FAX_HOME: + return R.string.sms_fax_home; + case Phone.TYPE_PAGER: + return R.string.sms_pager; + case Phone.TYPE_OTHER: + return R.string.sms_other; + case Phone.TYPE_CALLBACK: + return R.string.sms_callback; + case Phone.TYPE_CAR: + return R.string.sms_car; + case Phone.TYPE_COMPANY_MAIN: + return R.string.sms_company_main; + case Phone.TYPE_ISDN: + return R.string.sms_isdn; + case Phone.TYPE_MAIN: + return R.string.sms_main; + case Phone.TYPE_OTHER_FAX: + return R.string.sms_other_fax; + case Phone.TYPE_RADIO: + return R.string.sms_radio; + case Phone.TYPE_TELEX: + return R.string.sms_telex; + case Phone.TYPE_TTY_TDD: + return R.string.sms_tty_tdd; + case Phone.TYPE_WORK_MOBILE: + return R.string.sms_work_mobile; + case Phone.TYPE_WORK_PAGER: + return R.string.sms_work_pager; + case Phone.TYPE_ASSISTANT: + return R.string.sms_assistant; + case Phone.TYPE_MMS: + return R.string.sms_mms; + default: + return R.string.sms_custom; + } + } + + /** + * Whether the given text could be a phone number. + * + *

Note this will miss many things that are legitimate phone numbers, for example, phone + * numbers with letters. + */ + public static boolean isPossiblePhoneNumber(CharSequence text) { + return text != null && Patterns.PHONE.matcher(text.toString()).matches(); + } + + /** + * Returns a Spannable for the given message with a telephone {@link TtsSpan} set for the given + * phone number text wherever it is found within the message. + */ + public static Spannable getTelephoneTtsSpannable( + @Nullable String message, @Nullable String phoneNumber) { + if (message == null) { + return null; + } + final Spannable spannable = new SpannableString(message); + int start = phoneNumber == null ? -1 : message.indexOf(phoneNumber); + while (start >= 0) { + final int end = start + phoneNumber.length(); + final TtsSpan ttsSpan = PhoneNumberUtilsCompat.createTtsSpan(phoneNumber); + spannable.setSpan( + ttsSpan, + start, + end, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); // this is consistenly done in a misleading way.. + start = message.indexOf(phoneNumber, end); + } + return spannable; + } + + /** + * Retrieves a string from a string template that takes 1 phone number as argument, span the + * number with a telephone {@link TtsSpan}, and return the spanned string. + * + * @param resources to retrieve the string from + * @param stringId ID of the string + * @param number to pass in the template + * @return CharSequence with the phone number wrapped in a TtsSpan + */ + public static CharSequence getTtsSpannedPhoneNumber( + Resources resources, int stringId, String number) { + String msg = resources.getString(stringId, number); + return ContactDisplayUtils.getTelephoneTtsSpannable(msg, number); + } + + /** + * Returns either namePrimary or nameAlternative based on the {@link ContactsPreferences}. + * Defaults to the name that is non-null. + * + * @param namePrimary the primary name. + * @param nameAlternative the alternative name. + * @param contactsPreferences the ContactsPreferences used to determine the preferred display + * name. + * @return namePrimary or nameAlternative depending on the value of displayOrderPreference. + */ + public static String getPreferredDisplayName( + String namePrimary, + String nameAlternative, + @Nullable ContactsPreferences contactsPreferences) { + if (contactsPreferences == null) { + return namePrimary != null ? namePrimary : nameAlternative; + } + if (contactsPreferences.getDisplayOrder() == ContactsPreferences.DISPLAY_ORDER_PRIMARY) { + return namePrimary; + } + + if (contactsPreferences.getDisplayOrder() == ContactsPreferences.DISPLAY_ORDER_ALTERNATIVE + && !TextUtils.isEmpty(nameAlternative)) { + return nameAlternative; + } + + return namePrimary; + } + + /** + * Returns either namePrimary or nameAlternative based on the {@link ContactsPreferences}. + * Defaults to the name that is non-null. + * + * @param namePrimary the primary name. + * @param nameAlternative the alternative name. + * @param contactsPreferences the ContactsPreferences used to determine the preferred sort order. + * @return namePrimary or nameAlternative depending on the value of displayOrderPreference. + */ + public static String getPreferredSortName( + String namePrimary, + String nameAlternative, + @Nullable ContactsPreferences contactsPreferences) { + if (contactsPreferences == null) { + return namePrimary != null ? namePrimary : nameAlternative; + } + + if (contactsPreferences.getSortOrder() == ContactsPreferences.SORT_ORDER_PRIMARY) { + return namePrimary; + } + + if (contactsPreferences.getSortOrder() == ContactsPreferences.SORT_ORDER_ALTERNATIVE + && !TextUtils.isEmpty(nameAlternative)) { + return nameAlternative; + } + + return namePrimary; + } +} diff --git a/java/com/android/contacts/common/util/ContactListViewUtils.java b/java/com/android/contacts/common/util/ContactListViewUtils.java new file mode 100644 index 000000000..278c27d5c --- /dev/null +++ b/java/com/android/contacts/common/util/ContactListViewUtils.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.util; + +import android.content.res.Resources; +import android.view.View; +import android.widget.ListView; +import com.android.contacts.common.R; +import com.android.dialer.util.ViewUtil; + +/** Utilities for configuring ListViews with a card background. */ +public class ContactListViewUtils { + + // These two constants will help add more padding for the text inside the card. + private static final double TEXT_LEFT_PADDING_TO_CARD_PADDING_RATIO = 1.1; + + private static void addPaddingToView( + ListView listView, int parentWidth, int listSpaceWeight, int listViewWeight) { + if (listSpaceWeight > 0 && listViewWeight > 0) { + double paddingPercent = + (double) listSpaceWeight / (double) (listSpaceWeight * 2 + listViewWeight); + listView.setPadding( + (int) (parentWidth * paddingPercent * TEXT_LEFT_PADDING_TO_CARD_PADDING_RATIO), + listView.getPaddingTop(), + (int) (parentWidth * paddingPercent * TEXT_LEFT_PADDING_TO_CARD_PADDING_RATIO), + listView.getPaddingBottom()); + // The EdgeEffect and ScrollBar need to span to the edge of the ListView's padding. + listView.setClipToPadding(false); + listView.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY); + } + } + + /** + * Add padding to {@param listView} if this configuration has set both space weight and view + * weight on the layout. Use this util method instead of defining the padding in the layout file + * so that the {@param listView}'s padding can be set proportional to the card padding. + * + * @param listView ListView that we add padding to + * @param rootLayout layout that contains ListView and R.id.list_card + */ + public static void applyCardPaddingToView( + Resources resources, final ListView listView, final View rootLayout) { + // Set a padding on the list view so it appears in the center of the card + // in the layout if required. + final int listSpaceWeight = resources.getInteger(R.integer.contact_list_space_layout_weight); + final int listViewWeight = resources.getInteger(R.integer.contact_list_card_layout_weight); + if (listSpaceWeight > 0 && listViewWeight > 0) { + rootLayout.setBackgroundResource(0); + // Set the card view visible + View mCardView = rootLayout.findViewById(R.id.list_card); + if (mCardView == null) { + throw new RuntimeException( + "Your content must have a list card view who can be turned visible " + + "whenever it is necessary."); + } + mCardView.setVisibility(View.VISIBLE); + + // Add extra padding to the list view to make them appear in the center of the card. + // In order to avoid jumping, we skip drawing the next frame of the ListView. + ViewUtil.doOnPreDraw( + listView, + false, + new Runnable() { + @Override + public void run() { + // Use the rootLayout.getWidth() instead of listView.getWidth() since + // we sometimes hide the listView until we finish loading data. This would + // result in incorrect padding. + ContactListViewUtils.addPaddingToView( + listView, rootLayout.getWidth(), listSpaceWeight, listViewWeight); + } + }); + } + } +} diff --git a/java/com/android/contacts/common/util/ContactLoaderUtils.java b/java/com/android/contacts/common/util/ContactLoaderUtils.java new file mode 100644 index 000000000..e30971721 --- /dev/null +++ b/java/com/android/contacts/common/util/ContactLoaderUtils.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.util; + +import android.content.ContentResolver; +import android.content.ContentUris; +import android.net.Uri; +import android.provider.Contacts; +import android.provider.ContactsContract; +import android.provider.ContactsContract.RawContacts; + +/** Utility methods for the {@link ContactLoader}. */ +public final class ContactLoaderUtils { + + /** Static helper, not instantiable. */ + private ContactLoaderUtils() {} + + /** + * Transforms the given Uri and returns a Lookup-Uri that represents the contact. For legacy + * contacts, a raw-contact lookup is performed. An {@link IllegalArgumentException} can be thrown + * if the URI is null or the authority is not recognized. + * + *

Do not call from the UI thread. + */ + @SuppressWarnings("deprecation") + public static Uri ensureIsContactUri(final ContentResolver resolver, final Uri uri) + throws IllegalArgumentException { + if (uri == null) { + throw new IllegalArgumentException("uri must not be null"); + } + + final String authority = uri.getAuthority(); + + // Current Style Uri? + if (ContactsContract.AUTHORITY.equals(authority)) { + final String type = resolver.getType(uri); + // Contact-Uri? Good, return it + if (ContactsContract.Contacts.CONTENT_ITEM_TYPE.equals(type)) { + return uri; + } + + // RawContact-Uri? Transform it to ContactUri + if (RawContacts.CONTENT_ITEM_TYPE.equals(type)) { + final long rawContactId = ContentUris.parseId(uri); + return RawContacts.getContactLookupUri( + resolver, ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId)); + } + + // Anything else? We don't know what this is + throw new IllegalArgumentException("uri format is unknown"); + } + + // Legacy Style? Convert to RawContact + final String OBSOLETE_AUTHORITY = Contacts.AUTHORITY; + if (OBSOLETE_AUTHORITY.equals(authority)) { + // Legacy Format. Convert to RawContact-Uri and then lookup the contact + final long rawContactId = ContentUris.parseId(uri); + return RawContacts.getContactLookupUri( + resolver, ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId)); + } + + throw new IllegalArgumentException("uri authority is unknown"); + } +} diff --git a/java/com/android/contacts/common/util/DateUtils.java b/java/com/android/contacts/common/util/DateUtils.java new file mode 100644 index 000000000..1935d727a --- /dev/null +++ b/java/com/android/contacts/common/util/DateUtils.java @@ -0,0 +1,283 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.util; + +import android.content.Context; +import android.text.format.DateFormat; +import android.text.format.Time; +import java.text.ParsePosition; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.Locale; +import java.util.TimeZone; + +/** Utility methods for processing dates. */ +public class DateUtils { + + public static final TimeZone UTC_TIMEZONE = TimeZone.getTimeZone("UTC"); + + /** + * When parsing a date without a year, the system assumes 1970, which wasn't a leap-year. Let's + * add a one-off hack for that day of the year + */ + public static final String NO_YEAR_DATE_FEB29TH = "--02-29"; + + // Variations of ISO 8601 date format. Do not change the order - it does affect the + // result in ambiguous cases. + private static final SimpleDateFormat[] DATE_FORMATS = { + CommonDateUtils.FULL_DATE_FORMAT, + CommonDateUtils.DATE_AND_TIME_FORMAT, + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'", Locale.US), + new SimpleDateFormat("yyyyMMdd", Locale.US), + new SimpleDateFormat("yyyyMMdd'T'HHmmssSSS'Z'", Locale.US), + new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.US), + new SimpleDateFormat("yyyyMMdd'T'HHmm'Z'", Locale.US), + }; + + static { + for (SimpleDateFormat format : DATE_FORMATS) { + format.setLenient(true); + format.setTimeZone(UTC_TIMEZONE); + } + CommonDateUtils.NO_YEAR_DATE_FORMAT.setTimeZone(UTC_TIMEZONE); + } + + /** + * Parses the supplied string to see if it looks like a date. + * + * @param string The string representation of the provided date + * @param mustContainYear If true, the string is parsed as a date containing a year. If false, the + * string is parsed into a valid date even if the year field is missing. + * @return A Calendar object corresponding to the date if the string is successfully parsed. If + * not, null is returned. + */ + public static Calendar parseDate(String string, boolean mustContainYear) { + ParsePosition parsePosition = new ParsePosition(0); + Date date; + if (!mustContainYear) { + final boolean noYearParsed; + // Unfortunately, we can't parse Feb 29th correctly, so let's handle this day seperately + if (NO_YEAR_DATE_FEB29TH.equals(string)) { + return getUtcDate(0, Calendar.FEBRUARY, 29); + } else { + synchronized (CommonDateUtils.NO_YEAR_DATE_FORMAT) { + date = CommonDateUtils.NO_YEAR_DATE_FORMAT.parse(string, parsePosition); + } + noYearParsed = parsePosition.getIndex() == string.length(); + } + + if (noYearParsed) { + return getUtcDate(date, true); + } + } + for (int i = 0; i < DATE_FORMATS.length; i++) { + SimpleDateFormat f = DATE_FORMATS[i]; + synchronized (f) { + parsePosition.setIndex(0); + date = f.parse(string, parsePosition); + if (parsePosition.getIndex() == string.length()) { + return getUtcDate(date, false); + } + } + } + return null; + } + + private static final Calendar getUtcDate(Date date, boolean noYear) { + final Calendar calendar = Calendar.getInstance(UTC_TIMEZONE, Locale.US); + calendar.setTime(date); + if (noYear) { + calendar.set(Calendar.YEAR, 0); + } + return calendar; + } + + private static final Calendar getUtcDate(int year, int month, int dayOfMonth) { + final Calendar calendar = Calendar.getInstance(UTC_TIMEZONE, Locale.US); + calendar.clear(); + calendar.set(Calendar.YEAR, year); + calendar.set(Calendar.MONTH, month); + calendar.set(Calendar.DAY_OF_MONTH, dayOfMonth); + return calendar; + } + + public static boolean isYearSet(Calendar cal) { + // use the Calendar.YEAR field to track whether or not the year is set instead of + // Calendar.isSet() because doing Calendar.get() causes Calendar.isSet() to become + // true irregardless of what the previous value was + return cal.get(Calendar.YEAR) > 1; + } + + /** + * Same as {@link #formatDate(Context context, String string, boolean longForm)}, with longForm + * set to {@code true} by default. + * + * @param context Valid context + * @param string String representation of a date to parse + * @return Returns the same date in a cleaned up format. If the supplied string does not look like + * a date, return it unchanged. + */ + public static String formatDate(Context context, String string) { + return formatDate(context, string, true); + } + + /** + * Parses the supplied string to see if it looks like a date. + * + * @param context Valid context + * @param string String representation of a date to parse + * @param longForm If true, return the date formatted into its long string representation. If + * false, return the date formatted using its short form representation (i.e. 12/11/2012) + * @return Returns the same date in a cleaned up format. If the supplied string does not look like + * a date, return it unchanged. + */ + public static String formatDate(Context context, String string, boolean longForm) { + if (string == null) { + return null; + } + + string = string.trim(); + if (string.length() == 0) { + return string; + } + final Calendar cal = parseDate(string, false); + + // we weren't able to parse the string successfully so just return it unchanged + if (cal == null) { + return string; + } + + final boolean isYearSet = isYearSet(cal); + final java.text.DateFormat outFormat; + if (!isYearSet) { + outFormat = getLocalizedDateFormatWithoutYear(context); + } else { + outFormat = + longForm ? DateFormat.getLongDateFormat(context) : DateFormat.getDateFormat(context); + } + synchronized (outFormat) { + outFormat.setTimeZone(UTC_TIMEZONE); + return outFormat.format(cal.getTime()); + } + } + + public static boolean isMonthBeforeDay(Context context) { + char[] dateFormatOrder = DateFormat.getDateFormatOrder(context); + for (int i = 0; i < dateFormatOrder.length; i++) { + if (dateFormatOrder[i] == 'd') { + return false; + } + if (dateFormatOrder[i] == 'M') { + return true; + } + } + return false; + } + + /** + * Returns a SimpleDateFormat object without the year fields by using a regular expression to + * eliminate the year in the string pattern. In the rare occurence that the resulting pattern + * cannot be reconverted into a SimpleDateFormat, it uses the provided context to determine + * whether the month field should be displayed before the day field, and returns either "MMMM dd" + * or "dd MMMM" converted into a SimpleDateFormat. + */ + public static java.text.DateFormat getLocalizedDateFormatWithoutYear(Context context) { + final String pattern = + ((SimpleDateFormat) SimpleDateFormat.getDateInstance(java.text.DateFormat.LONG)) + .toPattern(); + // Determine the correct regex pattern for year. + // Special case handling for Spanish locale by checking for "de" + final String yearPattern = + pattern.contains("de") ? "[^Mm]*[Yy]+[^Mm]*" : "[^DdMm]*[Yy]+[^DdMm]*"; + try { + // Eliminate the substring in pattern that matches the format for that of year + return new SimpleDateFormat(pattern.replaceAll(yearPattern, "")); + } catch (IllegalArgumentException e) { + return new SimpleDateFormat(DateUtils.isMonthBeforeDay(context) ? "MMMM dd" : "dd MMMM"); + } + } + + /** + * Given a calendar (possibly containing only a day of the year), returns the earliest possible + * anniversary of the date that is equal to or after the current point in time if the date does + * not contain a year, or the date converted to the local time zone (if the date contains a year. + * + * @param target The date we wish to convert(in the UTC time zone). + * @return If date does not contain a year (year < 1900), returns the next earliest anniversary + * that is after the current point in time (in the local time zone). Otherwise, returns the + * adjusted Date in the local time zone. + */ + public static Date getNextAnnualDate(Calendar target) { + final Calendar today = Calendar.getInstance(); + today.setTime(new Date()); + + // Round the current time to the exact start of today so that when we compare + // today against the target date, both dates are set to exactly 0000H. + today.set(Calendar.HOUR_OF_DAY, 0); + today.set(Calendar.MINUTE, 0); + today.set(Calendar.SECOND, 0); + today.set(Calendar.MILLISECOND, 0); + + final boolean isYearSet = isYearSet(target); + final int targetYear = target.get(Calendar.YEAR); + final int targetMonth = target.get(Calendar.MONTH); + final int targetDay = target.get(Calendar.DAY_OF_MONTH); + final boolean isFeb29 = (targetMonth == Calendar.FEBRUARY && targetDay == 29); + final GregorianCalendar anniversary = new GregorianCalendar(); + // Convert from the UTC date to the local date. Set the year to today's year if the + // there is no provided year (targetYear < 1900) + anniversary.set(!isYearSet ? today.get(Calendar.YEAR) : targetYear, targetMonth, targetDay); + // If the anniversary's date is before the start of today and there is no year set, + // increment the year by 1 so that the returned date is always equal to or greater than + // today. If the day is a leap year, keep going until we get the next leap year anniversary + // Otherwise if there is already a year set, simply return the exact date. + if (!isYearSet) { + int anniversaryYear = today.get(Calendar.YEAR); + if (anniversary.before(today) || (isFeb29 && !anniversary.isLeapYear(anniversaryYear))) { + // If the target date is not Feb 29, then set the anniversary to the next year. + // Otherwise, keep going until we find the next leap year (this is not guaranteed + // to be in 4 years time). + do { + anniversaryYear += 1; + } while (isFeb29 && !anniversary.isLeapYear(anniversaryYear)); + anniversary.set(anniversaryYear, targetMonth, targetDay); + } + } + return anniversary.getTime(); + } + + /** + * Determine the difference, in days between two dates. Uses similar logic as the {@link + * android.text.format.DateUtils.getRelativeTimeSpanString} method. + * + * @param time Instance of time object to use for calculations. + * @param date1 First date to check. + * @param date2 Second date to check. + * @return The absolute difference in days between the two dates. + */ + public static int getDayDifference(Time time, long date1, long date2) { + time.set(date1); + int startDay = Time.getJulianDay(date1, time.gmtoff); + + time.set(date2); + int currentDay = Time.getJulianDay(date2, time.gmtoff); + + return Math.abs(currentDay - startDay); + } +} diff --git a/java/com/android/contacts/common/util/FabUtil.java b/java/com/android/contacts/common/util/FabUtil.java new file mode 100644 index 000000000..b1bb2e653 --- /dev/null +++ b/java/com/android/contacts/common/util/FabUtil.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.util; + +import android.content.res.Resources; +import android.graphics.Outline; +import android.view.View; +import android.view.ViewOutlineProvider; +import android.widget.ListView; +import com.android.contacts.common.R; +import com.android.dialer.compat.CompatUtils; + +/** Provides static functions to work with views */ +public class FabUtil { + + private static final ViewOutlineProvider OVAL_OUTLINE_PROVIDER = + new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + outline.setOval(0, 0, view.getWidth(), view.getHeight()); + } + }; + + private FabUtil() {} + + /** + * Configures the floating action button, clipping it to a circle and setting its translation z + * + * @param fabView the float action button's view + * @param res the resources file + */ + public static void setupFloatingActionButton(View fabView, Resources res) { + if (CompatUtils.isLollipopCompatible()) { + fabView.setOutlineProvider(OVAL_OUTLINE_PROVIDER); + fabView.setTranslationZ( + res.getDimensionPixelSize(R.dimen.floating_action_button_translation_z)); + } + } + + /** + * Adds padding to the bottom of the given {@link ListView} so that the floating action button + * does not obscure any content. + * + * @param listView to add the padding to + * @param res valid resources object + */ + public static void addBottomPaddingToListViewForFab(ListView listView, Resources res) { + final int fabPadding = + res.getDimensionPixelSize(R.dimen.floating_action_button_list_bottom_padding); + listView.setPaddingRelative( + listView.getPaddingStart(), + listView.getPaddingTop(), + listView.getPaddingEnd(), + listView.getPaddingBottom() + fabPadding); + listView.setClipToPadding(false); + } +} diff --git a/java/com/android/contacts/common/util/MaterialColorMapUtils.java b/java/com/android/contacts/common/util/MaterialColorMapUtils.java new file mode 100644 index 000000000..a2d9847ec --- /dev/null +++ b/java/com/android/contacts/common/util/MaterialColorMapUtils.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.util; + +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.Trace; +import com.android.contacts.common.R; + +public class MaterialColorMapUtils { + + private final TypedArray sPrimaryColors; + private final TypedArray sSecondaryColors; + + public MaterialColorMapUtils(Resources resources) { + sPrimaryColors = + resources.obtainTypedArray(com.android.contacts.common.R.array.letter_tile_colors); + sSecondaryColors = + resources.obtainTypedArray(com.android.contacts.common.R.array.letter_tile_colors_dark); + } + + public static MaterialPalette getDefaultPrimaryAndSecondaryColors(Resources resources) { + final int primaryColor = resources.getColor(R.color.quickcontact_default_photo_tint_color); + final int secondaryColor = + resources.getColor(R.color.quickcontact_default_photo_tint_color_dark); + return new MaterialPalette(primaryColor, secondaryColor); + } + + /** + * Returns the hue component of a color int. + * + * @return A value between 0.0f and 1.0f + */ + public static float hue(int color) { + int r = (color >> 16) & 0xFF; + int g = (color >> 8) & 0xFF; + int b = color & 0xFF; + + int V = Math.max(b, Math.max(r, g)); + int temp = Math.min(b, Math.min(r, g)); + + float H; + + if (V == temp) { + H = 0; + } else { + final float vtemp = V - temp; + final float cr = (V - r) / vtemp; + final float cg = (V - g) / vtemp; + final float cb = (V - b) / vtemp; + + if (r == V) { + H = cb - cg; + } else if (g == V) { + H = 2 + cr - cb; + } else { + H = 4 + cg - cr; + } + + H /= 6.f; + if (H < 0) { + H++; + } + } + + return H; + } + + /** + * Return primary and secondary colors from the Material color palette that are similar to {@param + * color}. + */ + public MaterialPalette calculatePrimaryAndSecondaryColor(int color) { + Trace.beginSection("calculatePrimaryAndSecondaryColor"); + + final float colorHue = hue(color); + float minimumDistance = Float.MAX_VALUE; + int indexBestMatch = 0; + for (int i = 0; i < sPrimaryColors.length(); i++) { + final int primaryColor = sPrimaryColors.getColor(i, 0); + final float comparedHue = hue(primaryColor); + // No need to be perceptually accurate when calculating color distances since + // we are only mapping to 15 colors. Being slightly inaccurate isn't going to change + // the mapping very often. + final float distance = Math.abs(comparedHue - colorHue); + if (distance < minimumDistance) { + minimumDistance = distance; + indexBestMatch = i; + } + } + + Trace.endSection(); + return new MaterialPalette( + sPrimaryColors.getColor(indexBestMatch, 0), sSecondaryColors.getColor(indexBestMatch, 0)); + } + + public static class MaterialPalette implements Parcelable { + + public static final Creator CREATOR = + new Creator() { + @Override + public MaterialPalette createFromParcel(Parcel in) { + return new MaterialPalette(in); + } + + @Override + public MaterialPalette[] newArray(int size) { + return new MaterialPalette[size]; + } + }; + public final int mPrimaryColor; + public final int mSecondaryColor; + + public MaterialPalette(int primaryColor, int secondaryColor) { + mPrimaryColor = primaryColor; + mSecondaryColor = secondaryColor; + } + + private MaterialPalette(Parcel in) { + mPrimaryColor = in.readInt(); + mSecondaryColor = in.readInt(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + MaterialPalette other = (MaterialPalette) obj; + if (mPrimaryColor != other.mPrimaryColor) { + return false; + } + if (mSecondaryColor != other.mSecondaryColor) { + return false; + } + return true; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + mPrimaryColor; + result = prime * result + mSecondaryColor; + return result; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mPrimaryColor); + dest.writeInt(mSecondaryColor); + } + } +} diff --git a/java/com/android/contacts/common/util/NameConverter.java b/java/com/android/contacts/common/util/NameConverter.java new file mode 100644 index 000000000..ae3275d14 --- /dev/null +++ b/java/com/android/contacts/common/util/NameConverter.java @@ -0,0 +1,242 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.contacts.common.util; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.net.Uri.Builder; +import android.provider.ContactsContract; +import android.provider.ContactsContract.CommonDataKinds.StructuredName; +import android.text.TextUtils; +import com.android.contacts.common.model.dataitem.StructuredNameDataItem; +import java.util.Map; +import java.util.TreeMap; + +/** + * Utility class for converting between a display name and structured name (and vice-versa), via + * calls to the contact provider. + */ +public class NameConverter { + + /** The array of fields that comprise a structured name. */ + public static final String[] STRUCTURED_NAME_FIELDS = + new String[] { + StructuredName.PREFIX, + StructuredName.GIVEN_NAME, + StructuredName.MIDDLE_NAME, + StructuredName.FAMILY_NAME, + StructuredName.SUFFIX + }; + + /** + * Converts the given structured name (provided as a map from {@link StructuredName} fields to + * corresponding values) into a display name string. + * + *

Note that this operates via a call back to the ContactProvider, but it does not access the + * database, so it should be safe to call from the UI thread. See ContactsProvider2.completeName() + * for the underlying method call. + * + * @param context Activity context. + * @param structuredName The structured name map to convert. + * @return The display name computed from the structured name map. + */ + public static String structuredNameToDisplayName( + Context context, Map structuredName) { + Builder builder = ContactsContract.AUTHORITY_URI.buildUpon().appendPath("complete_name"); + for (String key : STRUCTURED_NAME_FIELDS) { + if (structuredName.containsKey(key)) { + appendQueryParameter(builder, key, structuredName.get(key)); + } + } + return fetchDisplayName(context, builder.build()); + } + + /** + * Converts the given structured name (provided as ContentValues) into a display name string. + * + * @param context Activity context. + * @param values The content values containing values comprising the structured name. + */ + public static String structuredNameToDisplayName(Context context, ContentValues values) { + Builder builder = ContactsContract.AUTHORITY_URI.buildUpon().appendPath("complete_name"); + for (String key : STRUCTURED_NAME_FIELDS) { + if (values.containsKey(key)) { + appendQueryParameter(builder, key, values.getAsString(key)); + } + } + return fetchDisplayName(context, builder.build()); + } + + /** Helper method for fetching the display name via the given URI. */ + private static String fetchDisplayName(Context context, Uri uri) { + String displayName = null; + Cursor cursor = + context + .getContentResolver() + .query( + uri, + new String[] { + StructuredName.DISPLAY_NAME, + }, + null, + null, + null); + + if (cursor != null) { + try { + if (cursor.moveToFirst()) { + displayName = cursor.getString(0); + } + } finally { + cursor.close(); + } + } + return displayName; + } + + /** + * Converts the given display name string into a structured name (as a map from {@link + * StructuredName} fields to corresponding values). + * + *

Note that this operates via a call back to the ContactProvider, but it does not access the + * database, so it should be safe to call from the UI thread. + * + * @param context Activity context. + * @param displayName The display name to convert. + * @return The structured name map computed from the display name. + */ + public static Map displayNameToStructuredName( + Context context, String displayName) { + Map structuredName = new TreeMap(); + Builder builder = ContactsContract.AUTHORITY_URI.buildUpon().appendPath("complete_name"); + + appendQueryParameter(builder, StructuredName.DISPLAY_NAME, displayName); + Cursor cursor = + context + .getContentResolver() + .query(builder.build(), STRUCTURED_NAME_FIELDS, null, null, null); + + if (cursor != null) { + try { + if (cursor.moveToFirst()) { + for (int i = 0; i < STRUCTURED_NAME_FIELDS.length; i++) { + structuredName.put(STRUCTURED_NAME_FIELDS[i], cursor.getString(i)); + } + } + } finally { + cursor.close(); + } + } + return structuredName; + } + + /** + * Converts the given display name string into a structured name (inserting the structured values + * into a new or existing ContentValues object). + * + *

Note that this operates via a call back to the ContactProvider, but it does not access the + * database, so it should be safe to call from the UI thread. + * + * @param context Activity context. + * @param displayName The display name to convert. + * @param contentValues The content values object to place the structured name values into. If + * null, a new one will be created and returned. + * @return The ContentValues object containing the structured name fields derived from the display + * name. + */ + public static ContentValues displayNameToStructuredName( + Context context, String displayName, ContentValues contentValues) { + if (contentValues == null) { + contentValues = new ContentValues(); + } + Map mapValues = displayNameToStructuredName(context, displayName); + for (String key : mapValues.keySet()) { + contentValues.put(key, mapValues.get(key)); + } + return contentValues; + } + + private static void appendQueryParameter(Builder builder, String field, String value) { + if (!TextUtils.isEmpty(value)) { + builder.appendQueryParameter(field, value); + } + } + + /** + * Parses phonetic name and returns parsed data (family, middle, given) as ContentValues. Parsed + * data should be {@link StructuredName#PHONETIC_FAMILY_NAME}, {@link + * StructuredName#PHONETIC_MIDDLE_NAME}, and {@link StructuredName#PHONETIC_GIVEN_NAME}. If this + * method cannot parse given phoneticName, null values will be stored. + * + * @param phoneticName Phonetic name to be parsed + * @param values ContentValues to be used for storing data. If null, new instance will be created. + * @return ContentValues with parsed data. Those data can be null. + */ + public static StructuredNameDataItem parsePhoneticName( + String phoneticName, StructuredNameDataItem item) { + String family = null; + String middle = null; + String given = null; + + if (!TextUtils.isEmpty(phoneticName)) { + String[] strings = phoneticName.split(" ", 3); + switch (strings.length) { + case 1: + family = strings[0]; + break; + case 2: + family = strings[0]; + given = strings[1]; + break; + case 3: + family = strings[0]; + middle = strings[1]; + given = strings[2]; + break; + } + } + + if (item == null) { + item = new StructuredNameDataItem(); + } + item.setPhoneticFamilyName(family); + item.setPhoneticMiddleName(middle); + item.setPhoneticGivenName(given); + return item; + } + + /** Constructs and returns a phonetic full name from given parts. */ + public static String buildPhoneticName(String family, String middle, String given) { + if (!TextUtils.isEmpty(family) || !TextUtils.isEmpty(middle) || !TextUtils.isEmpty(given)) { + StringBuilder sb = new StringBuilder(); + if (!TextUtils.isEmpty(family)) { + sb.append(family.trim()).append(' '); + } + if (!TextUtils.isEmpty(middle)) { + sb.append(middle.trim()).append(' '); + } + if (!TextUtils.isEmpty(given)) { + sb.append(given.trim()).append(' '); + } + sb.setLength(sb.length() - 1); // Yank the last space + return sb.toString(); + } else { + return null; + } + } +} diff --git a/java/com/android/contacts/common/util/SearchUtil.java b/java/com/android/contacts/common/util/SearchUtil.java new file mode 100644 index 000000000..314d565b2 --- /dev/null +++ b/java/com/android/contacts/common/util/SearchUtil.java @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.util; + +import android.support.annotation.VisibleForTesting; + +/** Methods related to search. */ +public class SearchUtil { + + /** + * Given a string with lines delimited with '\n', finds the matching line to the given substring. + * + * @param contents The string to search. + * @param substring The substring to search for. + * @return A MatchedLine object containing the matching line and the startIndex of the substring + * match within that line. + */ + public static MatchedLine findMatchingLine(String contents, String substring) { + final MatchedLine matched = new MatchedLine(); + + // Snippet may contain multiple lines separated by "\n". + // Locate the lines of the content that contain the substring. + final int index = SearchUtil.contains(contents, substring); + if (index != -1) { + // Match found. Find the corresponding line. + int start = index - 1; + while (start > -1) { + if (contents.charAt(start) == '\n') { + break; + } + start--; + } + int end = index + 1; + while (end < contents.length()) { + if (contents.charAt(end) == '\n') { + break; + } + end++; + } + matched.line = contents.substring(start + 1, end); + matched.startIndex = index - (start + 1); + } + return matched; + } + + /** + * Similar to String.contains() with two main differences: + * + *

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

2) Returns the starting index where the substring is found. + * + * @param value The string to search. + * @param substring The substring to look for. + * @return The starting index where the substring is found. {@literal -1} if substring is not + * found in value. + */ + @VisibleForTesting + static int contains(String value, String substring) { + if (value.length() < substring.length()) { + return -1; + } + + // i18n support + // Generate the code points for the substring once. + // There will be a maximum of substring.length code points. But may be fewer. + // Since the array length is not an accurate size, we need to keep a separate variable. + final int[] substringCodePoints = new int[substring.length()]; + int substringLength = 0; // may not equal substring.length()!! + for (int i = 0; i < substring.length(); ) { + final int codePoint = Character.codePointAt(substring, i); + substringCodePoints[substringLength] = codePoint; + substringLength++; + i += Character.charCount(codePoint); + } + + for (int i = 0; i < value.length(); i = findNextTokenStart(value, i)) { + int numMatch = 0; + for (int j = i; j < value.length() && numMatch < substringLength; ++numMatch) { + int valueCp = Character.toLowerCase(value.codePointAt(j)); + int substringCp = substringCodePoints[numMatch]; + if (valueCp != substringCp) { + break; + } + j += Character.charCount(valueCp); + } + if (numMatch == substringLength) { + return i; + } + } + return -1; + } + + /** + * Find the start of the next token. A token is composed of letters and numbers. Any other + * character are considered delimiters. + * + * @param line The string to search for the next token. + * @param startIndex The index to start searching. 0 based indexing. + * @return The index for the start of the next token. line.length() if next token not found. + */ + @VisibleForTesting + static int findNextTokenStart(String line, int startIndex) { + int index = startIndex; + + // If already in token, eat remainder of token. + while (index <= line.length()) { + if (index == line.length()) { + // No more tokens. + return index; + } + final int codePoint = line.codePointAt(index); + if (!Character.isLetterOrDigit(codePoint)) { + break; + } + index += Character.charCount(codePoint); + } + + // Out of token, eat all consecutive delimiters. + while (index <= line.length()) { + if (index == line.length()) { + return index; + } + final int codePoint = line.codePointAt(index); + if (Character.isLetterOrDigit(codePoint)) { + break; + } + index += Character.charCount(codePoint); + } + + return index; + } + + /** + * Anything other than letter and numbers are considered delimiters. Remove start and end + * delimiters since they are not relevant to search. + * + * @param query The query string to clean. + * @return The cleaned query. Empty string if all characters are cleaned out. + */ + public static String cleanStartAndEndOfSearchQuery(String query) { + int start = 0; + while (start < query.length()) { + int codePoint = query.codePointAt(start); + if (Character.isLetterOrDigit(codePoint)) { + break; + } + start += Character.charCount(codePoint); + } + + if (start == query.length()) { + // All characters are delimiters. + return ""; + } + + int end = query.length() - 1; + while (end > -1) { + if (Character.isLowSurrogate(query.charAt(end))) { + // Assume valid i18n string. There should be a matching high surrogate before it. + end--; + } + int codePoint = query.codePointAt(end); + if (Character.isLetterOrDigit(codePoint)) { + break; + } + end--; + } + + // end is a letter or digit. + return query.substring(start, end + 1); + } + + public static class MatchedLine { + + public int startIndex = -1; + public String line; + + @Override + public String toString() { + return "MatchedLine{" + "line='" + line + '\'' + ", startIndex=" + startIndex + '}'; + } + } +} diff --git a/java/com/android/contacts/common/util/StopWatch.java b/java/com/android/contacts/common/util/StopWatch.java new file mode 100644 index 000000000..b944b9867 --- /dev/null +++ b/java/com/android/contacts/common/util/StopWatch.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.util; + +import android.util.Log; +import java.util.ArrayList; + +/** A {@link StopWatch} records start, laps and stop, and print them to logcat. */ +public class StopWatch { + + private final String mLabel; + + private final ArrayList mTimes = new ArrayList<>(); + private final ArrayList mLapLabels = new ArrayList<>(); + + private StopWatch(String label) { + mLabel = label; + lap(""); + } + + /** Create a new instance and start it. */ + public static StopWatch start(String label) { + return new StopWatch(label); + } + + /** Return a dummy instance that does no operations. */ + public static StopWatch getNullStopWatch() { + return NullStopWatch.INSTANCE; + } + + /** Record a lap. */ + public void lap(String lapLabel) { + mTimes.add(System.currentTimeMillis()); + mLapLabels.add(lapLabel); + } + + /** Stop it and log the result, if the total time >= {@code timeThresholdToLog}. */ + public void stopAndLog(String TAG, int timeThresholdToLog) { + + lap(""); + + final long start = mTimes.get(0); + final long stop = mTimes.get(mTimes.size() - 1); + + final long total = stop - start; + if (total < timeThresholdToLog) { + return; + } + + final StringBuilder sb = new StringBuilder(); + sb.append(mLabel); + sb.append(","); + sb.append(total); + sb.append(": "); + + long last = start; + for (int i = 1; i < mTimes.size(); i++) { + final long current = mTimes.get(i); + sb.append(mLapLabels.get(i)); + sb.append(","); + sb.append((current - last)); + sb.append(" "); + last = current; + } + Log.v(TAG, sb.toString()); + } + + private static class NullStopWatch extends StopWatch { + + public static final NullStopWatch INSTANCE = new NullStopWatch(); + + public NullStopWatch() { + super(null); + } + + @Override + public void lap(String lapLabel) { + // noop + } + + @Override + public void stopAndLog(String TAG, int timeThresholdToLog) { + // noop + } + } +} diff --git a/java/com/android/contacts/common/util/TelephonyManagerUtils.java b/java/com/android/contacts/common/util/TelephonyManagerUtils.java new file mode 100644 index 000000000..b664268ca --- /dev/null +++ b/java/com/android/contacts/common/util/TelephonyManagerUtils.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.contacts.common.util; + +import android.content.Context; +import android.telephony.TelephonyManager; + +/** This class provides several TelephonyManager util functions. */ +public class TelephonyManagerUtils { + + /** + * Gets the voicemail tag from Telephony Manager. + * + * @param context Current application context + * @return Voicemail tag, the alphabetic identifier associated with the voice mail number. + */ + public static String getVoiceMailAlphaTag(Context context) { + final TelephonyManager telephonyManager = + (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + final String voiceMailLabel = telephonyManager.getVoiceMailAlphaTag(); + return voiceMailLabel; + } + + /** + * @param context Current application context. + * @return True if there is a subscription which supports video calls. False otherwise. + */ + public static boolean hasVideoCallSubscription(Context context) { + // TODO: Check the telephony manager's subscriptions to see if any support video calls. + return true; + } +} diff --git a/java/com/android/contacts/common/util/TrafficStatsTags.java b/java/com/android/contacts/common/util/TrafficStatsTags.java new file mode 100644 index 000000000..b0e7fb583 --- /dev/null +++ b/java/com/android/contacts/common/util/TrafficStatsTags.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.contacts.common.util; + +public class TrafficStatsTags { + + public static final int CONTACT_PHOTO_DOWNLOAD_TAG = 0x0001; + public static final int TAG_MAX = 0x9999; +} diff --git a/java/com/android/contacts/common/util/UriUtils.java b/java/com/android/contacts/common/util/UriUtils.java new file mode 100644 index 000000000..4690942ba --- /dev/null +++ b/java/com/android/contacts/common/util/UriUtils.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.util; + +import android.net.Uri; +import android.provider.ContactsContract; +import java.util.List; + +/** Utility methods for dealing with URIs. */ +public class UriUtils { + + /** Static helper, not instantiable. */ + private UriUtils() {} + + /** Checks whether two URI are equal, taking care of the case where either is null. */ + public static boolean areEqual(Uri uri1, Uri uri2) { + if (uri1 == null && uri2 == null) { + return true; + } + if (uri1 == null || uri2 == null) { + return false; + } + return uri1.equals(uri2); + } + + /** Parses a string into a URI and returns null if the given string is null. */ + public static Uri parseUriOrNull(String uriString) { + if (uriString == null) { + return null; + } + return Uri.parse(uriString); + } + + /** Converts a URI into a string, returns null if the given URI is null. */ + public static String uriToString(Uri uri) { + return uri == null ? null : uri.toString(); + } + + public static boolean isEncodedContactUri(Uri uri) { + if (uri == null) { + return false; + } + final String lastPathSegment = uri.getLastPathSegment(); + if (lastPathSegment == null) { + return false; + } + return lastPathSegment.equals(Constants.LOOKUP_URI_ENCODED); + } + + /** + * @return {@code uri} as-is if the authority is of contacts provider. Otherwise or {@code uri} is + * null, return null otherwise + */ + public static Uri nullForNonContactsUri(Uri uri) { + if (uri == null) { + return null; + } + return ContactsContract.AUTHORITY.equals(uri.getAuthority()) ? uri : null; + } + + /** Parses the given URI to determine the original lookup key of the contact. */ + public static String getLookupKeyFromUri(Uri lookupUri) { + // Would be nice to be able to persist the lookup key somehow to avoid having to parse + // the uri entirely just to retrieve the lookup key, but every uri is already parsed + // once anyway to check if it is an encoded JSON uri, so this has negligible effect + // on performance. + if (lookupUri != null && !UriUtils.isEncodedContactUri(lookupUri)) { + final List segments = lookupUri.getPathSegments(); + // This returns the third path segment of the uri, where the lookup key is located. + // See {@link android.provider.ContactsContract.Contacts#CONTENT_LOOKUP_URI}. + return (segments.size() < 3) ? null : Uri.encode(segments.get(2)); + } else { + return null; + } + } +} diff --git a/java/com/android/contacts/common/widget/ActivityTouchLinearLayout.java b/java/com/android/contacts/common/widget/ActivityTouchLinearLayout.java new file mode 100644 index 000000000..2988a5a58 --- /dev/null +++ b/java/com/android/contacts/common/widget/ActivityTouchLinearLayout.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.widget; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.widget.LinearLayout; +import com.android.dialer.util.TouchPointManager; + +/** + * Linear layout for an activity that listens to all touch events on the screen and saves the touch + * point. Typically touch events are handled by child views--this class intercepts those touch + * events before passing them on to the child. + */ +public class ActivityTouchLinearLayout extends LinearLayout { + + public ActivityTouchLinearLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + TouchPointManager.getInstance().setPoint((int) ev.getRawX(), (int) ev.getRawY()); + } + return false; + } +} diff --git a/java/com/android/contacts/common/widget/FloatingActionButtonController.java b/java/com/android/contacts/common/widget/FloatingActionButtonController.java new file mode 100644 index 000000000..f03129779 --- /dev/null +++ b/java/com/android/contacts/common/widget/FloatingActionButtonController.java @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.widget; + +import android.app.Activity; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.view.View; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; +import android.widget.ImageButton; +import com.android.contacts.common.R; +import com.android.contacts.common.util.FabUtil; +import com.android.dialer.animation.AnimUtils; + +/** Controls the movement and appearance of the FAB (Floating Action Button). */ +public class FloatingActionButtonController { + + public static final int ALIGN_MIDDLE = 0; + public static final int ALIGN_QUARTER_END = 1; + public static final int ALIGN_END = 2; + + private static final int FAB_SCALE_IN_DURATION = 266; + private static final int FAB_SCALE_IN_FADE_IN_DELAY = 100; + private static final int FAB_ICON_FADE_OUT_DURATION = 66; + + private final int mAnimationDuration; + private final int mFloatingActionButtonWidth; + private final int mFloatingActionButtonMarginRight; + private final View mFloatingActionButtonContainer; + private final ImageButton mFloatingActionButton; + private final Interpolator mFabInterpolator; + private int mScreenWidth; + + public FloatingActionButtonController(Activity activity, View container, ImageButton button) { + Resources resources = activity.getResources(); + mFabInterpolator = + AnimationUtils.loadInterpolator(activity, android.R.interpolator.fast_out_slow_in); + mFloatingActionButtonWidth = + resources.getDimensionPixelSize(R.dimen.floating_action_button_width); + mFloatingActionButtonMarginRight = + resources.getDimensionPixelOffset(R.dimen.floating_action_button_margin_right); + mAnimationDuration = resources.getInteger(R.integer.floating_action_button_animation_duration); + mFloatingActionButtonContainer = container; + mFloatingActionButton = button; + FabUtil.setupFloatingActionButton(mFloatingActionButtonContainer, resources); + } + + /** + * Passes the screen width into the class. Necessary for translation calculations. Should be + * called as soon as parent View width is available. + * + * @param screenWidth The width of the screen in pixels. + */ + public void setScreenWidth(int screenWidth) { + mScreenWidth = screenWidth; + } + + public boolean isVisible() { + return mFloatingActionButtonContainer.getVisibility() == View.VISIBLE; + } + + /** + * Sets FAB as View.VISIBLE or View.GONE. + * + * @param visible Whether or not to make the container visible. + */ + public void setVisible(boolean visible) { + mFloatingActionButtonContainer.setVisibility(visible ? View.VISIBLE : View.GONE); + } + + public void changeIcon(Drawable icon, String description) { + if (mFloatingActionButton.getDrawable() != icon + || !mFloatingActionButton.getContentDescription().equals(description)) { + mFloatingActionButton.setImageDrawable(icon); + mFloatingActionButton.setContentDescription(description); + } + } + + /** + * Updates the FAB location (middle to right position) as the PageView scrolls. + * + * @param positionOffset A fraction used to calculate position of the FAB during page scroll. + */ + public void onPageScrolled(float positionOffset) { + // As the page is scrolling, if we're on the first tab, update the FAB position so it + // moves along with it. + mFloatingActionButtonContainer.setTranslationX( + (int) (positionOffset * getTranslationXForAlignment(ALIGN_END))); + } + + /** + * Aligns the FAB to the described location + * + * @param align One of ALIGN_MIDDLE, ALIGN_QUARTER_RIGHT, or ALIGN_RIGHT. + * @param animate Whether or not to animate the transition. + */ + public void align(int align, boolean animate) { + align(align, 0 /*offsetX */, 0 /* offsetY */, animate); + } + + /** + * Aligns the FAB to the described location plus specified additional offsets. + * + * @param align One of ALIGN_MIDDLE, ALIGN_QUARTER_RIGHT, or ALIGN_RIGHT. + * @param offsetX Additional offsetX to translate by. + * @param offsetY Additional offsetY to translate by. + * @param animate Whether or not to animate the transition. + */ + public void align(int align, int offsetX, int offsetY, boolean animate) { + if (mScreenWidth == 0) { + return; + } + + int translationX = getTranslationXForAlignment(align); + + // Skip animation if container is not shown; animation causes container to show again. + if (animate && mFloatingActionButtonContainer.isShown()) { + mFloatingActionButtonContainer + .animate() + .translationX(translationX + offsetX) + .translationY(offsetY) + .setInterpolator(mFabInterpolator) + .setDuration(mAnimationDuration) + .start(); + } else { + mFloatingActionButtonContainer.setTranslationX(translationX + offsetX); + mFloatingActionButtonContainer.setTranslationY(offsetY); + } + } + + /** + * Resizes width and height of the floating action bar container. + * + * @param dimension The new dimensions for the width and height. + * @param animate Whether to animate this change. + */ + public void resize(int dimension, boolean animate) { + if (animate) { + AnimUtils.changeDimensions(mFloatingActionButtonContainer, dimension, dimension); + } else { + mFloatingActionButtonContainer.getLayoutParams().width = dimension; + mFloatingActionButtonContainer.getLayoutParams().height = dimension; + mFloatingActionButtonContainer.requestLayout(); + } + } + + /** + * Scales the floating action button from no height and width to its actual dimensions. This is an + * animation for showing the floating action button. + * + * @param delayMs The delay for the effect, in milliseconds. + */ + public void scaleIn(int delayMs) { + setVisible(true); + AnimUtils.scaleIn(mFloatingActionButtonContainer, FAB_SCALE_IN_DURATION, delayMs); + AnimUtils.fadeIn( + mFloatingActionButton, FAB_SCALE_IN_DURATION, delayMs + FAB_SCALE_IN_FADE_IN_DELAY, null); + } + + /** Immediately remove the affects of the last call to {@link #scaleOut}. */ + public void resetIn() { + mFloatingActionButton.setAlpha(1f); + mFloatingActionButton.setVisibility(View.VISIBLE); + mFloatingActionButtonContainer.setScaleX(1); + mFloatingActionButtonContainer.setScaleY(1); + } + + /** + * Scales the floating action button from its actual dimensions to no height and width. This is an + * animation for hiding the floating action button. + */ + public void scaleOut() { + AnimUtils.scaleOut(mFloatingActionButtonContainer, mAnimationDuration); + // Fade out the icon faster than the scale out animation, so that the icon scaling is less + // obvious. We don't want it to scale, but the resizing the container is not as performant. + AnimUtils.fadeOut(mFloatingActionButton, FAB_ICON_FADE_OUT_DURATION, null); + } + + /** + * Calculates the X offset of the FAB to the given alignment, adjusted for whether or not the view + * is in RTL mode. + * + * @param align One of ALIGN_MIDDLE, ALIGN_QUARTER_RIGHT, or ALIGN_RIGHT. + * @return The translationX for the given alignment. + */ + public int getTranslationXForAlignment(int align) { + int result = 0; + switch (align) { + case ALIGN_MIDDLE: + // Moves the FAB to exactly center screen. + return 0; + case ALIGN_QUARTER_END: + // Moves the FAB a quarter of the screen width. + result = mScreenWidth / 4; + break; + case ALIGN_END: + // Moves the FAB half the screen width. Same as aligning right with a marginRight. + result = + mScreenWidth / 2 - mFloatingActionButtonWidth / 2 - mFloatingActionButtonMarginRight; + break; + } + if (isLayoutRtl()) { + result *= -1; + } + return result; + } + + private boolean isLayoutRtl() { + return mFloatingActionButtonContainer.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + } +} diff --git a/java/com/android/contacts/common/widget/LayoutSuppressingImageView.java b/java/com/android/contacts/common/widget/LayoutSuppressingImageView.java new file mode 100644 index 000000000..d84d8f757 --- /dev/null +++ b/java/com/android/contacts/common/widget/LayoutSuppressingImageView.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.widget; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.ImageView; + +/** + * Custom {@link ImageView} that improves layouting performance. + * + *

This improves the performance by not passing requestLayout() to its parent, taking advantage + * of knowing that image size won't change once set. + */ +public class LayoutSuppressingImageView extends ImageView { + + public LayoutSuppressingImageView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public void requestLayout() { + forceLayout(); + } +} diff --git a/java/com/android/contacts/common/widget/SelectPhoneAccountDialogFragment.java b/java/com/android/contacts/common/widget/SelectPhoneAccountDialogFragment.java new file mode 100644 index 000000000..63f8ca580 --- /dev/null +++ b/java/com/android/contacts/common/widget/SelectPhoneAccountDialogFragment.java @@ -0,0 +1,297 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.contacts.common.widget; + +import android.annotation.SuppressLint; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.os.Handler; +import android.os.ResultReceiver; +import android.support.annotation.Nullable; +import android.telecom.PhoneAccount; +import android.telecom.PhoneAccountHandle; +import android.telecom.TelecomManager; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ListAdapter; +import android.widget.TextView; +import com.android.contacts.common.R; +import com.android.contacts.common.compat.PhoneAccountCompat; +import com.android.contacts.common.compat.PhoneNumberUtilsCompat; +import java.util.ArrayList; +import java.util.List; + +/** + * Dialog that allows the user to select a phone accounts for a given action. Optionally provides + * the choice to set the phone account as default. + */ +public class SelectPhoneAccountDialogFragment extends DialogFragment { + + private static final String ARG_TITLE_RES_ID = "title_res_id"; + private static final String ARG_CAN_SET_DEFAULT = "can_set_default"; + private static final String ARG_ACCOUNT_HANDLES = "account_handles"; + private static final String ARG_IS_DEFAULT_CHECKED = "is_default_checked"; + private static final String ARG_LISTENER = "listener"; + private static final String ARG_CALL_ID = "call_id"; + + private int mTitleResId; + private boolean mCanSetDefault; + private List mAccountHandles; + private boolean mIsSelected; + private boolean mIsDefaultChecked; + private SelectPhoneAccountListener mListener; + + public SelectPhoneAccountDialogFragment() {} + + /** + * Create new fragment instance with default title and no option to set as default. + * + * @param accountHandles The {@code PhoneAccountHandle}s available to select from. + * @param listener The listener for the results of the account selection. + */ + public static SelectPhoneAccountDialogFragment newInstance( + List accountHandles, + SelectPhoneAccountListener listener, + @Nullable String callId) { + return newInstance( + R.string.select_account_dialog_title, false, accountHandles, listener, callId); + } + + /** + * Create new fragment instance. This method also allows specifying a custom title and "set + * default" checkbox. + * + * @param titleResId The resource ID for the string to use in the title of the dialog. + * @param canSetDefault {@code true} if the dialog should include an option to set the selection + * as the default. False otherwise. + * @param accountHandles The {@code PhoneAccountHandle}s available to select from. + * @param listener The listener for the results of the account selection. + */ + public static SelectPhoneAccountDialogFragment newInstance( + int titleResId, + boolean canSetDefault, + List accountHandles, + SelectPhoneAccountListener listener, + @Nullable String callId) { + ArrayList accountHandlesCopy = new ArrayList<>(); + if (accountHandles != null) { + accountHandlesCopy.addAll(accountHandles); + } + SelectPhoneAccountDialogFragment fragment = new SelectPhoneAccountDialogFragment(); + final Bundle args = new Bundle(); + args.putInt(ARG_TITLE_RES_ID, titleResId); + args.putBoolean(ARG_CAN_SET_DEFAULT, canSetDefault); + args.putParcelableArrayList(ARG_ACCOUNT_HANDLES, accountHandlesCopy); + args.putParcelable(ARG_LISTENER, listener); + args.putString(ARG_CALL_ID, callId); + fragment.setArguments(args); + fragment.setListener(listener); + return fragment; + } + + public void setListener(SelectPhoneAccountListener listener) { + mListener = listener; + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putBoolean(ARG_IS_DEFAULT_CHECKED, mIsDefaultChecked); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + mTitleResId = getArguments().getInt(ARG_TITLE_RES_ID); + mCanSetDefault = getArguments().getBoolean(ARG_CAN_SET_DEFAULT); + mAccountHandles = getArguments().getParcelableArrayList(ARG_ACCOUNT_HANDLES); + mListener = getArguments().getParcelable(ARG_LISTENER); + if (savedInstanceState != null) { + mIsDefaultChecked = savedInstanceState.getBoolean(ARG_IS_DEFAULT_CHECKED); + } + mIsSelected = false; + + final DialogInterface.OnClickListener selectionListener = + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + mIsSelected = true; + PhoneAccountHandle selectedAccountHandle = mAccountHandles.get(which); + Bundle result = new Bundle(); + result.putParcelable( + SelectPhoneAccountListener.EXTRA_SELECTED_ACCOUNT_HANDLE, selectedAccountHandle); + result.putBoolean(SelectPhoneAccountListener.EXTRA_SET_DEFAULT, mIsDefaultChecked); + result.putString(SelectPhoneAccountListener.EXTRA_CALL_ID, getCallId()); + if (mListener != null) { + mListener.onReceiveResult(SelectPhoneAccountListener.RESULT_SELECTED, result); + } + } + }; + + final CompoundButton.OnCheckedChangeListener checkListener = + new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton check, boolean isChecked) { + mIsDefaultChecked = isChecked; + } + }; + + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + ListAdapter selectAccountListAdapter = + new SelectAccountListAdapter( + builder.getContext(), R.layout.select_account_list_item, mAccountHandles); + + AlertDialog dialog = + builder + .setTitle(mTitleResId) + .setAdapter(selectAccountListAdapter, selectionListener) + .create(); + + if (mCanSetDefault) { + // Generate custom checkbox view, lint suppressed since no appropriate parent (is dialog) + @SuppressLint("InflateParams") + LinearLayout checkboxLayout = + (LinearLayout) + LayoutInflater.from(builder.getContext()) + .inflate(R.layout.default_account_checkbox, null); + + CheckBox cb = (CheckBox) checkboxLayout.findViewById(R.id.default_account_checkbox_view); + cb.setOnCheckedChangeListener(checkListener); + cb.setChecked(mIsDefaultChecked); + + dialog.getListView().addFooterView(checkboxLayout); + } + + return dialog; + } + + @Override + public void onStop() { + if (!mIsSelected && mListener != null) { + Bundle result = new Bundle(); + result.putString(SelectPhoneAccountListener.EXTRA_CALL_ID, getCallId()); + mListener.onReceiveResult(SelectPhoneAccountListener.RESULT_DISMISSED, result); + } + super.onStop(); + } + + @Nullable + private String getCallId() { + return getArguments().getString(ARG_CALL_ID); + } + + public static class SelectPhoneAccountListener extends ResultReceiver { + + static final int RESULT_SELECTED = 1; + static final int RESULT_DISMISSED = 2; + + static final String EXTRA_SELECTED_ACCOUNT_HANDLE = "extra_selected_account_handle"; + static final String EXTRA_SET_DEFAULT = "extra_set_default"; + static final String EXTRA_CALL_ID = "extra_call_id"; + + public SelectPhoneAccountListener() { + super(new Handler()); + } + + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + if (resultCode == RESULT_SELECTED) { + onPhoneAccountSelected( + resultData.getParcelable(EXTRA_SELECTED_ACCOUNT_HANDLE), + resultData.getBoolean(EXTRA_SET_DEFAULT), + resultData.getString(EXTRA_CALL_ID)); + } else if (resultCode == RESULT_DISMISSED) { + onDialogDismissed(resultData.getString(EXTRA_CALL_ID)); + } + } + + public void onPhoneAccountSelected( + PhoneAccountHandle selectedAccountHandle, boolean setDefault, @Nullable String callId) {} + + public void onDialogDismissed(@Nullable String callId) {} + } + + private static class SelectAccountListAdapter extends ArrayAdapter { + + private int mResId; + + public SelectAccountListAdapter( + Context context, int resource, List accountHandles) { + super(context, resource, accountHandles); + mResId = resource; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + LayoutInflater inflater = + (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); + + View rowView; + final ViewHolder holder; + + if (convertView == null) { + // Cache views for faster scrolling + rowView = inflater.inflate(mResId, null); + holder = new ViewHolder(); + holder.labelTextView = (TextView) rowView.findViewById(R.id.label); + holder.numberTextView = (TextView) rowView.findViewById(R.id.number); + holder.imageView = (ImageView) rowView.findViewById(R.id.icon); + rowView.setTag(holder); + } else { + rowView = convertView; + holder = (ViewHolder) rowView.getTag(); + } + + PhoneAccountHandle accountHandle = getItem(position); + PhoneAccount account = + getContext().getSystemService(TelecomManager.class).getPhoneAccount(accountHandle); + if (account == null) { + return rowView; + } + holder.labelTextView.setText(account.getLabel()); + if (account.getAddress() == null + || TextUtils.isEmpty(account.getAddress().getSchemeSpecificPart())) { + holder.numberTextView.setVisibility(View.GONE); + } else { + holder.numberTextView.setVisibility(View.VISIBLE); + holder.numberTextView.setText( + PhoneNumberUtilsCompat.createTtsSpannable( + account.getAddress().getSchemeSpecificPart())); + } + holder.imageView.setImageDrawable( + PhoneAccountCompat.createIconDrawable(account, getContext())); + return rowView; + } + + private static final class ViewHolder { + + TextView labelTextView; + TextView numberTextView; + ImageView imageView; + } + } +} diff --git a/java/com/android/dialer/animation/AnimUtils.java b/java/com/android/dialer/animation/AnimUtils.java new file mode 100644 index 000000000..9c9396e56 --- /dev/null +++ b/java/com/android/dialer/animation/AnimUtils.java @@ -0,0 +1,247 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.dialer.animation; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.view.View; +import android.view.ViewPropertyAnimator; +import android.view.animation.Interpolator; +import com.android.dialer.compat.PathInterpolatorCompat; + +public class AnimUtils { + + public static final int DEFAULT_DURATION = -1; + public static final int NO_DELAY = 0; + + public static final Interpolator EASE_IN = PathInterpolatorCompat.create(0.0f, 0.0f, 0.2f, 1.0f); + public static final Interpolator EASE_OUT = PathInterpolatorCompat.create(0.4f, 0.0f, 1.0f, 1.0f); + public static final Interpolator EASE_OUT_EASE_IN = + PathInterpolatorCompat.create(0.4f, 0, 0.2f, 1); + + public static void crossFadeViews(View fadeIn, View fadeOut, int duration) { + fadeIn(fadeIn, duration); + fadeOut(fadeOut, duration); + } + + public static void fadeOut(View fadeOut, int duration) { + fadeOut(fadeOut, duration, null); + } + + public static void fadeOut(final View fadeOut, int durationMs, final AnimationCallback callback) { + fadeOut.setAlpha(1); + final ViewPropertyAnimator animator = fadeOut.animate(); + animator.cancel(); + animator + .alpha(0) + .withLayer() + .setListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + fadeOut.setVisibility(View.GONE); + if (callback != null) { + callback.onAnimationEnd(); + } + } + + @Override + public void onAnimationCancel(Animator animation) { + fadeOut.setVisibility(View.GONE); + fadeOut.setAlpha(0); + if (callback != null) { + callback.onAnimationCancel(); + } + } + }); + if (durationMs != DEFAULT_DURATION) { + animator.setDuration(durationMs); + } + animator.start(); + } + + public static void fadeIn(View fadeIn, int durationMs) { + fadeIn(fadeIn, durationMs, NO_DELAY, null); + } + + public static void fadeIn( + final View fadeIn, int durationMs, int delay, final AnimationCallback callback) { + fadeIn.setAlpha(0); + final ViewPropertyAnimator animator = fadeIn.animate(); + animator.cancel(); + + animator.setStartDelay(delay); + animator + .alpha(1) + .withLayer() + .setListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + fadeIn.setVisibility(View.VISIBLE); + } + + @Override + public void onAnimationCancel(Animator animation) { + fadeIn.setAlpha(1); + if (callback != null) { + callback.onAnimationCancel(); + } + } + + @Override + public void onAnimationEnd(Animator animation) { + if (callback != null) { + callback.onAnimationEnd(); + } + } + }); + if (durationMs != DEFAULT_DURATION) { + animator.setDuration(durationMs); + } + animator.start(); + } + + /** + * Scales in the view from scale of 0 to actual dimensions. + * + * @param view The view to scale. + * @param durationMs The duration of the scaling in milliseconds. + * @param startDelayMs The delay to applying the scaling in milliseconds. + */ + public static void scaleIn(final View view, int durationMs, int startDelayMs) { + AnimatorListenerAdapter listener = + (new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + view.setVisibility(View.VISIBLE); + } + + @Override + public void onAnimationCancel(Animator animation) { + view.setScaleX(1); + view.setScaleY(1); + } + }); + scaleInternal( + view, + 0 /* startScaleValue */, + 1 /* endScaleValue */, + durationMs, + startDelayMs, + listener, + EASE_IN); + } + + /** + * Scales out the view from actual dimensions to 0. + * + * @param view The view to scale. + * @param durationMs The duration of the scaling in milliseconds. + */ + public static void scaleOut(final View view, int durationMs) { + AnimatorListenerAdapter listener = + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + view.setVisibility(View.GONE); + } + + @Override + public void onAnimationCancel(Animator animation) { + view.setVisibility(View.GONE); + view.setScaleX(0); + view.setScaleY(0); + } + }; + + scaleInternal( + view, + 1 /* startScaleValue */, + 0 /* endScaleValue */, + durationMs, + NO_DELAY, + listener, + EASE_OUT); + } + + private static void scaleInternal( + final View view, + int startScaleValue, + int endScaleValue, + int durationMs, + int startDelay, + AnimatorListenerAdapter listener, + Interpolator interpolator) { + view.setScaleX(startScaleValue); + view.setScaleY(startScaleValue); + + final ViewPropertyAnimator animator = view.animate(); + animator.cancel(); + + animator + .setInterpolator(interpolator) + .scaleX(endScaleValue) + .scaleY(endScaleValue) + .setListener(listener) + .withLayer(); + + if (durationMs != DEFAULT_DURATION) { + animator.setDuration(durationMs); + } + animator.setStartDelay(startDelay); + + animator.start(); + } + + /** + * Animates a view to the new specified dimensions. + * + * @param view The view to change the dimensions of. + * @param newWidth The new width of the view. + * @param newHeight The new height of the view. + */ + public static void changeDimensions(final View view, final int newWidth, final int newHeight) { + ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f); + + final int oldWidth = view.getWidth(); + final int oldHeight = view.getHeight(); + final int deltaWidth = newWidth - oldWidth; + final int deltaHeight = newHeight - oldHeight; + + animator.addUpdateListener( + new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animator) { + Float value = (Float) animator.getAnimatedValue(); + + view.getLayoutParams().width = (int) (value * deltaWidth + oldWidth); + view.getLayoutParams().height = (int) (value * deltaHeight + oldHeight); + view.requestLayout(); + } + }); + animator.start(); + } + + public static class AnimationCallback { + + public void onAnimationEnd() {} + + public void onAnimationCancel() {} + } +} diff --git a/java/com/android/dialer/animation/AnimationListenerAdapter.java b/java/com/android/dialer/animation/AnimationListenerAdapter.java new file mode 100644 index 000000000..3f847f2b6 --- /dev/null +++ b/java/com/android/dialer/animation/AnimationListenerAdapter.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.dialer.animation; + +import android.view.animation.Animation; +import android.view.animation.Animation.AnimationListener; + +/** + * Provides empty implementations of the methods in {@link AnimationListener} for convenience + * reasons. + */ +public class AnimationListenerAdapter implements AnimationListener { + + /** {@inheritDoc} */ + @Override + public void onAnimationStart(Animation animation) {} + + /** {@inheritDoc} */ + @Override + public void onAnimationEnd(Animation animation) {} + + /** {@inheritDoc} */ + @Override + public void onAnimationRepeat(Animation animation) {} +} diff --git a/java/com/android/dialer/app/AndroidManifest.xml b/java/com/android/dialer/app/AndroidManifest.xml new file mode 100644 index 000000000..80f294acc --- /dev/null +++ b/java/com/android/dialer/app/AndroidManifest.xml @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com/android/dialer/app/Bindings.java b/java/com/android/dialer/app/Bindings.java new file mode 100644 index 000000000..2beb40184 --- /dev/null +++ b/java/com/android/dialer/app/Bindings.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.app; + +import android.content.Context; +import com.android.dialer.app.bindings.DialerBindings; +import com.android.dialer.app.bindings.DialerBindingsFactory; +import com.android.dialer.app.bindings.DialerBindingsStub; +import com.android.dialer.app.legacybindings.DialerLegacyBindings; +import com.android.dialer.app.legacybindings.DialerLegacyBindingsFactory; +import com.android.dialer.app.legacybindings.DialerLegacyBindingsStub; +import java.util.Objects; + +/** Accessor for the in call UI bindings. */ +public class Bindings { + + private static DialerBindings instance; + private static DialerLegacyBindings legacyInstance; + + private Bindings() {} + + public static DialerBindings get(Context context) { + Objects.requireNonNull(context); + if (instance != null) { + return instance; + } + + Context application = context.getApplicationContext(); + if (application instanceof DialerBindingsFactory) { + instance = ((DialerBindingsFactory) application).newDialerBindings(); + } + + if (instance == null) { + instance = new DialerBindingsStub(); + } + return instance; + } + + public static DialerLegacyBindings getLegacy(Context context) { + Objects.requireNonNull(context); + if (legacyInstance != null) { + return legacyInstance; + } + + Context application = context.getApplicationContext(); + if (application instanceof DialerLegacyBindingsFactory) { + legacyInstance = ((DialerLegacyBindingsFactory) application).newDialerLegacyBindings(); + } + + if (legacyInstance == null) { + legacyInstance = new DialerLegacyBindingsStub(); + } + return legacyInstance; + } + + public static void setForTesting(DialerBindings testInstance) { + instance = testInstance; + } + + public static void setLegacyBindingForTesting(DialerLegacyBindings testLegacyInstance) { + legacyInstance = testLegacyInstance; + } +} diff --git a/java/com/android/dialer/app/CallDetailActivity.java b/java/com/android/dialer/app/CallDetailActivity.java new file mode 100644 index 000000000..cda2b2e2c --- /dev/null +++ b/java/com/android/dialer/app/CallDetailActivity.java @@ -0,0 +1,480 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.app; + +import android.content.ContentUris; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.support.v7.app.AppCompatActivity; +import android.text.BidiFormatter; +import android.text.TextDirectionHeuristics; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.widget.ListView; +import android.widget.QuickContactBadge; +import android.widget.TextView; +import android.widget.Toast; +import com.android.contacts.common.ClipboardUtils; +import com.android.contacts.common.ContactPhotoManager; +import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest; +import com.android.contacts.common.GeoUtil; +import com.android.contacts.common.preference.ContactsPreferences; +import com.android.contacts.common.util.UriUtils; +import com.android.dialer.app.calllog.CallDetailHistoryAdapter; +import com.android.dialer.app.calllog.CallLogAsyncTaskUtil; +import com.android.dialer.app.calllog.CallLogAsyncTaskUtil.CallLogAsyncTaskListener; +import com.android.dialer.app.calllog.CallTypeHelper; +import com.android.dialer.app.calllog.PhoneAccountUtils; +import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler; +import com.android.dialer.callintent.CallIntentBuilder; +import com.android.dialer.callintent.nano.CallInitiationType; +import com.android.dialer.common.Assert; +import com.android.dialer.common.AsyncTaskExecutor; +import com.android.dialer.common.AsyncTaskExecutors; +import com.android.dialer.compat.CompatUtils; +import com.android.dialer.logging.Logger; +import com.android.dialer.logging.nano.DialerImpression; +import com.android.dialer.logging.nano.ScreenEvent; +import com.android.dialer.phonenumbercache.ContactInfoHelper; +import com.android.dialer.phonenumberutil.PhoneNumberHelper; +import com.android.dialer.proguard.UsedByReflection; +import com.android.dialer.spam.Spam; +import com.android.dialer.telecom.TelecomUtil; +import com.android.dialer.util.CallUtil; +import com.android.dialer.util.DialerUtils; +import com.android.dialer.util.TouchPointManager; + +/** + * Displays the details of a specific call log entry. + * + *

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

It handles the following actions: + * + *

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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