summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorXin Li <delphij@google.com>2019-09-04 13:34:22 -0700
committerXin Li <delphij@google.com>2019-09-04 13:34:22 -0700
commit3a49f5e18afc0760300ccfab92c2253e2e781dba (patch)
treecf14dae4f78428217400aeea089d937f131d5624
parent0e196344041cb7a94c4b68bf9283ec1315bca80d (diff)
parentf6167a627d370fe6376ebe63acef77b6091e0ff1 (diff)
downloadplatform_packages_apps_Car_Dialer-ndk-sysroot-r21.tar.gz
platform_packages_apps_Car_Dialer-ndk-sysroot-r21.tar.bz2
platform_packages_apps_Car_Dialer-ndk-sysroot-r21.zip
DO NOT MERGE - Merge Android 10 into masterndk-sysroot-r21
Bug: 139893257 Change-Id: Ie70fb2f2073293b4ac3e4590cb4e62baf8274f58
-rw-r--r--Android.bp45
-rw-r--r--Android.mk128
-rw-r--r--AndroidManifest.xml84
-rw-r--r--PREUPLOAD.cfg8
-rw-r--r--res/anim/telecom_fade_in.xml23
-rw-r--r--res/anim/telecom_slide_in_with_delay.xml23
-rw-r--r--res/anim/telecom_slide_out.xml22
-rw-r--r--res/animator/fade_in.xml23
-rw-r--r--res/animator/fade_out.xml23
-rw-r--r--res/animator/scale_down.xml25
-rw-r--r--res/color/icon_accent_activatable.xml (renamed from res/values-w768dp/dimens.xml)12
-rw-r--r--res/color/icon_tint_state_list.xml (renamed from res/menu/options_menu.xml)20
-rw-r--r--res/color/text_accent_activatable.xml (renamed from res/values-h480dp/dimens.xml)11
-rw-r--r--res/drawable-hdpi/car_ic_bluetooth_disable_large.pngbin1614 -> 0 bytes
-rw-r--r--res/drawable-hdpi/ic_123_activated.pngbin4352 -> 0 bytes
-rw-r--r--res/drawable-hdpi/ic_123_normal.pngbin1866 -> 0 bytes
-rw-r--r--res/drawable-hdpi/ic_arrow_drop_down.pngbin572 -> 0 bytes
-rw-r--r--res/drawable-hdpi/ic_call_merge.pngbin1036 -> 0 bytes
-rw-r--r--res/drawable-hdpi/ic_call_voicemail.pngbin1029 -> 0 bytes
-rw-r--r--res/drawable-hdpi/ic_drawer_call_missed.pngbin960 -> 0 bytes
-rw-r--r--res/drawable-hdpi/ic_drawer_dialpad.pngbin2162 -> 0 bytes
-rw-r--r--res/drawable-hdpi/ic_drawer_history.pngbin2220 -> 0 bytes
-rw-r--r--res/drawable-hdpi/ic_empty_speed_dial.pngbin9850 -> 0 bytes
-rw-r--r--res/drawable-hdpi/ic_favorite.pngbin1027 -> 0 bytes
-rw-r--r--res/drawable-hdpi/ic_list_view_disable.pngbin2033 -> 0 bytes
-rw-r--r--res/drawable-hdpi/ic_pause.pngbin633 -> 0 bytes
-rw-r--r--res/drawable-hdpi/ic_swap_calls.pngbin1440 -> 0 bytes
-rw-r--r--res/drawable-mdpi/car_ic_bluetooth_disable_large.pngbin1141 -> 0 bytes
-rw-r--r--res/drawable-mdpi/ic_123_activated.pngbin2786 -> 0 bytes
-rw-r--r--res/drawable-mdpi/ic_123_normal.pngbin1294 -> 0 bytes
-rw-r--r--res/drawable-mdpi/ic_arrow_drop_down.pngbin546 -> 0 bytes
-rw-r--r--res/drawable-mdpi/ic_call_merge.pngbin883 -> 0 bytes
-rw-r--r--res/drawable-mdpi/ic_call_voicemail.pngbin743 -> 0 bytes
-rw-r--r--res/drawable-mdpi/ic_drawer_call_missed.pngbin845 -> 0 bytes
-rw-r--r--res/drawable-mdpi/ic_drawer_dialpad.pngbin1537 -> 0 bytes
-rw-r--r--res/drawable-mdpi/ic_drawer_history.pngbin1566 -> 0 bytes
-rw-r--r--res/drawable-mdpi/ic_empty_speed_dial.pngbin6424 -> 0 bytes
-rw-r--r--res/drawable-mdpi/ic_favorite.pngbin771 -> 0 bytes
-rw-r--r--res/drawable-mdpi/ic_list_view_disable.pngbin1739 -> 0 bytes
-rw-r--r--res/drawable-mdpi/ic_pause.pngbin549 -> 0 bytes
-rw-r--r--res/drawable-mdpi/ic_swap_calls.pngbin1068 -> 0 bytes
-rw-r--r--res/drawable-night-hdpi/ic_123_activated.pngbin4171 -> 0 bytes
-rw-r--r--res/drawable-night-hdpi/ic_123_normal.pngbin1878 -> 0 bytes
-rw-r--r--res/drawable-night-mdpi/ic_123_activated.pngbin2699 -> 0 bytes
-rw-r--r--res/drawable-night-mdpi/ic_123_normal.pngbin1303 -> 0 bytes
-rw-r--r--res/drawable-night-xhdpi/ic_123_activated.pngbin6927 -> 0 bytes
-rw-r--r--res/drawable-night-xhdpi/ic_123_normal.pngbin2668 -> 0 bytes
-rw-r--r--res/drawable-night-xxhdpi/ic_123_activated.pngbin4178 -> 0 bytes
-rw-r--r--res/drawable-night-xxhdpi/ic_123_normal.pngbin1566 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/car_ic_bluetooth_disable_large.pngbin2605 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/ic_123_activated.pngbin7175 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/ic_123_normal.pngbin2684 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/ic_arrow_drop_down.pngbin665 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/ic_call_merge.pngbin1361 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/ic_call_voicemail.pngbin1493 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/ic_drawer_call_missed.pngbin1243 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/ic_drawer_dialpad.pngbin2984 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/ic_drawer_history.pngbin3001 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/ic_empty_speed_dial.pngbin13422 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/ic_favorite.pngbin1365 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/ic_list_view_disable.pngbin2697 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/ic_pause.pngbin686 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/ic_swap_calls.pngbin1850 -> 0 bytes
-rw-r--r--res/drawable-xxhdpi/car_ic_bluetooth_disable_large.pngbin2132 -> 0 bytes
-rw-r--r--res/drawable-xxhdpi/ic_123_activated.pngbin4361 -> 0 bytes
-rw-r--r--res/drawable-xxhdpi/ic_123_normal.pngbin1562 -> 0 bytes
-rw-r--r--res/drawable-xxhdpi/ic_arrow_drop_down.pngbin290 -> 0 bytes
-rw-r--r--res/drawable-xxhdpi/ic_call_merge.pngbin715 -> 0 bytes
-rw-r--r--res/drawable-xxhdpi/ic_call_voicemail.pngbin2071 -> 0 bytes
-rw-r--r--res/drawable-xxhdpi/ic_drawer_call_missed.pngbin625 -> 0 bytes
-rw-r--r--res/drawable-xxhdpi/ic_drawer_dialpad.pngbin1389 -> 0 bytes
-rw-r--r--res/drawable-xxhdpi/ic_drawer_history.pngbin1804 -> 0 bytes
-rw-r--r--res/drawable-xxhdpi/ic_empty_speed_dial.pngbin8639 -> 0 bytes
-rw-r--r--res/drawable-xxhdpi/ic_favorite.pngbin3751 -> 0 bytes
-rw-r--r--res/drawable-xxhdpi/ic_list_view_disable.pngbin1502 -> 0 bytes
-rw-r--r--res/drawable-xxhdpi/ic_pause.pngbin253 -> 0 bytes
-rw-r--r--res/drawable-xxhdpi/ic_swap_calls.pngbin831 -> 0 bytes
-rw-r--r--res/drawable/action_button_background.xml27
-rw-r--r--res/drawable/avatar_rounded_bg.xml20
-rw-r--r--res/drawable/button_active_state_circle.xml (renamed from res/drawable/ic_call_state_switch.xml)20
-rw-r--r--res/drawable/contact_photo_gradient.xml21
-rw-r--r--res/drawable/dialer_ripple_background.xml2
-rw-r--r--res/drawable/dialpad_delete_button_background.xml17
-rw-r--r--res/drawable/hero_button_background.xml (renamed from res/drawable/in_call_card_background.xml)16
-rw-r--r--res/drawable/ic_add_favorite.xml39
-rw-r--r--res/drawable/ic_app_icon.xml47
-rw-r--r--res/drawable/ic_arrow_back.xml5
-rw-r--r--res/drawable/ic_arrow_right.xml (renamed from res/drawable/ic_arrow_down.xml)18
-rw-r--r--res/drawable/ic_avatar_bg.xml16
-rw-r--r--res/drawable/ic_bluetooth.xml29
-rw-r--r--res/drawable/ic_bluetooth_activatable.xml (renamed from res/drawable/ic_dialpad_activated.xml)15
-rw-r--r--res/drawable/ic_bluetooth_disable.xml24
-rw-r--r--res/drawable/ic_call_end.xml18
-rw-r--r--res/drawable/ic_call_end_button.xml33
-rw-r--r--res/drawable/ic_call_made.xml4
-rw-r--r--res/drawable/ic_call_missed.xml4
-rw-r--r--res/drawable/ic_call_received.xml4
-rw-r--r--res/drawable/ic_cancel.xml24
-rw-r--r--res/drawable/ic_contact.xml6
-rw-r--r--res/drawable/ic_dialpad.xml18
-rw-r--r--res/drawable/ic_dialpad_activatable.xml (renamed from res/anim/telecom_slide_in.xml)19
-rw-r--r--res/drawable/ic_dialpad_normal.xml21
-rw-r--r--res/drawable/ic_favorite.xml6
-rw-r--r--res/drawable/ic_favorite_activatable.xml (renamed from res/values-sw400dp/dimens.xml)12
-rw-r--r--res/drawable/ic_favorite_empty.xml (renamed from res/drawable/ic_down_outlined.xml)10
-rw-r--r--res/drawable/ic_history.xml24
-rw-r--r--res/drawable/ic_mute.xml18
-rw-r--r--res/drawable/ic_mute_activatable.xml (renamed from res/drawable/ic_mute_call_activated.xml)17
-rw-r--r--res/drawable/ic_mute_call_normal.xml21
-rw-r--r--res/drawable/ic_overflow.xml23
-rw-r--r--res/drawable/ic_overflow_vd.xml26
-rw-r--r--res/drawable/ic_pause.xml10
-rw-r--r--res/drawable/ic_pause_activatable.xml (renamed from res/anim/telecom_fade_out.xml)20
-rw-r--r--res/drawable/ic_phone.xml18
-rw-r--r--res/drawable/ic_search.xml6
-rw-r--r--res/drawable/ic_setting.xml36
-rw-r--r--res/drawable/ic_smartphone.xml31
-rw-r--r--res/drawable/ic_smartphone_activatable.xml25
-rw-r--r--res/drawable/ic_speaker_phone.xml9
-rw-r--r--res/drawable/ic_speaker_phone_activatable.xml25
-rw-r--r--res/drawable/ic_swap_calls.xml (renamed from res/drawable/dialpad_button_background.xml)15
-rw-r--r--res/drawable/ic_voicemail.xml10
-rw-r--r--res/drawable/icon_call_button.xml33
-rw-r--r--res/drawable/list_divider.xml10
-rw-r--r--res/drawable/logo_avatar.xml27
-rw-r--r--res/drawable/sized_logo.xml29
-rw-r--r--res/drawable/strequent_small_icon_bg.xml19
-rw-r--r--res/layout-h610dp/contact_details_action_bar.xml (renamed from res/drawable/ongoing_call_action_background.xml)11
-rw-r--r--res/layout-port/dialer_fragment.xml106
-rw-r--r--res/layout-port/dialpad_fragment.xml79
-rw-r--r--res/layout-port/in_call_fragment.xml53
-rw-r--r--res/layout-port/incall_dialpad_fragment.xml65
-rw-r--r--res/layout-port/top_bar_with_tabs.xml34
-rw-r--r--res/layout-port/user_profile_large.xml45
-rw-r--r--res/layout/audio_route_list_item.xml18
-rw-r--r--res/layout/audio_route_switch_dialog.xml18
-rw-r--r--res/layout/call_history_list_item.xml104
-rw-r--r--res/layout/call_log_list_item_card.xml29
-rw-r--r--res/layout/call_log_list_item_card_base.xml101
-rw-r--r--res/layout/call_log_list_item_empty.xml45
-rw-r--r--res/layout/call_type_icons.xml41
-rw-r--r--res/layout/contact_detail_card_content.xml55
-rw-r--r--res/layout/contact_detail_name_image.xml74
-rw-r--r--res/layout/contact_details.xml31
-rw-r--r--res/layout/contact_details_action_bar.xml (renamed from res/layout/call_list_fragment.xml)28
-rw-r--r--res/layout/contact_details_name_image.xml45
-rw-r--r--res/layout/contact_details_number.xml73
-rw-r--r--res/layout/contact_list_fragment.xml74
-rw-r--r--res/layout/contact_list_item.xml102
-rw-r--r--res/layout/contact_result.xml63
-rw-r--r--res/layout/contact_search_activity.xml105
-rw-r--r--res/layout/dialer_fragment.xml63
-rw-r--r--res/layout/dialer_info_fragment.xml91
-rw-r--r--res/layout/dialer_settings_activity.xml39
-rw-r--r--res/layout/dialpad.xml135
-rw-r--r--res/layout/dialpad_fragment.xml49
-rw-r--r--res/layout/dialpad_info.xml70
-rw-r--r--res/layout/favorite_contact_list_item.xml45
-rw-r--r--res/layout/favorite_fragment.xml55
-rw-r--r--res/layout/in_call_activity.xml (renamed from res/layout/contact_result_fragment.xml)19
-rw-r--r--res/layout/in_call_fragment.xml50
-rw-r--r--res/layout/incall_dialpad_fragment.xml78
-rw-r--r--res/layout/incoming_call_fragment.xml51
-rw-r--r--res/layout/keypad.xml201
-rw-r--r--res/layout/keypad_button.xml (renamed from res/layout/dialpad_button.xml)15
-rw-r--r--res/layout/list_fragment.xml22
-rw-r--r--res/layout/menu_action_view.xml30
-rw-r--r--res/layout/no_hfp.xml63
-rw-r--r--res/layout/on_going_call_controller_bar_fragment.xml54
-rw-r--r--res/layout/ongoing_call.xml240
-rw-r--r--res/layout/ongoing_call_fragment.xml83
-rw-r--r--res/layout/onhold_user_profile.xml80
-rw-r--r--res/layout/phone_number_list_item.xml43
-rw-r--r--res/layout/ringing_call_controller_bar_fragment.xml23
-rw-r--r--res/layout/search_view.xml (renamed from res/drawable/car_list_item_background.xml)18
-rw-r--r--res/layout/strequents_fragment.xml31
-rw-r--r--res/layout/telecom_activity.xml33
-rw-r--r--res/layout/top_bar_with_tabs.xml28
-rw-r--r--res/layout/user_profile_large.xml59
-rw-r--r--res/menu/contact_edit.xml24
-rw-r--r--res/menu/main_menu.xml31
-rw-r--r--res/values-h1200dp/bools.xml20
-rw-r--r--res/values-h1200dp/dimens.xml25
-rw-r--r--res/values-h420dp/dimens.xml21
-rw-r--r--res/values-h600dp/dimens.xml20
-rw-r--r--res/values-h610dp/bools.xml20
-rw-r--r--res/values-h610dp/dimens.xml28
-rw-r--r--res/values-h720dp/dimens.xml23
-rw-r--r--res/values-night/colors.xml15
-rw-r--r--res/values-port/dimens.xml (renamed from res/values-w748dp/dimens.xml)10
-rw-r--r--res/values-port/strings.xml22
-rw-r--r--res/values-w1024dp/dimens.xml40
-rw-r--r--res/values-w1200dp/dimens.xml20
-rw-r--r--res/values-wheel/bools.xml17
-rw-r--r--res/values-wheel/colors.xml17
-rw-r--r--res/values/arrays.xml50
-rw-r--r--res/values/attrs.xml6
-rw-r--r--res/values/bools.xml (renamed from res/drawable/rotary_in_call_dialpad_button_background.xml)7
-rw-r--r--res/values/colors.xml76
-rw-r--r--res/values/configs.xml22
-rw-r--r--res/values/dimens.xml204
-rw-r--r--res/values/integer.xml2
-rw-r--r--res/values/strings.xml183
-rw-r--r--res/values/styles.xml117
-rw-r--r--res/values/styles_preference.xml (renamed from res/drawable/ic_play.xml)19
-rw-r--r--res/values/themes.xml29
-rw-r--r--res/xml/searchable.xml (renamed from res/drawable/ongoing_call_secondary_action_background.xml)10
-rw-r--r--res/xml/settings_page.xml33
-rw-r--r--src/com/android/car/dialer/BitmapWorkerTask.java118
-rw-r--r--src/com/android/car/dialer/CallListener.java16
-rw-r--r--src/com/android/car/dialer/CallLogViewHolder.java55
-rw-r--r--src/com/android/car/dialer/CallTypeIconsView.java132
-rw-r--r--src/com/android/car/dialer/Constants.java43
-rw-r--r--src/com/android/car/dialer/ContactDetailsFragment.java338
-rw-r--r--src/com/android/car/dialer/ContactEntry.java274
-rw-r--r--src/com/android/car/dialer/ContactResultViewHolder.java126
-rw-r--r--src/com/android/car/dialer/ContactResultsFragment.java196
-rw-r--r--src/com/android/car/dialer/ContactSearchActivity.java274
-rw-r--r--src/com/android/car/dialer/DialerApplication.java38
-rw-r--r--src/com/android/car/dialer/DialerFragment.java93
-rw-r--r--src/com/android/car/dialer/OngoingCallFragment.java667
-rw-r--r--src/com/android/car/dialer/StrequentsAdapter.java421
-rw-r--r--src/com/android/car/dialer/StrequentsFragment.java276
-rw-r--r--src/com/android/car/dialer/TelecomActivity.java576
-rw-r--r--src/com/android/car/dialer/TelecomIntents.java40
-rw-r--r--src/com/android/car/dialer/UiBluetoothMonitor.java116
-rw-r--r--src/com/android/car/dialer/livedata/AudioRouteLiveData.java70
-rw-r--r--src/com/android/car/dialer/livedata/BluetoothHfpStateLiveData.java83
-rw-r--r--src/com/android/car/dialer/livedata/BluetoothPairListLiveData.java78
-rw-r--r--src/com/android/car/dialer/livedata/BluetoothStateLiveData.java95
-rw-r--r--src/com/android/car/dialer/livedata/CallDetailLiveData.java94
-rw-r--r--src/com/android/car/dialer/livedata/CallHistoryLiveData.java130
-rw-r--r--src/com/android/car/dialer/livedata/CallStateLiveData.java88
-rw-r--r--src/com/android/car/dialer/livedata/ContactDetailsLiveData.java98
-rw-r--r--src/com/android/car/dialer/livedata/FavoriteContactLiveData.java80
-rw-r--r--src/com/android/car/dialer/livedata/HeartBeatLiveData.java58
-rw-r--r--src/com/android/car/dialer/livedata/SharedPreferencesLiveData.java75
-rw-r--r--src/com/android/car/dialer/livedata/UnreadMissedCallLiveData.java85
-rw-r--r--src/com/android/car/dialer/log/L.java80
-rw-r--r--src/com/android/car/dialer/notification/InCallNotificationController.java153
-rw-r--r--src/com/android/car/dialer/notification/MissedCallNotificationController.java213
-rw-r--r--src/com/android/car/dialer/notification/MissedCallReceiver.java52
-rw-r--r--src/com/android/car/dialer/notification/NotificationReceiver.java30
-rw-r--r--src/com/android/car/dialer/notification/NotificationService.java106
-rw-r--r--src/com/android/car/dialer/notification/NotificationUtils.java88
-rw-r--r--src/com/android/car/dialer/telecom/ContactBitmapWorker.java119
-rw-r--r--src/com/android/car/dialer/telecom/InCallRouter.java143
-rw-r--r--src/com/android/car/dialer/telecom/InCallServiceImpl.java118
-rw-r--r--src/com/android/car/dialer/telecom/InMemoryPhoneBook.java122
-rw-r--r--src/com/android/car/dialer/telecom/PhoneLoader.java257
-rw-r--r--src/com/android/car/dialer/telecom/ProjectionCallHandler.java185
-rw-r--r--src/com/android/car/dialer/telecom/TelecomUtils.java374
-rw-r--r--src/com/android/car/dialer/telecom/UiBluetoothMonitor.java102
-rw-r--r--src/com/android/car/dialer/telecom/UiCall.java97
-rw-r--r--src/com/android/car/dialer/telecom/UiCallManager.java631
-rw-r--r--src/com/android/car/dialer/ui/CallHistoryFragment.java81
-rw-r--r--src/com/android/car/dialer/ui/CallHistoryListItemProvider.java60
-rw-r--r--src/com/android/car/dialer/ui/CallLogListingTask.java182
-rw-r--r--src/com/android/car/dialer/ui/ContactListFragment.java210
-rw-r--r--src/com/android/car/dialer/ui/ContactListItemProvider.java77
-rw-r--r--src/com/android/car/dialer/ui/DialerInfoController.java172
-rw-r--r--src/com/android/car/dialer/ui/DialpadFragment.java223
-rw-r--r--src/com/android/car/dialer/ui/InCallFragment.java186
-rw-r--r--src/com/android/car/dialer/ui/OnGoingCallControllerBarFragment.java277
-rw-r--r--src/com/android/car/dialer/ui/TelecomActivity.java436
-rw-r--r--src/com/android/car/dialer/ui/TelecomActivityViewModel.java211
-rw-r--r--src/com/android/car/dialer/ui/TelecomPageTab.java165
-rw-r--r--src/com/android/car/dialer/ui/activecall/InCallActivity.java105
-rw-r--r--src/com/android/car/dialer/ui/activecall/InCallFragment.java145
-rw-r--r--src/com/android/car/dialer/ui/activecall/InCallViewModel.java294
-rw-r--r--src/com/android/car/dialer/ui/activecall/IncomingCallFragment.java47
-rw-r--r--src/com/android/car/dialer/ui/activecall/OnGoingCallControllerBarFragment.java328
-rw-r--r--src/com/android/car/dialer/ui/activecall/OnHoldCallUserProfileFragment.java98
-rw-r--r--src/com/android/car/dialer/ui/activecall/OngoingCallFragment.java102
-rw-r--r--src/com/android/car/dialer/ui/activecall/OngoingCallStateViewModel.java40
-rw-r--r--src/com/android/car/dialer/ui/activecall/RingingCallControllerBarFragment.java (renamed from src/com/android/car/dialer/ui/RingingCallControllerBarFragment.java)35
-rw-r--r--src/com/android/car/dialer/ui/calllog/CallHistoryFragment.java56
-rw-r--r--src/com/android/car/dialer/ui/calllog/CallHistoryViewModel.java (renamed from src/com/android/car/dialer/ui/viewmodel/CallHistoryViewModel.java)50
-rw-r--r--src/com/android/car/dialer/ui/calllog/CallLogAdapter.java83
-rw-r--r--src/com/android/car/dialer/ui/calllog/CallLogViewHolder.java117
-rw-r--r--src/com/android/car/dialer/ui/common/DialerBaseFragment.java126
-rw-r--r--src/com/android/car/dialer/ui/common/DialerListBaseFragment.java78
-rw-r--r--src/com/android/car/dialer/ui/common/DialerUtils.java122
-rw-r--r--src/com/android/car/dialer/ui/common/OnItemClickedListener.java26
-rw-r--r--src/com/android/car/dialer/ui/common/PhoneNumberListAdapter.java65
-rw-r--r--src/com/android/car/dialer/ui/common/UiCallLogLiveData.java163
-rw-r--r--src/com/android/car/dialer/ui/common/entity/UiCallLog.java120
-rw-r--r--src/com/android/car/dialer/ui/contact/ContactDefaultNumberActionProvider.java95
-rw-r--r--src/com/android/car/dialer/ui/contact/ContactDetailsAdapter.java123
-rw-r--r--src/com/android/car/dialer/ui/contact/ContactDetailsFragment.java173
-rw-r--r--src/com/android/car/dialer/ui/contact/ContactDetailsViewHolder.java99
-rw-r--r--src/com/android/car/dialer/ui/contact/ContactDetailsViewModel.java56
-rw-r--r--src/com/android/car/dialer/ui/contact/ContactListAdapter.java77
-rw-r--r--src/com/android/car/dialer/ui/contact/ContactListFragment.java57
-rw-r--r--src/com/android/car/dialer/ui/contact/ContactListViewHolder.java94
-rw-r--r--src/com/android/car/dialer/ui/contact/ContactListViewModel.java147
-rw-r--r--src/com/android/car/dialer/ui/dialpad/AbstractDialpadFragment.java200
-rw-r--r--src/com/android/car/dialer/ui/dialpad/DialpadFragment.java241
-rw-r--r--src/com/android/car/dialer/ui/dialpad/InCallDialpadFragment.java119
-rw-r--r--src/com/android/car/dialer/ui/dialpad/KeypadButton.java (renamed from src/com/android/car/dialer/DialpadButton.java)33
-rw-r--r--src/com/android/car/dialer/ui/dialpad/KeypadFragment.java165
-rw-r--r--src/com/android/car/dialer/ui/favorite/FavoriteAdapter.java91
-rw-r--r--src/com/android/car/dialer/ui/favorite/FavoriteContactViewHolder.java94
-rw-r--r--src/com/android/car/dialer/ui/favorite/FavoriteFragment.java67
-rw-r--r--src/com/android/car/dialer/ui/favorite/FavoriteListFragment.java99
-rw-r--r--src/com/android/car/dialer/ui/favorite/FavoriteViewModel.java42
-rw-r--r--src/com/android/car/dialer/ui/listitem/CallLogListItem.java87
-rw-r--r--src/com/android/car/dialer/ui/listitem/ContactListItem.java86
-rw-r--r--src/com/android/car/dialer/ui/menu/MenuActionProvider.java69
-rw-r--r--src/com/android/car/dialer/ui/search/ContactDetails.java34
-rw-r--r--src/com/android/car/dialer/ui/search/ContactResultViewHolder.java65
-rw-r--r--src/com/android/car/dialer/ui/search/ContactResultsAdapter.java (renamed from src/com/android/car/dialer/ContactResultsAdapter.java)58
-rw-r--r--src/com/android/car/dialer/ui/search/ContactResultsFragment.java213
-rw-r--r--src/com/android/car/dialer/ui/search/ContactResultsViewModel.java118
-rw-r--r--src/com/android/car/dialer/ui/settings/DialerSettingsActivity.java50
-rw-r--r--src/com/android/car/dialer/ui/settings/DialerSettingsFragment.java80
-rw-r--r--src/com/android/car/dialer/ui/settings/common/SettingsListPreferenceDialogFragment.java121
-rw-r--r--src/com/android/car/dialer/ui/settings/common/SettingsPreferenceDialogFragment.java286
-rw-r--r--src/com/android/car/dialer/ui/view/CircleBitmapDrawable.java (renamed from src/com/android/car/dialer/ui/CircleBitmapDrawable.java)2
-rw-r--r--src/com/android/car/dialer/ui/view/ContactAvatarOutputlineProvider.java46
-rw-r--r--src/com/android/car/dialer/ui/view/ListItemOutlineProvider.java61
-rw-r--r--src/com/android/car/dialer/ui/view/ScaleSpan.java71
-rw-r--r--src/com/android/car/dialer/ui/view/VerticalListDividerDecoration.java109
-rw-r--r--src/com/android/car/dialer/ui/warning/NoHfpFragment.java (renamed from src/com/android/car/dialer/NoHfpFragment.java)40
-rw-r--r--src/com/android/car/dialer/widget/CallTypeIconsView.java166
-rw-r--r--src/com/android/car/dialer/widget/CheckableRelativeLayout.java86
-rw-r--r--src/com/android/car/dialer/widget/WorkerExecutor.java53
-rw-r--r--tests/Android.mk19
-rwxr-xr-xtests/checkresources.py22
-rw-r--r--tests/robotests/Android.mk49
-rw-r--r--tests/robotests/config/robolectric.properties14
-rw-r--r--tests/robotests/readme.md6
-rw-r--r--tests/robotests/res/layout/test_activity.xml (renamed from res/drawable/button_active_state_ring.xml)15
-rw-r--r--tests/robotests/res/values/arrays.xml30
-rw-r--r--tests/robotests/res/values/bools.xml20
-rw-r--r--tests/robotests/src/com/android/car/dialer/CarDialerRobolectricTestRunner.java (renamed from src/com/android/car/dialer/livedata/MissedCallHistoryLiveData.java)21
-rw-r--r--tests/robotests/src/com/android/car/dialer/FragmentTestActivity.java65
-rw-r--r--tests/robotests/src/com/android/car/dialer/LiveDataObserver.java28
-rw-r--r--tests/robotests/src/com/android/car/dialer/TestDialerApplication.java75
-rw-r--r--tests/robotests/src/com/android/car/dialer/TestFragment.java41
-rw-r--r--tests/robotests/src/com/android/car/dialer/livedata/AudioRouteLiveDataTest.java119
-rw-r--r--tests/robotests/src/com/android/car/dialer/livedata/BluetoothHfpStateLiveDataTest.java131
-rw-r--r--tests/robotests/src/com/android/car/dialer/livedata/BluetoothPairListLiveDataTest.java145
-rw-r--r--tests/robotests/src/com/android/car/dialer/livedata/BluetoothStateLiveDataTest.java117
-rw-r--r--tests/robotests/src/com/android/car/dialer/livedata/CallDetailLiveDataTest.java137
-rw-r--r--tests/robotests/src/com/android/car/dialer/livedata/CallStateLiveDataTest.java114
-rw-r--r--tests/robotests/src/com/android/car/dialer/livedata/HeartBeatLiveDataTest.java65
-rw-r--r--tests/robotests/src/com/android/car/dialer/telecom/InCallServiceImplTest.java171
-rw-r--r--tests/robotests/src/com/android/car/dialer/telecom/ProjectionCallHandlerTest.java315
-rw-r--r--tests/robotests/src/com/android/car/dialer/telecom/UiBluetoothMonitorTest.java67
-rw-r--r--tests/robotests/src/com/android/car/dialer/telecom/UiCallManagerTest.java275
-rw-r--r--tests/robotests/src/com/android/car/dialer/testutils/BroadcastReceiverVerifier.java90
-rw-r--r--tests/robotests/src/com/android/car/dialer/testutils/ShadowAndroidViewModelFactory.java51
-rw-r--r--tests/robotests/src/com/android/car/dialer/testutils/ShadowBluetoothAdapterForDialer.java59
-rw-r--r--tests/robotests/src/com/android/car/dialer/testutils/ShadowCallLogCalls.java39
-rw-r--r--tests/robotests/src/com/android/car/dialer/testutils/ShadowCar.java47
-rw-r--r--tests/robotests/src/com/android/car/dialer/testutils/ShadowInMemoryPhoneBook.java50
-rw-r--r--tests/robotests/src/com/android/car/dialer/ui/TelecomActivityViewModelTest.java176
-rw-r--r--tests/robotests/src/com/android/car/dialer/ui/activecall/InCallViewModelTest.java200
-rw-r--r--tests/robotests/src/com/android/car/dialer/ui/activecall/IncomingCallFragmentTest.java75
-rw-r--r--tests/robotests/src/com/android/car/dialer/ui/activecall/OnGoingCallControllerBarFragmentTest.java227
-rw-r--r--tests/robotests/src/com/android/car/dialer/ui/activecall/OngoingCallFragmentTest.java96
-rw-r--r--tests/robotests/src/com/android/car/dialer/ui/activecall/RingingCallControllerBarFragmentTest.java127
-rw-r--r--tests/robotests/src/com/android/car/dialer/ui/calllog/CallHistoryFragmentTest.java134
-rw-r--r--tests/robotests/src/com/android/car/dialer/ui/contact/ContactDetailsFragmentTest.java142
-rw-r--r--tests/robotests/src/com/android/car/dialer/ui/contact/ContactListFragmentTest.java159
-rw-r--r--tests/robotests/src/com/android/car/dialer/ui/contact/ContactListViewHolderTest.java257
-rw-r--r--tests/robotests/src/com/android/car/dialer/ui/dialpad/DialpadFragmentTest.java235
-rw-r--r--tests/robotests/src/com/android/car/dialer/ui/dialpad/InCallDialpadFragmentTest.java68
-rw-r--r--tests/robotests/src/com/android/car/dialer/ui/favorite/FavoriteListFragmentTest.java125
-rw-r--r--tests/robotests/src/com/android/car/dialer/ui/search/ContactResultsFragmentTest.java156
-rw-r--r--tests/robotests/src/com/android/car/dialer/ui/settings/common/SettingsListPreferenceDialogFragmentTest.java129
-rw-r--r--tests/robotests/src/com/android/car/dialer/ui/settings/common/SettingsPreferenceDialogFragmentTest.java251
-rw-r--r--tests/robotests/src/com/android/car/dialer/ui/warning/NoHfpFragmentTest.java109
374 files changed, 17221 insertions, 9670 deletions
diff --git a/Android.bp b/Android.bp
deleted file mode 100644
index 4bac9dfb..00000000
--- a/Android.bp
+++ /dev/null
@@ -1,45 +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.
-//
-
-android_app {
- name: "CarDialerApp",
- srcs: ["src/**/*.java"],
- resource_dirs: ["res"],
- platform_apis: true,
- overrides: ["Dialer"],
- static_libs: [
- "androidx.car_car",
- "androidx.lifecycle_lifecycle-extensions",
- "androidx-constraintlayout_constraintlayout",
- "androidx.legacy_legacy-support-v4",
- "androidx.cardview_cardview",
- "car-apps-common",
- "androidx-constraintlayout_constraintlayout-solver",
- "guava",
- ],
- optimize: {
- enabled: false,
- },
- privileged: true,
- dex_preopt: {
- enabled: false,
- },
- product_variables: {
- pdk: {
- enabled: false,
- },
- },
-}
diff --git a/Android.mk b/Android.mk
new file mode 100644
index 00000000..2d8691c8
--- /dev/null
+++ b/Android.mk
@@ -0,0 +1,128 @@
+#
+# Copyright (C) 2015 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+ifneq ($(TARGET_BUILD_PDK), true)
+
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+
+LOCAL_PACKAGE_NAME := CarDialerApp
+LOCAL_PRIVATE_PLATFORM_APIS := true
+
+LOCAL_REQUIRED_MODULES := privapp_whitelist_com.android.car.dialer
+
+LOCAL_OVERRIDES_PACKAGES := Dialer
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_USE_AAPT2 := true
+
+LOCAL_JAVA_LIBRARIES += \
+ android.car
+
+LOCAL_STATIC_ANDROID_LIBRARIES += \
+ androidx.recyclerview_recyclerview \
+ androidx.lifecycle_lifecycle-extensions \
+ androidx.preference_preference \
+ androidx-constraintlayout_constraintlayout \
+ androidx.legacy_legacy-support-v4 \
+ androidx.cardview_cardview \
+ car-apps-common \
+ car-arch-common \
+ car-telephony-common \
+ car-theme-lib \
+
+# Including the resources for the static android libraries allows to pick up their static overlays.
+LOCAL_RESOURCE_DIR += \
+ $(LOCAL_PATH)/../libs/car-apps-common/res \
+ $(LOCAL_PATH)/../libs/car-telephony-common/res \
+ $(LOCAL_PATH)/../libs/car-theme-lib/res
+
+LOCAL_STATIC_JAVA_LIBRARIES := \
+ androidx-constraintlayout_constraintlayout-solver \
+ guava \
+ car-glide \
+ car-glide-disklrucache \
+ car-gifdecoder \
+ libphonenumber
+
+LOCAL_PROGUARD_ENABLED := disabled
+
+LOCAL_PRIVILEGED_MODULE := true
+
+LOCAL_DEX_PREOPT := false
+
+include $(BUILD_PACKAGE)
+
+###################################################################################
+# Duplicate of CarDialerApp which includes testing only resources for Robolectric #
+###################################################################################
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_RESOURCE_DIR := \
+ $(LOCAL_PATH)/res \
+ $(LOCAL_PATH)/tests/robotests/res
+
+LOCAL_PACKAGE_NAME := CarDialerAppForTesting
+LOCAL_PRIVATE_PLATFORM_APIS := true
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_USE_AAPT2 := true
+
+LOCAL_JAVA_LIBRARIES += \
+ android.car
+
+LOCAL_STATIC_ANDROID_LIBRARIES += \
+ androidx.recyclerview_recyclerview \
+ androidx.lifecycle_lifecycle-extensions \
+ androidx.preference_preference \
+ androidx-constraintlayout_constraintlayout \
+ androidx.legacy_legacy-support-v4 \
+ androidx.cardview_cardview \
+ car-apps-common \
+ car-arch-common \
+ car-telephony-common \
+ car-theme-lib \
+
+LOCAL_STATIC_JAVA_LIBRARIES := \
+ androidx-constraintlayout_constraintlayout-solver \
+ guava \
+ car-glide \
+ car-glide-disklrucache \
+ car-gifdecoder \
+ libphonenumber
+
+LOCAL_PROGUARD_ENABLED := disabled
+
+LOCAL_PRIVILEGED_MODULE := true
+
+LOCAL_DEX_PREOPT := false
+
+include $(BUILD_PACKAGE)
+###################################################################################
+
+# Use the following include to make our test apk.
+ifeq (,$(ONE_SHOT_MAKEFILE))
+include $(call all-makefiles-under,$(LOCAL_PATH))
+endif
+
+endif
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 113363d8..cdde5cf1 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -14,78 +14,106 @@
limitations under the License.
-->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.car.dialer">
<uses-sdk android:minSdkVersion="24" android:targetSdkVersion='24'/>
- <uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.READ_CALL_LOG"/>
+ <uses-permission android:name="android.permission.WRITE_CALL_LOG"/>
<uses-permission android:name="android.permission.READ_CONTACTS"/>
+ <uses-permission android:name="android.permission.WRITE_CONTACTS"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.MODIFY_PHONE_STATE"/>
<uses-permission android:name="android.permission.CALL_PHONE"/>
<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS"/>
+ <uses-permission android:name="android.car.permission.ACCESS_CAR_PROJECTION_STATUS"/>
<!-- The Dialer needs to be directBootAware so that it can reflect the correct call state
when the system boots up. -->
<application
+ android:name=".DialerApplication"
android:directBootAware="true"
android:label="@string/phone_app_name"
android:icon="@drawable/ic_app_icon">
- <activity android:name=".TelecomActivity"
- android:launchMode="singleInstance"
- android:theme="@style/TelecomActivityTheme"
- android:label="@string/phone_app_name"
- android:exported="true"
- android:resizeableActivity="true">
+ <activity android:name=".ui.TelecomActivity"
+ android:launchMode="singleTask"
+ android:theme="@style/Theme.Dialer"
+ android:label="@string/phone_app_name"
+ android:exported="true"
+ android:resizeableActivity="true">
+ <meta-data android:name="distractionOptimized" android:value="true"/>
+ <meta-data android:name="android.app.searchable" android:resource="@xml/searchable"/>
<intent-filter>
<action android:name="android.intent.action.DIAL"/>
- <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.CALL"/>
- <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
<intent-filter>
- <action android:name="android.intent.action.VIEW" />
+ <action android:name="android.intent.action.VIEW"/>
<action android:name="android.intent.action.DIAL"/>
- <category android:name="android.intent.category.DEFAULT" />
- <data android:scheme="tel" />
+ <category android:name="android.intent.category.DEFAULT"/>
+ <data android:scheme="tel"/>
</intent-filter>
<intent-filter>
- <action android:name="android.intent.action.MAIN" />
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
- </activity>
- <activity android:name=".ContactSearchActivity"
- android:label="@string/phone_app_name"
- android:theme="@style/ContactSearchActivityTheme"
- android:exported="true"
- android:launchMode="singleTop"
- android:resizeableActivity="true">
<intent-filter>
- <action android:name="android.intent.action.SEARCH" />
- <category android:name="android.intent.category.DEFAULT" />
+ <action android:name="android.intent.action.SEARCH"/>
+ <category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
+ </activity>
- <intent-filter>
- <action android:name="com.android.car.dialer.SHOW_CONTACT_DETAILS" />
- <category android:name="android.intent.category.DEFAULT" />
- </intent-filter>
+ <activity android:name=".ui.activecall.InCallActivity"
+ android:launchMode="singleInstance"
+ android:theme="@style/Theme.Dialer"
+ android:label="@string/phone_app_name">
+ <meta-data android:name="distractionOptimized" android:value="true"/>
+ </activity>
+
+ <activity android:name=".ui.settings.DialerSettingsActivity"
+ android:parentActivityName=".ui.TelecomActivity"
+ android:launchMode="singleTask"
+ android:theme="@style/Theme.Dialer.Setting"
+ android:label="@string/setting_title">
</activity>
<service android:name="com.android.car.dialer.telecom.InCallServiceImpl"
android:permission="android.permission.BIND_INCALL_SERVICE"
android:exported="true">
- <meta-data android:name="android.telecom.IN_CALL_SERVICE_UI" android:value="true" />
+ <meta-data android:name="android.telecom.IN_CALL_SERVICE_UI" android:value="true"/>
<intent-filter>
<action android:name="android.telecom.InCallService"/>
</intent-filter>
</service>
+
+ <service
+ android:name="com.android.car.dialer.notification.NotificationService"
+ android:permission="android.permission.BIND_JOB_SERVICE">
+ </service>
+
+ <!-- BroadcastReceiver for receiving Intents from Notification mechanism. -->
+ <receiver
+ android:directBootAware="true"
+ android:exported="false"
+ android:name="com.android.car.dialer.notification.NotificationReceiver"/>
+
+ <receiver
+ android:directBootAware="true"
+ android:name="com.android.car.dialer.notification.MissedCallReceiver">
+ <intent-filter>
+ <action android:name="android.telecom.action.SHOW_MISSED_CALLS_NOTIFICATION"/>
+ </intent-filter>
+ </receiver>
</application>
</manifest>
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
new file mode 100644
index 00000000..0ef19c57
--- /dev/null
+++ b/PREUPLOAD.cfg
@@ -0,0 +1,8 @@
+[Hook Scripts]
+checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT}
+ktlint_hook = ${REPO_ROOT}/prebuilts/ktlint/ktlint.py -f ${PREUPLOAD_FILES}
+resources_hook = ${REPO_ROOT}/packages/apps/Car/Dialer/tests/checkresources.py ${REPO_ROOT}/packages/apps/Car/Dialer
+
+[Builtin Hooks]
+commit_msg_changeid_field = true
+commit_msg_test_field = true
diff --git a/res/anim/telecom_fade_in.xml b/res/anim/telecom_fade_in.xml
deleted file mode 100644
index e7d2e143..00000000
--- a/res/anim/telecom_fade_in.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2015 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<set xmlns:android="http://schemas.android.com/apk/res/android">
- <alpha
- android:fromAlpha="0"
- android:toAlpha="1"
- android:duration="600"
- android:startOffset="0"
- android:interpolator="@android:interpolator/linear_out_slow_in" />
-</set> \ No newline at end of file
diff --git a/res/anim/telecom_slide_in_with_delay.xml b/res/anim/telecom_slide_in_with_delay.xml
deleted file mode 100644
index 475389c9..00000000
--- a/res/anim/telecom_slide_in_with_delay.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2015 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<set xmlns:android="http://schemas.android.com/apk/res/android">
- <translate
- android:fromYDelta="100%p"
- android:toYDelta="0%p"
- android:interpolator="@android:interpolator/accelerate_decelerate"
- android:startOffset="200"
- android:duration="500" />
-</set> \ No newline at end of file
diff --git a/res/anim/telecom_slide_out.xml b/res/anim/telecom_slide_out.xml
deleted file mode 100644
index 4856ac6a..00000000
--- a/res/anim/telecom_slide_out.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2015 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<set xmlns:android="http://schemas.android.com/apk/res/android">
- <translate
- android:fromYDelta="0%p"
- android:toYDelta="100%p"
- android:interpolator="@android:interpolator/accelerate_decelerate"
- android:duration="300" />
-</set>
diff --git a/res/animator/fade_in.xml b/res/animator/fade_in.xml
deleted file mode 100644
index 14001946..00000000
--- a/res/animator/fade_in.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-<set xmlns:android="http://schemas.android.com/apk/res/android">
- <objectAnimator
- android:propertyName="alpha"
- android:valueFrom="0.0"
- android:valueTo="1.0"
- android:duration="600"
- android:interpolator="@android:interpolator/linear_out_slow_in" />
-</set>
diff --git a/res/animator/fade_out.xml b/res/animator/fade_out.xml
deleted file mode 100644
index 14f1372c..00000000
--- a/res/animator/fade_out.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-<set xmlns:android="http://schemas.android.com/apk/res/android">
- <objectAnimator
- android:propertyName="alpha"
- android:valueFrom="1.0"
- android:valueTo="0.0"
- android:duration="600"
- android:interpolator="@android:interpolator/linear_out_slow_in" />
-</set>
diff --git a/res/animator/scale_down.xml b/res/animator/scale_down.xml
new file mode 100644
index 00000000..677c7f53
--- /dev/null
+++ b/res/animator/scale_down.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<animator
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="@integer/config_dial_motion_duration"
+ android:interpolator="@android:interpolator/fast_out_slow_in">
+ <propertyValuesHolder
+ android:valueFrom="@integer/config_dial_motion_scale_start"
+ android:valueTo="1"/>
+</animator>
diff --git a/res/values-w768dp/dimens.xml b/res/color/icon_accent_activatable.xml
index 3717afc0..cf4b9e79 100644
--- a/res/values-w768dp/dimens.xml
+++ b/res/color/icon_accent_activatable.xml
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='UTF-8'?>
-<!-- Copyright (C) 2015 The Android Open Source Project
+<!-- Copyright (C) 2019 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -13,7 +13,9 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<resources>
- <!-- The max width of content in apps for adaptive responsive -->
- <dimen name="apps_max_content_width">768dp</dimen>
-</resources>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="@color/primary_icon_color"
+ android:state_activated="false"/>
+ <item android:color="?android:colorAccent"
+ android:state_activated="true"/>
+</selector>
diff --git a/res/menu/options_menu.xml b/res/color/icon_tint_state_list.xml
index 66b644e3..6822c7b6 100644
--- a/res/menu/options_menu.xml
+++ b/res/color/icon_tint_state_list.xml
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='UTF-8'?>
-<!-- Copyright (C) 2017 The Android Open Source Project
+<!-- Copyright (C) 2019 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -13,11 +13,13 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<menu xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto">
- <item android:id="@+id/search"
- android:title="@string/search_title"
- android:icon="@drawable/ic_search"
- app:actionViewClass="androidx.appcompat.widget.SearchView"
- app:showAsAction="always|collapseActionView" />
-</menu>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="@color/primary_icon_color"
+ android:state_enabled="true"
+ android:state_activated="false"/>
+ <item android:color="#FF000000"
+ android:state_enabled="true"
+ android:state_activated="true"/>
+ <item android:color="#80FFFFFF"
+ android:state_enabled="false"/>
+</selector>
diff --git a/res/values-h480dp/dimens.xml b/res/color/text_accent_activatable.xml
index eb97fb6b..4f1dc0fc 100644
--- a/res/values-h480dp/dimens.xml
+++ b/res/color/text_accent_activatable.xml
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='UTF-8'?>
-<!-- Copyright (C) 2018 The Android Open Source Project
+<!-- Copyright (C) 2019 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -13,6 +13,9 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<resources>
- <dimen name="dialer_fab_size">100dp</dimen>
-</resources>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="@color/primary_text_color"
+ android:state_activated="false"/>
+ <item android:color="?android:colorAccent"
+ android:state_activated="true"/>
+</selector>
diff --git a/res/drawable-hdpi/car_ic_bluetooth_disable_large.png b/res/drawable-hdpi/car_ic_bluetooth_disable_large.png
deleted file mode 100644
index 4889078c..00000000
--- a/res/drawable-hdpi/car_ic_bluetooth_disable_large.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/ic_123_activated.png b/res/drawable-hdpi/ic_123_activated.png
deleted file mode 100644
index de2fd981..00000000
--- a/res/drawable-hdpi/ic_123_activated.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/ic_123_normal.png b/res/drawable-hdpi/ic_123_normal.png
deleted file mode 100644
index 16cebe08..00000000
--- a/res/drawable-hdpi/ic_123_normal.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/ic_arrow_drop_down.png b/res/drawable-hdpi/ic_arrow_drop_down.png
deleted file mode 100644
index 6d5aedf0..00000000
--- a/res/drawable-hdpi/ic_arrow_drop_down.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/ic_call_merge.png b/res/drawable-hdpi/ic_call_merge.png
deleted file mode 100644
index 739f7b77..00000000
--- a/res/drawable-hdpi/ic_call_merge.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/ic_call_voicemail.png b/res/drawable-hdpi/ic_call_voicemail.png
deleted file mode 100644
index a2b40a24..00000000
--- a/res/drawable-hdpi/ic_call_voicemail.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/ic_drawer_call_missed.png b/res/drawable-hdpi/ic_drawer_call_missed.png
deleted file mode 100644
index 6ecda9e0..00000000
--- a/res/drawable-hdpi/ic_drawer_call_missed.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/ic_drawer_dialpad.png b/res/drawable-hdpi/ic_drawer_dialpad.png
deleted file mode 100644
index 84e056bc..00000000
--- a/res/drawable-hdpi/ic_drawer_dialpad.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/ic_drawer_history.png b/res/drawable-hdpi/ic_drawer_history.png
deleted file mode 100644
index 37bf6f55..00000000
--- a/res/drawable-hdpi/ic_drawer_history.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/ic_empty_speed_dial.png b/res/drawable-hdpi/ic_empty_speed_dial.png
deleted file mode 100644
index cebe7f03..00000000
--- a/res/drawable-hdpi/ic_empty_speed_dial.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/ic_favorite.png b/res/drawable-hdpi/ic_favorite.png
deleted file mode 100644
index 2685eabb..00000000
--- a/res/drawable-hdpi/ic_favorite.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/ic_list_view_disable.png b/res/drawable-hdpi/ic_list_view_disable.png
deleted file mode 100644
index 651ee625..00000000
--- a/res/drawable-hdpi/ic_list_view_disable.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/ic_pause.png b/res/drawable-hdpi/ic_pause.png
deleted file mode 100644
index 4d5fa03e..00000000
--- a/res/drawable-hdpi/ic_pause.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/ic_swap_calls.png b/res/drawable-hdpi/ic_swap_calls.png
deleted file mode 100644
index b0423797..00000000
--- a/res/drawable-hdpi/ic_swap_calls.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/car_ic_bluetooth_disable_large.png b/res/drawable-mdpi/car_ic_bluetooth_disable_large.png
deleted file mode 100644
index d4fa02e1..00000000
--- a/res/drawable-mdpi/car_ic_bluetooth_disable_large.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/ic_123_activated.png b/res/drawable-mdpi/ic_123_activated.png
deleted file mode 100644
index ca499271..00000000
--- a/res/drawable-mdpi/ic_123_activated.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/ic_123_normal.png b/res/drawable-mdpi/ic_123_normal.png
deleted file mode 100644
index df0a0fa4..00000000
--- a/res/drawable-mdpi/ic_123_normal.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/ic_arrow_drop_down.png b/res/drawable-mdpi/ic_arrow_drop_down.png
deleted file mode 100644
index 87d2b494..00000000
--- a/res/drawable-mdpi/ic_arrow_drop_down.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/ic_call_merge.png b/res/drawable-mdpi/ic_call_merge.png
deleted file mode 100644
index 16900ce7..00000000
--- a/res/drawable-mdpi/ic_call_merge.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/ic_call_voicemail.png b/res/drawable-mdpi/ic_call_voicemail.png
deleted file mode 100644
index 76f5767b..00000000
--- a/res/drawable-mdpi/ic_call_voicemail.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/ic_drawer_call_missed.png b/res/drawable-mdpi/ic_drawer_call_missed.png
deleted file mode 100644
index 3016dc77..00000000
--- a/res/drawable-mdpi/ic_drawer_call_missed.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/ic_drawer_dialpad.png b/res/drawable-mdpi/ic_drawer_dialpad.png
deleted file mode 100644
index c2e9a42c..00000000
--- a/res/drawable-mdpi/ic_drawer_dialpad.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/ic_drawer_history.png b/res/drawable-mdpi/ic_drawer_history.png
deleted file mode 100644
index e141b5c5..00000000
--- a/res/drawable-mdpi/ic_drawer_history.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/ic_empty_speed_dial.png b/res/drawable-mdpi/ic_empty_speed_dial.png
deleted file mode 100644
index 9c2c58a0..00000000
--- a/res/drawable-mdpi/ic_empty_speed_dial.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/ic_favorite.png b/res/drawable-mdpi/ic_favorite.png
deleted file mode 100644
index 2c543c49..00000000
--- a/res/drawable-mdpi/ic_favorite.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/ic_list_view_disable.png b/res/drawable-mdpi/ic_list_view_disable.png
deleted file mode 100644
index 8de79686..00000000
--- a/res/drawable-mdpi/ic_list_view_disable.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/ic_pause.png b/res/drawable-mdpi/ic_pause.png
deleted file mode 100644
index 156807ee..00000000
--- a/res/drawable-mdpi/ic_pause.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/ic_swap_calls.png b/res/drawable-mdpi/ic_swap_calls.png
deleted file mode 100644
index a18d3d15..00000000
--- a/res/drawable-mdpi/ic_swap_calls.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-night-hdpi/ic_123_activated.png b/res/drawable-night-hdpi/ic_123_activated.png
deleted file mode 100644
index 5f5614ea..00000000
--- a/res/drawable-night-hdpi/ic_123_activated.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-night-hdpi/ic_123_normal.png b/res/drawable-night-hdpi/ic_123_normal.png
deleted file mode 100644
index d6fa21d2..00000000
--- a/res/drawable-night-hdpi/ic_123_normal.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-night-mdpi/ic_123_activated.png b/res/drawable-night-mdpi/ic_123_activated.png
deleted file mode 100644
index 94cc3b8f..00000000
--- a/res/drawable-night-mdpi/ic_123_activated.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-night-mdpi/ic_123_normal.png b/res/drawable-night-mdpi/ic_123_normal.png
deleted file mode 100644
index b5e01ba5..00000000
--- a/res/drawable-night-mdpi/ic_123_normal.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-night-xhdpi/ic_123_activated.png b/res/drawable-night-xhdpi/ic_123_activated.png
deleted file mode 100644
index beab20fc..00000000
--- a/res/drawable-night-xhdpi/ic_123_activated.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-night-xhdpi/ic_123_normal.png b/res/drawable-night-xhdpi/ic_123_normal.png
deleted file mode 100644
index 389156a0..00000000
--- a/res/drawable-night-xhdpi/ic_123_normal.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-night-xxhdpi/ic_123_activated.png b/res/drawable-night-xxhdpi/ic_123_activated.png
deleted file mode 100644
index cf02c8e4..00000000
--- a/res/drawable-night-xxhdpi/ic_123_activated.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-night-xxhdpi/ic_123_normal.png b/res/drawable-night-xxhdpi/ic_123_normal.png
deleted file mode 100644
index 991ceedb..00000000
--- a/res/drawable-night-xxhdpi/ic_123_normal.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/car_ic_bluetooth_disable_large.png b/res/drawable-xhdpi/car_ic_bluetooth_disable_large.png
deleted file mode 100644
index 46514acf..00000000
--- a/res/drawable-xhdpi/car_ic_bluetooth_disable_large.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/ic_123_activated.png b/res/drawable-xhdpi/ic_123_activated.png
deleted file mode 100644
index 72da4a08..00000000
--- a/res/drawable-xhdpi/ic_123_activated.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/ic_123_normal.png b/res/drawable-xhdpi/ic_123_normal.png
deleted file mode 100644
index 2bbe71cb..00000000
--- a/res/drawable-xhdpi/ic_123_normal.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/ic_arrow_drop_down.png b/res/drawable-xhdpi/ic_arrow_drop_down.png
deleted file mode 100644
index 996de9d0..00000000
--- a/res/drawable-xhdpi/ic_arrow_drop_down.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/ic_call_merge.png b/res/drawable-xhdpi/ic_call_merge.png
deleted file mode 100644
index f38639bd..00000000
--- a/res/drawable-xhdpi/ic_call_merge.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/ic_call_voicemail.png b/res/drawable-xhdpi/ic_call_voicemail.png
deleted file mode 100644
index 11d5c954..00000000
--- a/res/drawable-xhdpi/ic_call_voicemail.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/ic_drawer_call_missed.png b/res/drawable-xhdpi/ic_drawer_call_missed.png
deleted file mode 100644
index edf49bba..00000000
--- a/res/drawable-xhdpi/ic_drawer_call_missed.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/ic_drawer_dialpad.png b/res/drawable-xhdpi/ic_drawer_dialpad.png
deleted file mode 100644
index b2c0058c..00000000
--- a/res/drawable-xhdpi/ic_drawer_dialpad.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/ic_drawer_history.png b/res/drawable-xhdpi/ic_drawer_history.png
deleted file mode 100644
index 92e22021..00000000
--- a/res/drawable-xhdpi/ic_drawer_history.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/ic_empty_speed_dial.png b/res/drawable-xhdpi/ic_empty_speed_dial.png
deleted file mode 100644
index f807ee98..00000000
--- a/res/drawable-xhdpi/ic_empty_speed_dial.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/ic_favorite.png b/res/drawable-xhdpi/ic_favorite.png
deleted file mode 100644
index b933ec67..00000000
--- a/res/drawable-xhdpi/ic_favorite.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/ic_list_view_disable.png b/res/drawable-xhdpi/ic_list_view_disable.png
deleted file mode 100644
index 82adcb2a..00000000
--- a/res/drawable-xhdpi/ic_list_view_disable.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/ic_pause.png b/res/drawable-xhdpi/ic_pause.png
deleted file mode 100644
index 262e9d82..00000000
--- a/res/drawable-xhdpi/ic_pause.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/ic_swap_calls.png b/res/drawable-xhdpi/ic_swap_calls.png
deleted file mode 100644
index 9f97adc3..00000000
--- a/res/drawable-xhdpi/ic_swap_calls.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xxhdpi/car_ic_bluetooth_disable_large.png b/res/drawable-xxhdpi/car_ic_bluetooth_disable_large.png
deleted file mode 100644
index 80280d87..00000000
--- a/res/drawable-xxhdpi/car_ic_bluetooth_disable_large.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_123_activated.png b/res/drawable-xxhdpi/ic_123_activated.png
deleted file mode 100644
index 6da957e6..00000000
--- a/res/drawable-xxhdpi/ic_123_activated.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_123_normal.png b/res/drawable-xxhdpi/ic_123_normal.png
deleted file mode 100644
index 79488b3a..00000000
--- a/res/drawable-xxhdpi/ic_123_normal.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_arrow_drop_down.png b/res/drawable-xxhdpi/ic_arrow_drop_down.png
deleted file mode 100644
index e385d000..00000000
--- a/res/drawable-xxhdpi/ic_arrow_drop_down.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_call_merge.png b/res/drawable-xxhdpi/ic_call_merge.png
deleted file mode 100644
index 6886a16e..00000000
--- a/res/drawable-xxhdpi/ic_call_merge.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_call_voicemail.png b/res/drawable-xxhdpi/ic_call_voicemail.png
deleted file mode 100644
index c2ba302e..00000000
--- a/res/drawable-xxhdpi/ic_call_voicemail.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_drawer_call_missed.png b/res/drawable-xxhdpi/ic_drawer_call_missed.png
deleted file mode 100644
index f62ce667..00000000
--- a/res/drawable-xxhdpi/ic_drawer_call_missed.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_drawer_dialpad.png b/res/drawable-xxhdpi/ic_drawer_dialpad.png
deleted file mode 100644
index 09f3c0b0..00000000
--- a/res/drawable-xxhdpi/ic_drawer_dialpad.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_drawer_history.png b/res/drawable-xxhdpi/ic_drawer_history.png
deleted file mode 100644
index b23fd7fa..00000000
--- a/res/drawable-xxhdpi/ic_drawer_history.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_empty_speed_dial.png b/res/drawable-xxhdpi/ic_empty_speed_dial.png
deleted file mode 100644
index 76764e4b..00000000
--- a/res/drawable-xxhdpi/ic_empty_speed_dial.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_favorite.png b/res/drawable-xxhdpi/ic_favorite.png
deleted file mode 100644
index 825c2459..00000000
--- a/res/drawable-xxhdpi/ic_favorite.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_list_view_disable.png b/res/drawable-xxhdpi/ic_list_view_disable.png
deleted file mode 100644
index fc649359..00000000
--- a/res/drawable-xxhdpi/ic_list_view_disable.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_pause.png b/res/drawable-xxhdpi/ic_pause.png
deleted file mode 100644
index 46b2dd11..00000000
--- a/res/drawable-xxhdpi/ic_pause.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_swap_calls.png b/res/drawable-xxhdpi/ic_swap_calls.png
deleted file mode 100644
index 2dbf844e..00000000
--- a/res/drawable-xxhdpi/ic_swap_calls.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable/action_button_background.xml b/res/drawable/action_button_background.xml
new file mode 100644
index 00000000..75a71fb1
--- /dev/null
+++ b/res/drawable/action_button_background.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<ripple
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="@*android:color/car_card_ripple_background">
+ <item android:id="@android:id/mask">
+ <shape android:shape="rectangle">
+ <solid android:color="?android:colorAccent" />
+ <corners android:radius="@*android:dimen/car_radius_1"/>
+ </shape>
+ </item>
+</ripple>
diff --git a/res/drawable/avatar_rounded_bg.xml b/res/drawable/avatar_rounded_bg.xml
deleted file mode 100644
index 322ecdd9..00000000
--- a/res/drawable/avatar_rounded_bg.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2016 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
- android:shape="rectangle" >
- <solid android:color="#01579B"/>
- <corners android:radius="@dimen/avatar_rounded_radius" />
-</shape>
diff --git a/res/drawable/ic_call_state_switch.xml b/res/drawable/button_active_state_circle.xml
index b7142dde..6f19f5e6 100644
--- a/res/drawable/ic_call_state_switch.xml
+++ b/res/drawable/button_active_state_circle.xml
@@ -13,12 +13,16 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<selector
- xmlns:android="http://schemas.android.com/apk/res/android" >
- <item
- android:state_activated="true"
- android:drawable="@drawable/ic_play" />
- <item
- android:state_activated="false"
- android:drawable="@drawable/ic_pause" />
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_enabled="true"
+ android:state_activated="true">
+ <shape android:shape="oval">
+ <solid android:color="@color/primary_icon_color"/>
+ </shape>
+ </item>
+ <item android:state_enabled="true"
+ android:state_activated="false"
+ android:drawable="@android:color/transparent"/>
+ <item android:state_enabled="false"
+ android:drawable="@android:color/transparent"/>
</selector>
diff --git a/res/drawable/contact_photo_gradient.xml b/res/drawable/contact_photo_gradient.xml
deleted file mode 100644
index d40ad154..00000000
--- a/res/drawable/contact_photo_gradient.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2016 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
- android:shape="rectangle">
- <gradient
- android:startColor="@color/phone_theme"
- android:endColor="#00000000" />
-</shape>
diff --git a/res/drawable/dialer_ripple_background.xml b/res/drawable/dialer_ripple_background.xml
index c4ee2291..e7086ee7 100644
--- a/res/drawable/dialer_ripple_background.xml
+++ b/res/drawable/dialer_ripple_background.xml
@@ -14,5 +14,5 @@ See the License for the specific language governing permissions and
limitations under the License.
-->
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="@color/car_card_ripple_background_dark">
+ android:color="@*android:color/car_card_ripple_background">
</ripple>
diff --git a/res/drawable/dialpad_delete_button_background.xml b/res/drawable/dialpad_delete_button_background.xml
deleted file mode 100644
index 688e41d2..00000000
--- a/res/drawable/dialpad_delete_button_background.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2015 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<ripple xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="@color/car_card_ripple_background" />
diff --git a/res/drawable/in_call_card_background.xml b/res/drawable/hero_button_background.xml
index f1e45400..15e9b2a4 100644
--- a/res/drawable/in_call_card_background.xml
+++ b/res/drawable/hero_button_background.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2018 The Android Open Source Project
+<!-- Copyright (C) 2019 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -13,8 +13,12 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
-<shape xmlns:android="http://schemas.android.com/apk/res/android">
- <solid android:color="@color/car_card" />
- <corners
- android:topRightRadius="@dimen/in_call_card_corner_radius"/>
-</shape>
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="@*android:color/car_card_ripple_background">
+ <item>
+ <shape android:shape="rectangle">
+ <solid android:color="@color/hero_button_background_color" />
+ <corners android:radius="@dimen/hero_button_corner_radius"/>
+ </shape>
+ </item>
+</ripple>
diff --git a/res/drawable/ic_add_favorite.xml b/res/drawable/ic_add_favorite.xml
new file mode 100644
index 00000000..20d741cc
--- /dev/null
+++ b/res/drawable/ic_add_favorite.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:width="@dimen/large_avatar_icon_size"
+ android:height="@dimen/large_avatar_icon_size">
+ <shape android:shape="rectangle">
+ <solid android:color="@color/add_favorite_background_color"/>
+ </shape>
+ </item>
+
+ <item android:width="@dimen/primary_icon_size"
+ android:height="@dimen/primary_icon_size"
+ android:gravity="center">
+ <vector android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:width="@dimen/primary_icon_size"
+ android:height="@dimen/primary_icon_size">
+
+ <path android:pathData="M0-.25h48v48H0z"/>
+ <path
+ android:fillColor="#fffafafa"
+ android:pathData="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
+ </vector>
+ </item>
+</layer-list>
diff --git a/res/drawable/ic_app_icon.xml b/res/drawable/ic_app_icon.xml
index d6e46f02..eb5c9f6d 100644
--- a/res/drawable/ic_app_icon.xml
+++ b/res/drawable/ic_app_icon.xml
@@ -1,16 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="56dp"
- android:height="56dp"
- android:viewportWidth="24"
- android:viewportHeight="24">
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="56dp"
+ android:height="56dp"
+ android:viewportWidth="192"
+ android:viewportHeight="192">
+ <path
+ android:fillColor="#1A73E8"
+ android:pathData="M 8,96 a 88,88 0 0 1 176,0 a 88,88 0 0 1 -176,0 z"/>
+ <path
+ android:fillColor="#185ABC"
+ android:pathData="M57.99 49C51 49 43 57 43 66c0 7 2 18 3 21c10.29 30.9 37.04 48.91 54 54c9.99 3 15 4 22 4 c14.99 0 21-5 24-12c1.18-2.76-17 3-17 3l-49-20L58.99 87L57.99 49z"/>
+ <path
+ android:fillColor="#FFFFFF"
+ android:pathData="M145.84 113.21c-1.35-6.81-2.48-8.93-8.98-9.03c-6.27 0.26-9.62 0.22-13.85-0.18c-4.79-0.53-5.58-0.19-8 3 l-7 12c-19.32-6.82-30.73-20.87-36-29l8.95-8.89c2.73-2.92 3.28-5.34 2-10.03c-1.08-4.15-2.24-8.76-2.99-15.05 c-1.14-6.46-2.1-7.77-8.98-8.03l-10.98 0c-7.42-0.05-12.21 6.56-11.98 14.04c0.33 10.78 1.84 21.46 5 28.09 c9.24 19.39 26.72 38.93 53.9 47.15c7.21 2.18 16.52 3.66 28.95 2.01c7.36-0.98 13.22-6.66 11.98-14.05L145.84 113.21z"/>
+ <path
+ android:fillColor="#F1F3F4"
+ android:pathData="M61.49 104.51c8.12 11.32 19.39 21.65 34.11 28.47l9-15.29c-15.58-6.49-25.54-17.58-30.98-25.3 C71.76 94.24 63.58 102.42 61.49 104.51z"/>
<path
- android:pathData="M0 0h24v24H0z" />
+ android:fillColor="#DADCE0"
+ android:pathData="M108 119c-1.16-0.41-2.3-0.85-3.4-1.31l-9 15.29c1.09 0.5 2.19 0.99 3.32 1.45L108 119z"/>
<path
- android:fillColor="#ff4486f6"
- android:pathData="M6.62 10.79c1.44 2.83 3.76 5.14 6.59 6.59l2.2-2.2c.27-.27 .67 -.36 1.02-.24 1.12
-.37 2.33 .57 3.57 .57 .55 0 1 .45 1 1V20c0 .55-.45 1-1 1-9.39 0-17-7.61-17-17
-0-.55 .45 -1 1-1h3.5c.55 0 1 .45 1 1 0 1.25 .2 2.45 .57 3.57 .11 .35 .03 .74-.25
-1.02l-2.2 2.2z" />
+ android:fillColor="#DADCE0"
+ android:pathData="M61.91 105.09l12.12-12.12C73.28 91.92 72.59 90.92 72 90l-12.14 12.14 C60.52 103.13 61.2 104.11 61.91 105.09z"/>
</vector>
diff --git a/res/drawable/ic_arrow_back.xml b/res/drawable/ic_arrow_back.xml
index 2c1655f3..865c6c79 100644
--- a/res/drawable/ic_arrow_back.xml
+++ b/res/drawable/ic_arrow_back.xml
@@ -14,8 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="@dimen/car_primary_icon_size"
- android:height="@dimen/car_primary_icon_size"
+ android:width="@dimen/primary_icon_size"
+ android:height="@dimen/primary_icon_size"
+ android:tint="@color/primary_icon_color"
android:viewportWidth="48.0"
android:viewportHeight="48.0">
<path
diff --git a/res/drawable/ic_arrow_down.xml b/res/drawable/ic_arrow_right.xml
index 60f49aa6..fa71f058 100644
--- a/res/drawable/ic_arrow_down.xml
+++ b/res/drawable/ic_arrow_right.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2018 The Android Open Source Project
+<!-- Copyright (C) 2019 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,11 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:viewportWidth="24"
- android:viewportHeight="24"
- android:width="24dp"
- android:height="24dp">
+ android:viewportWidth="48"
+ android:viewportHeight="48"
+ android:width="@dimen/primary_icon_size"
+ android:height="@dimen/primary_icon_size">
+
+ <path android:pathData="M0-.25h48v48H0z"/>
<path
- android:pathData="M7 10l5 5 5 -5z"
- android:fillColor="#000000" />
-</vector> \ No newline at end of file
+ android:fillColor="#fffafafa"
+ android:pathData="M17.17 32.92l9.17-9.17-9.17-9.17L20 11.75l12 12-12 12z"/>
+</vector>
diff --git a/res/drawable/ic_avatar_bg.xml b/res/drawable/ic_avatar_bg.xml
deleted file mode 100644
index 258e8789..00000000
--- a/res/drawable/ic_avatar_bg.xml
+++ /dev/null
@@ -1,16 +0,0 @@
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="336dp"
- android:height="480dp"
- android:viewportWidth="336.0"
- android:viewportHeight="480.0">
- <path
- android:pathData="M277.9,54.4C277.9,-5.8 227.4,-56 166.9,-56C106.4,-56 56,-5.8 56,54.4C56,114.7 106.4,164.8 166.9,164.8C227.4,164.8 277.9,114.7 277.9,54.4Z"
- android:strokeColor="@android:color/white"
- android:fillColor="@android:color/white"
- android:strokeWidth="1"/>
- <path
- android:pathData="M168,228C33.6,228 -74,291.6 -74,351.8L-74,482.3L410,482.3L410,351.8C410,294.9 302.5,228 168,228L168,228Z"
- android:strokeColor="@android:color/white"
- android:fillColor="@android:color/white"
- android:strokeWidth="1"/>
-</vector>
diff --git a/res/drawable/ic_bluetooth.xml b/res/drawable/ic_bluetooth.xml
index 01fc1159..7aca6ff6 100644
--- a/res/drawable/ic_bluetooth.xml
+++ b/res/drawable/ic_bluetooth.xml
@@ -1,24 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2018 The Android Open Source Project
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
+ http://www.apache.org/licenses/LICENSE-2.0
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="44dp"
- android:height="44dp"
+ android:width="@dimen/primary_icon_size"
+ android:height="@dimen/primary_icon_size"
android:viewportWidth="24.0"
- android:viewportHeight="24.0">
+ android:viewportHeight="24.0"
+ android:tint="@color/icon_tint_state_list">
<path
- android:fillColor="#FF000000"
- android:pathData="M14.24,12.01l2.32,2.32c0.28,-0.72 0.44,-1.51 0.44,-2.33 0,-0.82 -0.16,-1.59 -0.43,-2.31l-2.33,2.32zM19.53,6.71l-1.26,1.26c0.63,1.21 0.98,2.57 0.98,4.02s-0.36,2.82 -0.98,4.02l1.2,1.2c0.97,-1.54 1.54,-3.36 1.54,-5.31 -0.01,-1.89 -0.55,-3.67 -1.48,-5.19zM15.71,7.71L10,2L9,2v7.59L4.41,5 3,6.41 8.59,12 3,17.59 4.41,19 9,14.41L9,22h1l5.71,-5.71 -4.3,-4.29 4.3,-4.29zM11,5.83l1.88,1.88L11,9.59L11,5.83zM12.88,16.29L11,18.17v-3.76l1.88,1.88z"/>
+ android:fillColor="#FFFFFFFF"
+ android:pathData="M17.71 7.71L12 2h-1v7.59L6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 11 14.41V22h1l5.71-5.71-4.3-4.29 4.3-4.29zM13 5.83l1.88 1.88L13 9.59V5.83zm1.88 10.46L13 18.17v-3.76l1.88 1.88z"/>
</vector>
diff --git a/res/drawable/ic_dialpad_activated.xml b/res/drawable/ic_bluetooth_activatable.xml
index 42f5d8ce..26293255 100644
--- a/res/drawable/ic_dialpad_activated.xml
+++ b/res/drawable/ic_bluetooth_activatable.xml
@@ -13,10 +13,13 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<layer-list
- xmlns:android="http://schemas.android.com/apk/res/android" >
- <item
- android:drawable="@drawable/button_active_state_ring"/>
- <item
- android:drawable="@drawable/ic_dialpad_normal"/>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:width="@dimen/primary_icon_enclosing_circle_size"
+ android:height="@dimen/primary_icon_enclosing_circle_size"
+ android:drawable="@drawable/button_active_state_circle"/>
+
+ <item android:width="@dimen/primary_icon_size"
+ android:height="@dimen/primary_icon_size"
+ android:gravity="center"
+ android:drawable="@drawable/ic_bluetooth"/>
</layer-list>
diff --git a/res/drawable/ic_bluetooth_disable.xml b/res/drawable/ic_bluetooth_disable.xml
new file mode 100644
index 00000000..fbbeadaf
--- /dev/null
+++ b/res/drawable/ic_bluetooth_disable.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:width="@dimen/primary_icon_size"
+ android:height="@dimen/primary_icon_size">
+ <path
+ android:pathData="M13 5.83l1.88 1.88 -1.6 1.6 1.41 1.41L17.71 7.7 12 2l-1 0 0 5.03 2 2 0 -3.2zM5.41 4L4 5.41 10.59 12 5 17.59 6.41 19 11 14.41 11 22 12 22 16.29 17.71 18.59 20 20 18.59 5.41 4ZM13 18.17L13 14.41 14.88 16.29 13 18.17Z"
+ android:fillColor="#000000"/>
+</vector> \ No newline at end of file
diff --git a/res/drawable/ic_call_end.xml b/res/drawable/ic_call_end.xml
index 2b008b93..b97e9b20 100644
--- a/res/drawable/ic_call_end.xml
+++ b/res/drawable/ic_call_end.xml
@@ -1,7 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="44dp"
- android:height="44dp"
+ android:width="@dimen/primary_icon_size"
+ android:height="@dimen/primary_icon_size"
android:viewportWidth="24"
android:viewportHeight="24">
diff --git a/res/drawable/ic_call_end_button.xml b/res/drawable/ic_call_end_button.xml
new file mode 100644
index 00000000..d75f6e36
--- /dev/null
+++ b/res/drawable/ic_call_end_button.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <shape android:shape="oval">
+ <stroke
+ android:width="@dimen/fab_outline_thickness"
+ android:color="@color/phone_end_call"/>
+ <size
+ android:width="@dimen/fab_outline_size"
+ android:height="@dimen/fab_outline_size"/>
+ </shape>
+ </item>
+ <item android:gravity="center"
+ android:drawable="@drawable/ic_call_end"/>
+ <item>
+ <ripple android:color="@*android:color/car_card_ripple_background"
+ android:radius="@dimen/fab_ripple_radius"/>
+ </item>
+</layer-list>
diff --git a/res/drawable/ic_call_made.xml b/res/drawable/ic_call_made.xml
index 573048f4..8b721b06 100644
--- a/res/drawable/ic_call_made.xml
+++ b/res/drawable/ic_call_made.xml
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24dp"
- android:height="24dp"
+ android:width="@dimen/inline_icon_size"
+ android:height="@dimen/inline_icon_size"
android:viewportWidth="24"
android:viewportHeight="24">
diff --git a/res/drawable/ic_call_missed.xml b/res/drawable/ic_call_missed.xml
index 4ff54657..6b80ee61 100644
--- a/res/drawable/ic_call_missed.xml
+++ b/res/drawable/ic_call_missed.xml
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24dp"
- android:height="24dp"
+ android:width="@dimen/inline_icon_size"
+ android:height="@dimen/inline_icon_size"
android:viewportWidth="24"
android:viewportHeight="24">
diff --git a/res/drawable/ic_call_received.xml b/res/drawable/ic_call_received.xml
index 413a3cfd..b4f56156 100644
--- a/res/drawable/ic_call_received.xml
+++ b/res/drawable/ic_call_received.xml
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24dp"
- android:height="24dp"
+ android:width="@dimen/inline_icon_size"
+ android:height="@dimen/inline_icon_size"
android:viewportWidth="24"
android:viewportHeight="24">
diff --git a/res/drawable/ic_cancel.xml b/res/drawable/ic_cancel.xml
deleted file mode 100644
index 35749891..00000000
--- a/res/drawable/ic_cancel.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="@dimen/car_primary_icon_size"
- android:height="@dimen/car_primary_icon_size"
- android:viewportWidth="48.0"
- android:viewportHeight="48.0">
- <path
- android:pathData="M24,4C12.95,4 4,12.95 4,24s8.95,20 20,20 20,-8.95 20,-20S35.05,4 24,4zM34,31.17L31.17,34 24,26.83 16.83,34 14,31.17 21.17,24 14,16.83 16.83,14 24,21.17 31.17,14 34,16.83 26.83,24 34,31.17z"
- android:fillColor="#000000"/>
-</vector>
diff --git a/res/drawable/ic_contact.xml b/res/drawable/ic_contact.xml
index 7b547753..56785b9b 100644
--- a/res/drawable/ic_contact.xml
+++ b/res/drawable/ic_contact.xml
@@ -16,9 +16,9 @@ limitations under the License.
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:viewportWidth="48"
android:viewportHeight="48"
- android:width="48dp"
- android:height="48dp">
+ android:width="@dimen/primary_icon_size"
+ android:height="@dimen/primary_icon_size">
<path
android:pathData="M6 10l0 28c0 2.21 1.79 4 4 4l28 0c2.21 0 4 -1.79 4 -4L42 10C42 7.79 40.21 6 38 6L10 6C7.79 6 6 7.79 6 10Zm24 8c0 3.32 -2.69 6 -6 6 -3.31 0 -6 -2.68 -6 -6 0 -3.31 2.69 -6 6 -6 3.31 0 6 2.69 6 6zM12 34c0 -4 8 -6.2 12 -6.2 4 0 12 2.2 12 6.2l0 2 -24 0 0 -2z"
- android:fillColor="#000000" />
+ android:fillColor="#000000"/>
</vector> \ No newline at end of file
diff --git a/res/drawable/ic_dialpad.xml b/res/drawable/ic_dialpad.xml
index a380d62b..383eeece 100644
--- a/res/drawable/ic_dialpad.xml
+++ b/res/drawable/ic_dialpad.xml
@@ -13,12 +13,12 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<selector
- xmlns:android="http://schemas.android.com/apk/res/android" >
- <item
- android:state_activated="true"
- android:drawable="@drawable/ic_dialpad_activated" />
- <item
- android:state_activated="false"
- android:drawable="@drawable/ic_dialpad_normal" />
-</selector>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/primary_icon_size"
+ android:height="@dimen/primary_icon_size"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0"
+ android:tint="@color/icon_tint_state_list">
+ <path android:fillColor="#FFFFFFFF"
+ android:pathData="M24,38c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM12,2C9.79,2 8,3.79 8,6s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM12,14c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM12,26c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM36,10c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM24,26c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM36,26c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM36,14c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM24,14c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM24,2c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4z"/>
+</vector>
diff --git a/res/anim/telecom_slide_in.xml b/res/drawable/ic_dialpad_activatable.xml
index 5e52da4e..893c1770 100644
--- a/res/anim/telecom_slide_in.xml
+++ b/res/drawable/ic_dialpad_activatable.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2015 The Android Open Source Project
+<!-- Copyright (C) 2019 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -13,10 +13,13 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<set xmlns:android="http://schemas.android.com/apk/res/android">
- <translate
- android:fromYDelta="100%p"
- android:toYDelta="0%p"
- android:interpolator="@android:interpolator/accelerate_decelerate"
- android:duration="300" />
-</set>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:width="@dimen/primary_icon_enclosing_circle_size"
+ android:height="@dimen/primary_icon_enclosing_circle_size"
+ android:drawable="@drawable/button_active_state_circle"/>
+
+ <item android:width="@dimen/primary_icon_size"
+ android:height="@dimen/primary_icon_size"
+ android:gravity="center"
+ android:drawable="@drawable/ic_dialpad"/>
+</layer-list>
diff --git a/res/drawable/ic_dialpad_normal.xml b/res/drawable/ic_dialpad_normal.xml
deleted file mode 100644
index be92de7f..00000000
--- a/res/drawable/ic_dialpad_normal.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2018 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<vector android:height="44dp" android:viewportHeight="48.0"
- android:viewportWidth="48.0" android:width="44dp"
- xmlns:android="http://schemas.android.com/apk/res/android">
- <path android:fillColor="#000000"
- android:pathData="M24,38c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM12,2C9.79,2 8,3.79 8,6s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM12,14c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM12,26c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM36,10c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM24,26c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM36,26c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM36,14c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM24,14c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM24,2c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4z"/>
-</vector>
diff --git a/res/drawable/ic_favorite.xml b/res/drawable/ic_favorite.xml
index 5c6b6556..78428b97 100644
--- a/res/drawable/ic_favorite.xml
+++ b/res/drawable/ic_favorite.xml
@@ -14,11 +14,11 @@
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24dp"
- android:height="24dp"
+ android:width="@dimen/primary_icon_size"
+ android:height="@dimen/primary_icon_size"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
- android:pathData="M12,21.35l-1.45,-1.32C5.4,15.36 2,12.28 2,8.5 2,5.42 4.42,3 7.5,3c1.74,0 3.41,0.81 4.5,2.09C13.09,3.81 14.76,3 16.5,3 19.58,3 22,5.42 22,8.5c0,3.78 -3.4,6.86 -8.55,11.54L12,21.35z"/>
+ android:pathData="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/>
</vector>
diff --git a/res/values-sw400dp/dimens.xml b/res/drawable/ic_favorite_activatable.xml
index 9eca8ff3..72855133 100644
--- a/res/values-sw400dp/dimens.xml
+++ b/res/drawable/ic_favorite_activatable.xml
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='UTF-8'?>
-<!-- Copyright (C) 2015 The Android Open Source Project
+<!-- Copyright (C) 2019 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -13,7 +13,9 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<resources>
-
- <!-- Companion App -->
- </resources> \ No newline at end of file
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/ic_favorite_empty"
+ android:state_activated="false"/>
+ <item android:drawable="@drawable/ic_favorite"
+ android:state_activated="true"/>
+</selector>
diff --git a/res/drawable/ic_down_outlined.xml b/res/drawable/ic_favorite_empty.xml
index 162fe4ad..2e2863f4 100644
--- a/res/drawable/ic_down_outlined.xml
+++ b/res/drawable/ic_favorite_empty.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2015 The Android Open Source Project
+<!-- Copyright (C) 2019 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,11 +14,11 @@
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="48dp"
- android:height="48dp"
+ android:width="@dimen/primary_icon_size"
+ android:height="@dimen/primary_icon_size"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
- android:pathData="M11.909,19C11.449,19 11.02,18.726 10.761,18.267L4.201,6.852C3.892,6.308 3.996,5.851 4.138,5.6C4.451,5.044 5.053,5 5.309,5L18.439,5C18.743,5 19.308,5.044 19.615,5.585C19.754,5.831 19.855,6.258 19.554,6.795L12.987,18.322C12.752,18.739 12.348,19 11.909,19L11.909,19ZM6.471,7.098L11.89,16.528L17.268,7.098L6.471,7.098Z"
- android:fillColor="#000000"/>
+ android:fillColor="#FF000000"
+ android:pathData="M22 9.24l-7.19-.62L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21 12 17.27 18.18 21l-1.63-7.03L22 9.24zM12 15.4l-3.76 2.27 1-4.28-3.32-2.88 4.38-.38L12 6.1l1.71 4.04 4.38.38-3.32 2.88 1 4.28L12 15.4z"/>
</vector>
diff --git a/res/drawable/ic_history.xml b/res/drawable/ic_history.xml
new file mode 100644
index 00000000..e7ace86d
--- /dev/null
+++ b/res/drawable/ic_history.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:width="@dimen/primary_icon_size"
+ android:height="@dimen/primary_icon_size">
+ <path
+ android:pathData="M13 3C8.03 3 4 7.03 4 12L1 12 4.89 15.89 4.96 16.03 9 12 6 12c0 -3.87 3.13 -7 7 -7 3.87 0 7 3.13 7 7 0 3.87 -3.13 7 -7 7 -1.93 0 -3.68 -0.79 -4.94 -2.06L6.64 18.36C8.27 19.99 10.51 21 13 21c4.97 0 9 -4.03 9 -9 0 -4.97 -4.03 -9 -9 -9zm-1 5l0 5 4.28 2.54L17 14.33 13.5 12.25 13.5 8 12 8Z"
+ android:fillColor="#000000" />
+</vector> \ No newline at end of file
diff --git a/res/drawable/ic_mute.xml b/res/drawable/ic_mute.xml
index 28cbd715..9a233ce9 100644
--- a/res/drawable/ic_mute.xml
+++ b/res/drawable/ic_mute.xml
@@ -13,12 +13,12 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<selector
- xmlns:android="http://schemas.android.com/apk/res/android" >
- <item
- android:state_activated="true"
- android:drawable="@drawable/ic_mute_call_activated" />
- <item
- android:state_activated="false"
- android:drawable="@drawable/ic_mute_call_normal" />
-</selector>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/primary_icon_size"
+ android:height="@dimen/primary_icon_size"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0"
+ android:tint="@color/icon_tint_state_list">
+ <path android:fillColor="#FFFFFFFF"
+ android:pathData="M38,22h-3.4c0,1.49 -0.31,2.87 -0.87,4.1l2.46,2.46C37.33,26.61 38,24.38 38,22zM29.97,22.33c0,-0.11 0.03,-0.22 0.03,-0.33L30,10c0,-3.32 -2.69,-6 -6,-6s-6,2.68 -6,6v0.37l11.97,11.96zM8.55,6L6,8.55l12.02,12.02v1.44c0,3.31 2.67,6 5.98,6 0.45,0 0.88,-0.06 1.3,-0.15l3.32,3.32c-1.43,0.66 -3,1.03 -4.62,1.03 -5.52,0 -10.6,-4.2 -10.6,-10.2L10,22.01c0,6.83 5.44,12.47 12,13.44L22,42h4v-6.56c1.81,-0.27 3.53,-0.9 5.08,-1.81L39.45,42 42,39.46 8.55,6z"/>
+</vector>
diff --git a/res/drawable/ic_mute_call_activated.xml b/res/drawable/ic_mute_activatable.xml
index 822c7363..373aeb8d 100644
--- a/res/drawable/ic_mute_call_activated.xml
+++ b/res/drawable/ic_mute_activatable.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2018 The Android Open Source Project
+<!-- Copyright (C) 2019 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -13,10 +13,13 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<layer-list
- xmlns:android="http://schemas.android.com/apk/res/android" >
- <item
- android:drawable="@drawable/button_active_state_ring"/>
- <item
- android:drawable="@drawable/ic_mute_call_normal"/>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:width="@dimen/primary_icon_enclosing_circle_size"
+ android:height="@dimen/primary_icon_enclosing_circle_size"
+ android:drawable="@drawable/button_active_state_circle"/>
+
+ <item android:width="@dimen/primary_icon_size"
+ android:height="@dimen/primary_icon_size"
+ android:gravity="center"
+ android:drawable="@drawable/ic_mute"/>
</layer-list>
diff --git a/res/drawable/ic_mute_call_normal.xml b/res/drawable/ic_mute_call_normal.xml
deleted file mode 100644
index 78163878..00000000
--- a/res/drawable/ic_mute_call_normal.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2018 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<vector android:height="44dp" android:viewportHeight="48.0"
- android:viewportWidth="48.0" android:width="44dp"
- xmlns:android="http://schemas.android.com/apk/res/android">
- <path android:fillColor="#000000"
- android:pathData="M38,22h-3.4c0,1.49 -0.31,2.87 -0.87,4.1l2.46,2.46C37.33,26.61 38,24.38 38,22zM29.97,22.33c0,-0.11 0.03,-0.22 0.03,-0.33L30,10c0,-3.32 -2.69,-6 -6,-6s-6,2.68 -6,6v0.37l11.97,11.96zM8.55,6L6,8.55l12.02,12.02v1.44c0,3.31 2.67,6 5.98,6 0.45,0 0.88,-0.06 1.3,-0.15l3.32,3.32c-1.43,0.66 -3,1.03 -4.62,1.03 -5.52,0 -10.6,-4.2 -10.6,-10.2L10,22.01c0,6.83 5.44,12.47 12,13.44L22,42h4v-6.56c1.81,-0.27 3.53,-0.9 5.08,-1.81L39.45,42 42,39.46 8.55,6z"/>
-</vector>
diff --git a/res/drawable/ic_overflow.xml b/res/drawable/ic_overflow.xml
new file mode 100644
index 00000000..4742133d
--- /dev/null
+++ b/res/drawable/ic_overflow.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:width="@dimen/touch_target_size"
+ android:height="@dimen/touch_target_size"
+ android:drawable="?android:attr/actionBarItemBackground"/>
+ <item android:gravity="center" android:drawable="@drawable/ic_overflow_vd"/>
+</layer-list>
diff --git a/res/drawable/ic_overflow_vd.xml b/res/drawable/ic_overflow_vd.xml
new file mode 100644
index 00000000..ef0435fb
--- /dev/null
+++ b/res/drawable/ic_overflow_vd.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/primary_icon_size"
+ android:height="@dimen/primary_icon_size"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"/>
+</vector>
diff --git a/res/drawable/ic_pause.xml b/res/drawable/ic_pause.xml
index 515720e1..aa513057 100644
--- a/res/drawable/ic_pause.xml
+++ b/res/drawable/ic_pause.xml
@@ -13,9 +13,13 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
-<vector android:height="44dp" android:viewportHeight="48.0"
- android:viewportWidth="48.0" android:width="44dp"
- xmlns:android="http://schemas.android.com/apk/res/android">
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/primary_icon_size"
+ android:height="@dimen/primary_icon_size"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0"
+ android:tint="@color/icon_tint_state_list">
<path android:fillColor="#000000"
android:pathData="M12,38h8L20,10h-8v28zM28,10v28h8L36,10h-8z"/>
</vector>
diff --git a/res/anim/telecom_fade_out.xml b/res/drawable/ic_pause_activatable.xml
index 069fd69a..b4da807c 100644
--- a/res/anim/telecom_fade_out.xml
+++ b/res/drawable/ic_pause_activatable.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2015 The Android Open Source Project
+<!-- Copyright (C) 2019 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -13,11 +13,13 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<set xmlns:android="http://schemas.android.com/apk/res/android">
- <alpha
- android:fromAlpha="1"
- android:toAlpha="0"
- android:duration="600"
- android:startOffset="0"
- android:interpolator="@android:interpolator/fast_out_linear_in" />
-</set> \ No newline at end of file
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:width="@dimen/primary_icon_enclosing_circle_size"
+ android:height="@dimen/primary_icon_enclosing_circle_size"
+ android:drawable="@drawable/button_active_state_circle"/>
+
+ <item android:width="@dimen/primary_icon_size"
+ android:height="@dimen/primary_icon_size"
+ android:gravity="center"
+ android:drawable="@drawable/ic_pause"/>
+</layer-list>
diff --git a/res/drawable/ic_phone.xml b/res/drawable/ic_phone.xml
index 56db0642..3f1ebd19 100644
--- a/res/drawable/ic_phone.xml
+++ b/res/drawable/ic_phone.xml
@@ -1,7 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="44dp"
- android:height="44dp"
+ android:width="@dimen/primary_icon_size"
+ android:height="@dimen/primary_icon_size"
android:viewportWidth="24"
android:viewportHeight="24">
diff --git a/res/drawable/ic_search.xml b/res/drawable/ic_search.xml
index b85f8a5f..87e7d46d 100644
--- a/res/drawable/ic_search.xml
+++ b/res/drawable/ic_search.xml
@@ -14,13 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="@dimen/car_primary_icon_size"
- android:height="@dimen/car_primary_icon_size"
+ android:width="@dimen/primary_icon_size"
+ android:height="@dimen/primary_icon_size"
android:viewportWidth="48"
android:viewportHeight="48">
<path
- android:fillColor="@color/car_grey_50"
+ android:fillColor="@*android:color/car_grey_50"
android:pathData="M31 28h-1.59l-.55-.55C30.82 25.18 32 22.23 32 19c0-7.18-5.82-13-13-13S6 11.82 6
19s5.82 13 13 13c3.23 0 6.18-1.18 8.45-3.13l.55 .55 V31l10 9.98L40.98 38 31
28zm-12 0c-4.97 0-9-4.03-9-9s4.03-9 9-9 9 4.03 9 9-4.03 9-9 9z" />
diff --git a/res/drawable/ic_setting.xml b/res/drawable/ic_setting.xml
new file mode 100644
index 00000000..5c9e6a7b
--- /dev/null
+++ b/res/drawable/ic_setting.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/primary_icon_size"
+ android:height="@dimen/primary_icon_size"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+
+ <path
+ android:pathData="M0 0h20v20H0V0z" />
+ <path
+ android:fillColor="#fffafafa"
+ android:pathData="M21.4 14.2l-1.94-1.45c.03-.25 .04 -.5 .04 -.76s-.01-.51-.04-.76L21.4 9.8c.42-.31
+.52 -.94 .24 -1.41l-1.6-2.76c-.28-.48-.88-.7-1.36-.5l-2.14 .91
+c-.48-.37-1.01-.68-1.57-.92l-.27-2.2c-.06-.52-.56-.92-1.11-.92h-3.18c-.55 0-1.05
+.4 -1.11 .92 l-.26 2.19c-.57 .24 -1.1 .55 -1.58 .92 l-2.14-.91c-.48-.2-1.08 .02
+-1.36 .5 l-1.6 2.76c-.28 .48 -.18 1.1 .24 1.42l1.94 1.45c-.03 .24 -.04 .49 -.04
+.75 s.01 .51 .04 .76 L2.6 14.2c-.42 .31 -.52 .94 -.24 1.41l1.6 2.76c.28 .48 .88
+.7 1.36 .5 l2.14-.91c.48 .37 1.01 .68 1.57 .92 l.27 2.19c.06 .53 .56 .93 1.11
+.93 h3.18c.55 0 1.04-.4 1.11-.92l.27-2.19c.56-.24 1.09-.55 1.57-.92l2.14 .91
+c.48 .2 1.08-.02 1.36-.5l1.6-2.76c.28-.48 .18 -1.1-.24-1.42zM12 15.5c-1.93
+0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5z"/>
+</vector>
diff --git a/res/drawable/ic_smartphone.xml b/res/drawable/ic_smartphone.xml
index a15382d7..718af25a 100644
--- a/res/drawable/ic_smartphone.xml
+++ b/res/drawable/ic_smartphone.xml
@@ -1,24 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2018 The Android Open Source Project
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
+ http://www.apache.org/licenses/LICENSE-2.0
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:viewportWidth="24"
- android:viewportHeight="24"
- android:width="44dp"
- android:height="44dp">
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:width="@dimen/primary_icon_size"
+ android:height="@dimen/primary_icon_size"
+ android:tint="@color/icon_tint_state_list">
<path
android:pathData="M16 1L8 1C6.34 1 5 2.34 5 4l0 16c0 1.66 1.34 3 3 3l8 0c1.66 0 3 -1.34 3 -3L19 4C19 2.34 17.66 1 16 1Zm-2 20l-4 0 0 -1 4 0 0 1zm3.25 -3l-10.5 0 0 -14 10.5 0 0 14z"
- android:fillColor="#000000" />
-</vector> \ No newline at end of file
+ android:fillColor="#FFFFFFFF" />
+</vector>
diff --git a/res/drawable/ic_smartphone_activatable.xml b/res/drawable/ic_smartphone_activatable.xml
new file mode 100644
index 00000000..c2bd89e8
--- /dev/null
+++ b/res/drawable/ic_smartphone_activatable.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:width="@dimen/primary_icon_enclosing_circle_size"
+ android:height="@dimen/primary_icon_enclosing_circle_size"
+ android:drawable="@drawable/button_active_state_circle"/>
+
+ <item android:width="@dimen/primary_icon_size"
+ android:height="@dimen/primary_icon_size"
+ android:gravity="center"
+ android:drawable="@drawable/ic_smartphone"/>
+</layer-list>
diff --git a/res/drawable/ic_speaker_phone.xml b/res/drawable/ic_speaker_phone.xml
index ed96b698..701f693f 100644
--- a/res/drawable/ic_speaker_phone.xml
+++ b/res/drawable/ic_speaker_phone.xml
@@ -16,9 +16,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:viewportWidth="24"
android:viewportHeight="24"
- android:width="44dp"
- android:height="44dp">
+ android:width="@dimen/primary_icon_size"
+ android:height="@dimen/primary_icon_size"
+ android:tint="@color/icon_tint_state_list">
<path
android:pathData="M7 7.07L8.43 8.5C9.34 7.59 10.61 7.02 12 7.02c1.39 0 2.66 0.57 3.57 1.48L17 7.07C15.72 5.79 13.95 5 12 5 10.05 5 8.28 5.79 7 7.07ZM12 1C8.98 1 6.24 2.23 4.25 4.21L5.66 5.62C7.28 4 9.53 3 12 3c2.47 0 4.72 1 6.34 2.62L19.75 4.21C17.76 2.23 15.02 1 12 1ZM14.86 10.01L9.14 10C8.51 10 8 10.51 8 11.14l0 9.71c0 0.63 0.51 1.14 1.14 1.14l5.71 0c0.63 0 1.14 -0.51 1.14 -1.14l0 -9.71C16 10.51 15.49 10.01 14.86 10.01ZM15 20l-6 0 0 -8 6 0 0 8z"
- android:fillColor="#000000" />
-</vector> \ No newline at end of file
+ android:fillColor="#FFFFFFFF" />
+</vector>
diff --git a/res/drawable/ic_speaker_phone_activatable.xml b/res/drawable/ic_speaker_phone_activatable.xml
new file mode 100644
index 00000000..e233027d
--- /dev/null
+++ b/res/drawable/ic_speaker_phone_activatable.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:width="@dimen/primary_icon_enclosing_circle_size"
+ android:height="@dimen/primary_icon_enclosing_circle_size"
+ android:drawable="@drawable/button_active_state_circle"/>
+
+ <item android:width="@dimen/primary_icon_size"
+ android:height="@dimen/primary_icon_size"
+ android:gravity="center"
+ android:drawable="@drawable/ic_speaker_phone"/>
+</layer-list>
diff --git a/res/drawable/dialpad_button_background.xml b/res/drawable/ic_swap_calls.xml
index 07f67d2a..098ca973 100644
--- a/res/drawable/dialpad_button_background.xml
+++ b/res/drawable/ic_swap_calls.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2015 The Android Open Source Project
+<!-- Copyright (C) 2019 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -13,6 +13,15 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/primary_icon_size"
+ android:height="@dimen/primary_icon_size"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
-<ripple xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="@color/car_card_ripple_background" />
+ <path
+ android:pathData="M0 0h24v24H0z" />
+ <path
+ android:fillColor="#fffafafa"
+ android:pathData="M18 4l-4 4h3v7c0 1.1-.9 2-2 2s-2-.9-2-2V8c0-2.21-1.79-4-4-4S5 5.79 5 8v7H2l4 4 4-4H7V8c0-1.1.9-2 2-2s2 .9 2 2v7c0 2.21 1.79 4 4 4s4-1.79 4-4V8h3l-4-4z"/>
+</vector>
diff --git a/res/drawable/ic_voicemail.xml b/res/drawable/ic_voicemail.xml
deleted file mode 100644
index dea8b37a..00000000
--- a/res/drawable/ic_voicemail.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="36dp"
- android:height="36dp"
- android:viewportWidth="48.0"
- android:viewportHeight="48.0">
- <path
- android:pathData="M37,12c-6.08,0 -11,4.92 -11,11 0,2.66 0.94,5.1 2.51,7h-9.03c1.57,-1.9 2.51,-4.34 2.51,-7 0,-6.08 -4.92,-11 -11,-11S0,16.92 0,23s4.92,11 11,11h26c6.08,0 11,-4.92 11,-11s-4.92,-11 -11,-11zM11,30c-3.87,0 -7,-3.13 -7,-7s3.13,-7 7,-7 7,3.13 7,7 -3.13,7 -7,7zM37,30c-3.87,0 -7,-3.13 -7,-7s3.13,-7 7,-7 7,3.13 7,7 -3.13,7 -7,7z"
- android:fillColor="#000000"/>
-</vector>
diff --git a/res/drawable/icon_call_button.xml b/res/drawable/icon_call_button.xml
new file mode 100644
index 00000000..016f9c98
--- /dev/null
+++ b/res/drawable/icon_call_button.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <shape
+ android:shape="oval">
+ <stroke
+ android:width="@dimen/fab_outline_thickness"
+ android:color="@color/call_button_outline"/>
+ <size
+ android:width="@dimen/fab_outline_size"
+ android:height="@dimen/fab_outline_size"/>
+ </shape>
+ </item>
+ <item android:drawable="@drawable/ic_phone" android:gravity="center"/>
+ <item>
+ <ripple android:color="@*android:color/car_card_ripple_background"
+ android:radius="@dimen/fab_ripple_radius"/>
+ </item>
+</layer-list> \ No newline at end of file
diff --git a/res/drawable/list_divider.xml b/res/drawable/list_divider.xml
new file mode 100644
index 00000000..38d558f8
--- /dev/null
+++ b/res/drawable/list_divider.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<inset
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:insetLeft="@dimen/list_divider_inset"
+ android:insetRight="@dimen/list_divider_inset">
+ <shape android:shape="rectangle">
+ <solid android:color="@color/divider_color"/>
+ <size android:height="@dimen/list_divider_height"/>
+ </shape>
+</inset>
diff --git a/res/drawable/logo_avatar.xml b/res/drawable/logo_avatar.xml
deleted file mode 100644
index c2d781a0..00000000
--- a/res/drawable/logo_avatar.xml
+++ /dev/null
@@ -1,27 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2016 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="48dp"
- android:height="48dp"
- android:viewportWidth="192.0"
- android:viewportHeight="192.0">
- <path
- android:pathData="M96,85.09c13.28,0 24,-10.72 24,-24c0,-13.28 -10.72,-24 -24,-24s-24,10.72 -24,24C72,74.37 82.72,85.09 96,85.09z"
- android:fillColor="#FFFFFF"/>
- <path
- android:pathData="M96,99.27c-29.33,0 -52.36,14.18 -52.36,27.27c11.09,17.06 30.51,28.36 52.36,28.36s41.27,-11.3 52.36,-28.36C148.36,113.45 125.33,99.27 96,99.27z"
- android:fillColor="#FFFFFF"/>
-</vector>
diff --git a/res/drawable/sized_logo.xml b/res/drawable/sized_logo.xml
new file mode 100644
index 00000000..2cade0a7
--- /dev/null
+++ b/res/drawable/sized_logo.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:width="@*android:dimen/car_margin"
+ android:drawable="@android:color/transparent"/>
+
+ <item
+ android:gravity="center"
+ android:width="@dimen/primary_icon_size"
+ android:height="@dimen/primary_icon_size"
+ android:drawable="@drawable/ic_app_icon">
+ </item>
+</layer-list>
diff --git a/res/drawable/strequent_small_icon_bg.xml b/res/drawable/strequent_small_icon_bg.xml
deleted file mode 100644
index e9cb5c11..00000000
--- a/res/drawable/strequent_small_icon_bg.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2015 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
- android:shape="oval" >
- <solid android:color="#FBC02D" />
-</shape>
diff --git a/res/drawable/ongoing_call_action_background.xml b/res/layout-h610dp/contact_details_action_bar.xml
index 902cc8c1..d077bbed 100644
--- a/res/drawable/ongoing_call_action_background.xml
+++ b/res/layout-h610dp/contact_details_action_bar.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2015 The Android Open Source Project
+<!-- 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.
@@ -13,8 +13,9 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<inset
+
+<Space
xmlns:android="http://schemas.android.com/apk/res/android"
- android:inset="32dp">
- <ripple android:color="@color/car_card_ripple_background" />
-</inset> \ No newline at end of file
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+</Space> \ No newline at end of file
diff --git a/res/layout-port/dialer_fragment.xml b/res/layout-port/dialer_fragment.xml
deleted file mode 100644
index 1e3093a2..00000000
--- a/res/layout-port/dialer_fragment.xml
+++ /dev/null
@@ -1,106 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2018 The Android Open Source Project
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<!-- There seems to be a bug in layout inflation where it can't use a resource to inflate a view
- group that sets layout_marginTop with a dimension. Work around by putting in a shell layout.
--->
-<FrameLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:paddingTop="@dimen/car_app_bar_height">
- <androidx.constraintlayout.widget.ConstraintLayout
- android:id="@+id/dialer_info_fragment_container"
- android:paddingLeft="@dimen/car_margin"
- android:paddingRight="@dimen/car_margin"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
-
- <FrameLayout
- android:id="@+id/dialpad_fragment_container"
- android:layout_height="wrap_content"
- android:layout_width="wrap_content"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintEnd_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent"/>
-
- <TextView
- android:id="@+id/title"
- style="@style/TextAppearance.Car.Headline2"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginTop="@dimen/car_padding_4"
- android:gravity="center"
- android:focusable="true"
- android:maxLines="1"
- android:text="@string/dial_a_number"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent"/>
-
- <TextView
- android:id="@+id/body"
- style="@style/TextAppearance.Car.Body1"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginTop="@dimen/car_padding_3"
- android:gravity="center"
- android:visibility="gone"
- android:maxLines="1"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/title"/>
-
- <ImageButton
- android:id="@+id/call_button"
- style="@style/DialpadPrimaryButton"
- android:src="@drawable/ic_phone"
- android:layout_marginBottom="@dimen/car_padding_4"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toStartOf="@+id/delete_button"
- app:layout_constraintHorizontal_chainStyle="packed"
- app:layout_constraintStart_toStartOf="parent"/>
-
- <ImageButton
- android:id="@+id/delete_button"
- style="@style/DialpadSecondaryButton"
- android:layout_marginBottom="@dimen/car_padding_4"
- android:layout_marginStart="@dimen/car_padding_6"
- android:src="@drawable/ic_backspace"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toEndOf="@+id/call_button"/>
-
- <ImageButton
- android:id="@+id/end_call_button"
- style="@style/DialpadPrimaryButton"
- android:layout_marginBottom="@dimen/car_padding_4"
- android:visibility="gone"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toStartOf="@+id/mute_button"
- app:layout_constraintHorizontal_chainStyle="packed"
- app:layout_constraintStart_toStartOf="parent"/>
-
- <ImageButton
- android:id="@+id/mute_button"
- style="@style/DialpadSecondaryButton"
- android:layout_marginBottom="@dimen/car_padding_4"
- android:layout_marginStart="@dimen/car_padding_6"
- android:src="@drawable/ic_mute_call_normal"
- android:visibility="gone"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toEndOf="@+id/end_call_button"/>
- </androidx.constraintlayout.widget.ConstraintLayout>
-</FrameLayout> \ No newline at end of file
diff --git a/res/layout-port/dialpad_fragment.xml b/res/layout-port/dialpad_fragment.xml
new file mode 100644
index 00000000..8eef7c94
--- /dev/null
+++ b/res/layout-port/dialpad_fragment.xml
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 The Android Open Source Project
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <fragment
+ android:id="@+id/dialpad_fragment"
+ android:name="com.android.car.dialer.ui.dialpad.KeypadFragment"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:layout_marginTop="@dimen/keypad_margin"
+ app:layout_constraintTop_toBottomOf="@+id/title"
+ app:layout_constraintBottom_toTopOf="@+id/call_button"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"/>
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:focusable="true"
+ android:singleLine="true"
+ android:textAppearance="@style/TextAppearance.DialNumber"
+ android:layout_marginStart="@dimen/dialpad_info_edge_padding_size"
+ app:layout_constraintVertical_chainStyle="packed"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toTopOf="@id/dialpad_fragment"
+ app:layout_constraintStart_toStartOf="@id/dialpad_fragment"
+ app:layout_constraintEnd_toStartOf="@+id/delete_button"
+ app:layout_goneMarginEnd="@dimen/dialpad_info_edge_padding_size"/>
+
+ <ImageButton
+ android:id="@+id/delete_button"
+ style="@style/DialpadSecondaryButton"
+ android:src="@drawable/ic_backspace"
+ android:layout_marginStart="@dimen/dialpad_info_title_padding_size"
+ android:layout_marginEnd="@dimen/dialpad_info_edge_padding_size"
+ app:layout_constraintTop_toTopOf="@id/title"
+ app:layout_constraintBottom_toBottomOf="@id/title"
+ app:layout_constraintStart_toEndOf="@id/title"
+ app:layout_constraintEnd_toEndOf="@id/dialpad_fragment"/>
+
+ <TextView
+ android:id="@+id/display_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="@style/TextAppearance.DialpadDisplayName"
+ android:focusable="true"
+ android:singleLine="true"
+ app:layout_constraintTop_toBottomOf="@id/title"
+ app:layout_constraintBottom_toTopOf="@id/dialpad_fragment"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"/>
+
+ <ImageView
+ android:id="@+id/call_button"
+ style="@style/DialpadPrimaryButton"
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/call_button_height"
+ android:src="@drawable/icon_call_button"
+ android:layout_marginBottom="@dimen/call_button_bottom_margin"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"/>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/res/layout-port/in_call_fragment.xml b/res/layout-port/in_call_fragment.xml
deleted file mode 100644
index bc8f55a8..00000000
--- a/res/layout-port/in_call_fragment.xml
+++ /dev/null
@@ -1,53 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2018 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<androidx.constraintlayout.widget.ConstraintLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="@color/phone_theme_secondary"
- android:orientation="vertical">
-
- <FrameLayout
- android:id="@+id/dialer_container"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:visibility="gone"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"/>
-
- <FrameLayout
- android:id="@+id/user_profile_container"
- android:layout_width="match_parent"
- android:layout_height="0dp"
- app:layout_constraintBottom_toTopOf="@+id/controller_bar_container"
- app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent">
- <include layout="@layout/user_profile_large"/>
- </FrameLayout>
-
- <FrameLayout
- android:id="@+id/controller_bar_container"
- android:layout_width="match_parent"
- android:layout_height="@dimen/car_action_bar_height"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"/>
-</androidx.constraintlayout.widget.ConstraintLayout
-> \ No newline at end of file
diff --git a/res/layout-port/incall_dialpad_fragment.xml b/res/layout-port/incall_dialpad_fragment.xml
new file mode 100644
index 00000000..5d05dcb8
--- /dev/null
+++ b/res/layout-port/incall_dialpad_fragment.xml
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <Chronometer
+ android:id="@+id/call_state"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:textAppearance="@style/TextAppearance.InCallState"
+ android:singleLine="true"
+ android:gravity="center"
+ android:layout_marginStart="@dimen/dialpad_info_edge_padding_size"
+ android:layout_marginEnd="@dimen/dialpad_info_edge_padding_size"
+ app:layout_constraintVertical_chainStyle="packed"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toTopOf="@id/dialpad_fragment"
+ app:layout_constraintStart_toStartOf="@id/dialpad_fragment"
+ app:layout_constraintEnd_toEndOf="@id/dialpad_fragment"/>
+
+ <fragment
+ android:id="@+id/dialpad_fragment"
+ android:name="com.android.car.dialer.ui.dialpad.KeypadFragment"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:layout_marginTop="@dimen/keypad_margin"
+ app:layout_constraintTop_toBottomOf="@+id/call_state"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"/>
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:focusable="true"
+ android:singleLine="true"
+ android:gravity="end"
+ android:textAppearance="@style/TextAppearance.DialNumber"
+ android:layout_marginStart="@dimen/dialpad_info_edge_padding_size"
+ android:layout_marginEnd="@dimen/dialpad_info_edge_padding_size"
+ app:layout_constrainedWidth="true"
+ app:layout_constraintTop_toBottomOf="@id/call_state"
+ app:layout_constraintBottom_toTopOf="@id/dialpad_fragment"
+ app:layout_constraintStart_toStartOf="@id/dialpad_fragment"
+ app:layout_constraintEnd_toEndOf="@id/dialpad_fragment"/>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/res/layout-port/top_bar_with_tabs.xml b/res/layout-port/top_bar_with_tabs.xml
new file mode 100644
index 00000000..34ef463f
--- /dev/null
+++ b/res/layout-port/top_bar_with_tabs.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:id="@+id/car_toolbar_container"
+ android:background="@color/app_bar_background_color"
+ android:orientation="vertical">
+ <com.android.car.apps.common.ClickThroughToolbar
+ android:id="@+id/car_toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="?android:attr/actionBarSize"/>
+
+ <com.android.car.apps.common.widget.CarTabLayout
+ android:id="@+id/tab_layout"
+ android:layout_width="match_parent"
+ android:layout_height="?android:attr/actionBarSize"/>
+</LinearLayout>
diff --git a/res/layout-port/user_profile_large.xml b/res/layout-port/user_profile_large.xml
new file mode 100644
index 00000000..02d7e807
--- /dev/null
+++ b/res/layout-port/user_profile_large.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center"
+ android:orientation="vertical">
+ <ImageView
+ android:id="@+id/user_profile_avatar"
+ android:layout_width="@dimen/in_call_avatar_icon_size"
+ android:layout_height="@dimen/in_call_avatar_icon_size"
+ android:scaleType="fitCenter" />
+ <TextView
+ android:id="@+id/user_profile_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="@style/TextAppearance.InCallUserTitle"
+ android:paddingTop="@dimen/in_call_margin_between_avatar_and_text"
+ android:singleLine="true"/>
+ <TextView
+ android:id="@+id/user_profile_phone_number"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="@style/TextAppearance.InCallUserPhoneNumber"
+ android:singleLine="true"
+ android:paddingTop="@dimen/in_call_phone_number_margin_top"/>
+ <Chronometer
+ android:id="@+id/user_profile_call_state"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="@style/TextAppearance.InCallState"
+ android:singleLine="true"
+ android:paddingTop="@dimen/in_call_state_margin_top"/>
+</LinearLayout>
diff --git a/res/layout/audio_route_list_item.xml b/res/layout/audio_route_list_item.xml
index 6d55cb8a..c1510ef6 100644
--- a/res/layout/audio_route_list_item.xml
+++ b/res/layout/audio_route_list_item.xml
@@ -17,21 +17,22 @@ limitations under the License.
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
- android:layout_height="@dimen/car_action_bar_height"
- android:background="@color/phone_theme"
- android:elevation="@dimen/in_call_card_elevation">
+ android:layout_height="@dimen/audio_route_height"
+ android:background="?android:attr/selectableItemBackground"
+ android:elevation="@dimen/dialer_card_elevation">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/text_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
- app:layout_constraintGuide_begin="124dp"/>
+ app:layout_constraintGuide_begin="@dimen/audio_route_constraint_guide_begin"/>
<ImageView
android:id="@+id/icon"
- android:layout_width="@dimen/car_app_icon_size"
- android:layout_height="@dimen/car_app_icon_size"
+ android:layout_width="@dimen/audio_route_icon_size"
+ android:layout_height="@dimen/audio_route_icon_size"
android:scaleType="fitCenter"
- android:tint="@color/primary_icon_color"
+ android:layout_marginStart="@dimen/audio_route_content_padding"
+ android:tint="@color/icon_accent_activatable"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
@@ -39,7 +40,8 @@ limitations under the License.
android:id="@+id/body"
android:layout_width="0dp"
android:layout_height="wrap_content"
- style="@style/TextAppearance.Car.Body1"
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ android:textColor="@color/text_accent_activatable"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="@+id/text_start"
app:layout_constraintEnd_toEndOf="parent"
diff --git a/res/layout/audio_route_switch_dialog.xml b/res/layout/audio_route_switch_dialog.xml
index 000021ce..7a5013c7 100644
--- a/res/layout/audio_route_switch_dialog.xml
+++ b/res/layout/audio_route_switch_dialog.xml
@@ -13,21 +13,9 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<androidx.cardview.widget.CardView
+<androidx.recyclerview.widget.RecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
- app:cardBackgroundColor="@color/phone_theme"
- app:contentPadding="@dimen/car_keyline_1"
- app:cardCornerRadius="@dimen/car_radius_3"
- app:cardElevation="@dimen/car_action_bar_elevation">
-
- <androidx.car.widget.PagedListView
- android:id="@+id/list"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:clipChildren="false"
- app:scrollBarEnabled="false"
- app:gutter="none"/>
-</androidx.cardview.widget.CardView> \ No newline at end of file
+ android:clipChildren="false"/>
diff --git a/res/layout/call_history_list_item.xml b/res/layout/call_history_list_item.xml
index 3033144e..e2fd81e4 100644
--- a/res/layout/call_history_list_item.xml
+++ b/res/layout/call_history_list_item.xml
@@ -17,26 +17,104 @@ limitations under the License.
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
- android:layout_height="@dimen/car_action_bar_height"
- android:background="@color/phone_theme"
- android:paddingRight="@dimen/car_keyline_1"
- android:paddingLeft="@dimen/car_keyline_1"
- android:elevation = "@dimen/in_call_card_elevation">
+ android:layout_height="@dimen/call_history_item_height">
<androidx.constraintlayout.widget.Guideline
- android:id="@+id/list_item_left_edge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ android:id="@+id/guideline_begin"
android:orientation="vertical"
- app:layout_constraintGuide_begin="@dimen/car_keyline_1" />
+ app:layout_constraintGuide_begin="@dimen/call_history_guideline_begin"/>
+
+ <androidx.constraintlayout.widget.Guideline
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/guideline_end"
+ android:orientation="vertical"
+ app:layout_constraintGuide_end="@dimen/call_history_guideline_end"/>
+
+ <View
+ android:id="@+id/call_action_id"
+ android:background="?android:attr/selectableItemBackground"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/guideline_end"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"/>
<ImageView
- android:id="@+id/avatar"
- android:layout_width="@dimen/car_avatar_size"
- android:layout_height="@dimen/car_avatar_size"
+ android:id="@+id/icon"
+ android:layout_width="@dimen/avatar_icon_size"
+ android:layout_height="@dimen/avatar_icon_size"
+ android:layout_marginStart="@dimen/call_history_item_padding"
+ android:scaleType="centerCrop"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"/>
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="@dimen/call_history_text_margin_end"
+ android:singleLine="true"
+ app:layout_constraintVertical_chainStyle="packed"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toTopOf="@+id/text"
+ app:layout_constraintStart_toStartOf="@id/guideline_begin"
+ app:layout_constraintEnd_toEndOf="@id/guideline_end"/>
+
+ <com.android.car.dialer.widget.CallTypeIconsView
+ android:id="@+id/call_type_icons"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintStart_toStartOf="@id/guideline_begin"
+ app:layout_constraintTop_toBottomOf="@id/title"/>
+
+ <TextView
+ android:id="@+id/call_count_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/call_history_icons_margin"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:singleLine="true"
+ app:layout_constraintTop_toBottomOf="@id/title"
+ app:layout_constraintStart_toEndOf="@id/call_type_icons"
+ app:layout_constraintEnd_toStartOf="@+id/text"/>
+
+ <TextView
+ android:id="@id/text"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/call_history_icons_margin"
+ android:layout_marginEnd="@dimen/call_history_text_margin_end"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:singleLine="true"
+ app:layout_constraintTop_toBottomOf="@id/title"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toEndOf="@id/call_count_text"
+ app:layout_constraintEnd_toEndOf="@id/guideline_end"/>
+
+ <View
+ android:id="@+id/divider"
+ android:layout_width="@dimen/vertical_divider_width"
+ android:layout_height="match_parent"
+ android:background="@color/divider_color"
+ android:layout_marginTop="@dimen/vertical_divider_inset"
+ android:layout_marginBottom="@dimen/vertical_divider_inset"
+ app:layout_constraintStart_toStartOf="@id/guideline_end"/>
+
+ <ImageView
+ android:id="@+id/calllog_action_button"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:src="@drawable/ic_arrow_right"
android:scaleType="center"
+ android:tint="@color/secondary_icon_color"
+ android:background="?android:attr/selectableItemBackground"
+ app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintEnd_toStartOf="@+id/toggle_dialpad_button"
- app:layout_constraintTop_toTopOf="parent"/>
+ app:layout_constraintStart_toEndOf="@id/guideline_end"
+ app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/res/layout/call_log_list_item_card.xml b/res/layout/call_log_list_item_card.xml
deleted file mode 100644
index c2313e1e..00000000
--- a/res/layout/call_log_list_item_card.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2015 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<androidx.cardview.widget.CardView
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:id="@+id/call_log_card"
- android:foreground="@drawable/dialer_ripple_background"
- android:layout_gravity="center"
- android:layout_width="match_parent"
- android:layout_height="@dimen/call_log_item_height"
- app:cardBackgroundColor="@color/phone_theme"
- app:cardCornerRadius="@dimen/favorite_card_corner_radius"
- app:cardElevation="@dimen/car_action_bar_elevation">
-
- <include layout="@layout/call_log_list_item_card_base"/>
-</androidx.cardview.widget.CardView>
diff --git a/res/layout/call_log_list_item_card_base.xml b/res/layout/call_log_list_item_card_base.xml
deleted file mode 100644
index 6bb77eb9..00000000
--- a/res/layout/call_log_list_item_card_base.xml
+++ /dev/null
@@ -1,101 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2015 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<!-- The height + bottomMargin should equal car_paged_list_view_row_height -->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/container"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:duplicateParentState="true"
- android:orientation="horizontal">
-
- <FrameLayout
- android:id="@+id/icon_container"
- android:layout_width="@dimen/car_keyline_2"
- android:layout_height="@dimen/icon_container_height"
- android:layout_gravity="center_vertical"
- android:layout_marginStart="16dp"
- android:layout_marginEnd="8dp" >
- <ImageView
- android:id="@+id/icon"
- android:layout_width="@dimen/call_log_icon_size"
- android:layout_height="@dimen/call_log_icon_size"
- android:layout_gravity="center_vertical|start"
- android:layout_marginStart="@dimen/car_keyline_1"
- android:scaleType="centerCrop" />
- <ImageView
- android:id="@+id/small_icon"
- android:layout_width="28dp"
- android:layout_height="28dp"
- android:padding="4dp"
- android:scaleType="centerInside"
- android:layout_gravity="bottom|end"
- android:background="@drawable/strequent_small_icon_bg"
- android:visibility="gone" />
- </FrameLayout>
-
- <LinearLayout
- android:id="@+id/text_container"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_gravity="center_vertical"
- android:duplicateParentState="true"
- android:layout_weight="1"
- android:orientation="vertical">
-
- <TextView
- android:id="@+id/title"
- style="@style/TextAppearance.Car.Body1"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginBottom="@dimen/car_text_vertical_margin"
- android:layout_marginEnd="@dimen/car_keyline_1"
- android:ellipsize="end"
- android:maxLines="1" />
-
- <LinearLayout
- android:id="@+id/call_type"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:duplicateParentState="true"
- android:orientation="horizontal">
-
- <com.android.car.dialer.CallTypeIconsView
- android:id="@+id/call_type_icons"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center_vertical"
- android:layout_marginEnd="@dimen/call_log_icon_margin"
- android:visibility="gone"/>
-
- <TextView
- android:id="@+id/text"
- style="@style/TextAppearance.Car.Body2"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center_vertical" />
- </LinearLayout>
- </LinearLayout>
-
- <ImageView
- android:id="@+id/right_icon"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center_vertical"
- android:layout_marginEnd="@dimen/car_padding_4"
- android:scaleType="center"
- android:visibility="gone" />
-
-</LinearLayout>
diff --git a/res/layout/call_log_list_item_empty.xml b/res/layout/call_log_list_item_empty.xml
deleted file mode 100644
index 88d1aa1a..00000000
--- a/res/layout/call_log_list_item_empty.xml
+++ /dev/null
@@ -1,45 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-<LinearLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/container"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:layout_marginStart="16dp"
- android:layout_marginEnd="16dp"
- android:focusable="false"
- android:orientation="vertical"
- android:background="@drawable/car_list_item_background" >
- <FrameLayout
- android:id="@+id/icon_container"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:visibility="visible">
- <ImageView
- android:id="@+id/icon"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center_horizontal"
- android:layout_marginTop="48dp"
- android:layout_marginBottom="22dp" />
- </FrameLayout>
- <TextView
- android:id="@+id/title"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:gravity="center"
- style="@style/TextAppearance.Car.Body1" />
-</LinearLayout>
diff --git a/res/layout/call_type_icons.xml b/res/layout/call_type_icons.xml
deleted file mode 100644
index 068bc2eb..00000000
--- a/res/layout/call_type_icons.xml
+++ /dev/null
@@ -1,41 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2015 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:orientation="horizontal" >
-
- <ImageView
- android:id="@+id/call_type_icon_0"
- android:layout_width="28dp"
- android:layout_height="28dp"
- android:layout_gravity="center_vertical"
- android:layout_marginRight="@dimen/call_log_icon_margin"/>
-
- <ImageView
- android:id="@+id/call_type_icon_1"
- android:layout_width="28dp"
- android:layout_height="28dp"
- android:layout_gravity="center_vertical"
- android:layout_marginRight="@dimen/call_log_icon_margin"/>
-
- <ImageView
- android:id="@+id/call_type_icon_2"
- android:layout_width="28dp"
- android:layout_height="28dp"
- android:layout_gravity="center_vertical"
- android:layout_marginRight="@dimen/call_log_icon_margin"/>
-</LinearLayout> \ No newline at end of file
diff --git a/res/layout/contact_detail_card_content.xml b/res/layout/contact_detail_card_content.xml
deleted file mode 100644
index 3e2d8011..00000000
--- a/res/layout/contact_detail_card_content.xml
+++ /dev/null
@@ -1,55 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-<FrameLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="@dimen/car_single_line_list_item_height"
- android:layout_marginStart="@dimen/car_keyline_1"
- android:layout_marginEnd="@dimen/car_keyline_1"
- android:focusable="true"
- android:backgroundTint="@color/contact_detail_card_background_color"
- android:background="@drawable/car_list_item_background">
- <ImageView
- android:id="@+id/icon"
- android:layout_width="@dimen/contact_detail_image_size"
- android:layout_height="@dimen/contact_detail_image_size"
- android:layout_gravity="center_vertical"
- android:scaleType="centerCrop"/>
-
- <LinearLayout
- android:id="@+id/text_container"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center_vertical"
- android:orientation="vertical"
- android:layout_marginStart="@dimen/car_keyline_2">
- <TextView
- android:id="@+id/title"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginBottom="@dimen/car_padding_1"
- android:ellipsize="end"
- android:maxLines="1"
- style="@style/TextAppearance.Car.Body1"/>
- <TextView
- android:id="@+id/text"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:ellipsize="end"
- android:maxLines="1"
- style="@style/TextAppearance.Car.Body2"/>
- </LinearLayout>
-</FrameLayout>
diff --git a/res/layout/contact_detail_name_image.xml b/res/layout/contact_detail_name_image.xml
deleted file mode 100644
index 365260d6..00000000
--- a/res/layout/contact_detail_name_image.xml
+++ /dev/null
@@ -1,74 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-<!-- This FrameLayout is used to center the inner layout. -->
-<FrameLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:focusable="true">
-
- <!-- Wraps everything in a card -->
- <androidx.car.widget.ColumnCardView
- android:id="@+id/card"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:backgroundTint="@color/contact_detail_card_background_color"
- android:layout_gravity="center">
-
- <!-- Used to provide common margins and also allow for the textview to set its right margin
- to the width of the image to allow it to ellipsize.
- -->
- <androidx.constraintlayout.widget.ConstraintLayout
- android:layout_width="match_parent"
- android:layout_height="@dimen/contact_detail_name_card_height"
- android:layout_marginLeft="@dimen/car_keyline_1"
- android:layout_marginRight="@dimen/car_keyline_1">
-
- <ImageView
- android:id="@+id/avatar"
- android:layout_width="@dimen/contact_detail_image_size"
- android:layout_height="@dimen/contact_detail_image_size"
- android:background="@drawable/avatar_rounded_bg"
- app:layout_constraintVertical_chainStyle="packed"
- app:layout_constraintBottom_toTopOf="@+id/title"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintTop_toTopOf="parent"/>
-
- <TextView
- android:id="@+id/title"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:singleLine="true"
- android:ellipsize="end"
- android:layout_marginTop="@dimen/car_padding_4"
- style="@style/TextAppearance.Car.Body1"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/avatar"/>
- </androidx.constraintlayout.widget.ConstraintLayout>
- <View
- android:id="@+id/divider"
- android:layout_width="match_parent"
- android:layout_height="@dimen/car_list_divider_height"
- android:layout_marginStart="@dimen/car_keyline_1"
- android:layout_marginEnd="@dimen/car_keyline_1"
- android:background="@color/car_list_divider"
- android:layout_gravity="bottom"/>
- </androidx.car.widget.ColumnCardView>
-</FrameLayout>
diff --git a/res/layout/contact_details.xml b/res/layout/contact_details.xml
deleted file mode 100644
index 602f5729..00000000
--- a/res/layout/contact_details.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-<FrameLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="@color/phone_theme_secondary"
- android:clickable="true"
- android:clipChildren="false">
-
- <androidx.car.widget.PagedListView
- android:id="@+id/list_view"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:clipChildren="false"
- app:showPagedListViewDivider="false" />
-</FrameLayout>
diff --git a/res/layout/call_list_fragment.xml b/res/layout/contact_details_action_bar.xml
index a1f4d3c5..59ad063a 100644
--- a/res/layout/call_list_fragment.xml
+++ b/res/layout/contact_details_action_bar.xml
@@ -13,17 +13,25 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<FrameLayout
+
+<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:background="@color/call_list_fragment_background"
- android:clipChildren="false">
+ android:orientation="horizontal"
+ android:gravity="center_vertical">
+
+ <ImageView
+ android:id="@+id/contact_details_action_bar_avatar"
+ android:layout_height="@dimen/contact_details_avatar_size"
+ android:layout_width="@dimen/contact_details_avatar_size"/>
+
+ <TextView
+ android:id="@+id/contact_details_action_bar_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/contact_details_action_bar_name_margin"
+ android:textAppearance="@style/TextAppearance.Body1"
+ android:singleLine="true"/>
- <androidx.car.widget.PagedListView
- android:id="@+id/list_view"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:layout_marginTop="@dimen/car_app_bar_height"
- android:clipChildren="false" />
-</FrameLayout> \ No newline at end of file
+</LinearLayout>
diff --git a/res/layout/contact_details_name_image.xml b/res/layout/contact_details_name_image.xml
new file mode 100644
index 00000000..46682d1a
--- /dev/null
+++ b/res/layout/contact_details_name_image.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <ImageView
+ android:id="@+id/avatar"
+ android:layout_width="@dimen/contact_details_avatar_size"
+ android:layout_height="@dimen/contact_details_avatar_size"
+ android:layout_marginTop="@dimen/contact_details_avatar_margin_top"
+ app:layout_constraintVertical_chainStyle="packed"
+ app:layout_constraintBottom_toTopOf="@+id/title"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"/>
+
+ <TextView
+ android:id="@id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:layout_marginTop="@dimen/contact_details_title_margin_top"
+ android:layout_marginBottom="@dimen/contact_details_title_margin_bottom"
+ android:textAppearance="@style/TextAppearance.ContactDetailsTitle"
+ app:layout_constraintTop_toBottomOf="@id/avatar"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"/>
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/res/layout/contact_details_number.xml b/res/layout/contact_details_number.xml
index 94c49dbb..3a18d9d3 100644
--- a/res/layout/contact_details_number.xml
+++ b/res/layout/contact_details_number.xml
@@ -13,28 +13,63 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<!-- This FrameLayout is used to center the inner layout. -->
-<FrameLayout
+<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:focusable="true">
+ android:layout_height="@dimen/contact_details_item_height">
- <androidx.car.widget.ColumnCardView
- android:id="@+id/card"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center">
+ <LinearLayout
+ android:id="@+id/call_action_id"
+ android:background="?android:attr/selectableItemBackground"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:paddingStart="@dimen/contact_details_number_padding_start"
+ android:paddingEnd="@dimen/contact_details_number_padding_end"
+ android:gravity="center_vertical"
+ android:orientation="vertical"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/divider"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent">
- <include layout="@layout/contact_detail_card_content"/>
+ <TextView
+ android:id="@+id/title"
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:singleLine="true"/>
- <View
- android:id="@+id/divider"
+ <TextView
+ android:id="@id/text"
+ android:textAppearance="?android:attr/textAppearanceSmall"
android:layout_width="match_parent"
- android:layout_height="@dimen/car_list_divider_height"
- android:layout_marginStart="@dimen/car_keyline_1"
- android:layout_marginEnd="@dimen/car_keyline_1"
- android:background="@color/car_list_divider"
- android:layout_gravity="bottom"/>
- </androidx.car.widget.ColumnCardView>
-</FrameLayout>
+ android:layout_height="wrap_content"
+ android:singleLine="true"/>
+
+ </LinearLayout>
+
+ <!-- Divider line separating the text and the text icon -->
+ <View
+ android:id="@+id/divider"
+ android:layout_width="@dimen/vertical_divider_width"
+ android:layout_height="match_parent"
+ android:background="@color/divider_color"
+ android:layout_marginTop="@dimen/vertical_divider_inset"
+ android:layout_marginBottom="@dimen/vertical_divider_inset"
+ android:layout_marginEnd="@dimen/contact_details_text_button_guideline"
+ app:layout_constraintEnd_toEndOf="parent"/>
+
+ <ImageView
+ android:id="@+id/contact_details_favorite_button"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:scaleType="center"
+ android:src="@drawable/ic_favorite_activatable"
+ android:tint="@color/contact_details_icon_tint"
+ android:background="?android:attr/selectableItemBackground"
+ app:layout_constraintStart_toStartOf="@id/divider"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"/>
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/res/layout/contact_list_fragment.xml b/res/layout/contact_list_fragment.xml
deleted file mode 100644
index 67595aa7..00000000
--- a/res/layout/contact_list_fragment.xml
+++ /dev/null
@@ -1,74 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2018 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<FrameLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="@color/contact_fragment_background"
- android:clipChildren="false">
-
- <androidx.car.widget.PagedListView
- android:id="@+id/list_view"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:layout_marginTop="@dimen/car_app_bar_height"
- android:clipChildren="false"/>
-
- <FrameLayout
- android:id="@+id/contact_detail_container"
- android:background="@color/contact_detail_fragment_background_color"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:visibility="gone">
- <androidx.constraintlayout.widget.ConstraintLayout
- android:layout_width="match_parent"
- android:layout_height="@dimen/car_app_bar_height"
- android:background="@color/phone_theme_secondary">
- <ImageView
- android:id="@+id/back_button"
- android:layout_width="@dimen/car_touch_target_size"
- android:layout_height="@dimen/car_touch_target_size"
- android:scaleType="center"
- android:src="@drawable/ic_arrow_back"
- android:tint="@color/car_tint"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintEnd_toEndOf="@+id/margin_guideline"
- app:layout_constraintTop_toTopOf="parent"/>
- <androidx.constraintlayout.widget.Guideline
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:id="@+id/margin_guideline"
- app:layout_constraintGuide_begin="@dimen/car_margin"
- android:orientation="vertical"/>
- <TextView
- android:id="@+id/title"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- style="@style/TextAppearance.Car.Body1"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintStart_toEndOf="@+id/back_button"
- app:layout_constraintTop_toTopOf="parent"/>
- </androidx.constraintlayout.widget.ConstraintLayout>
- <FrameLayout
- android:id="@+id/contact_detail_fragment_container"
- android:layout_marginTop="@dimen/car_app_bar_height"
- android:background="@android:color/transparent"
- android:layout_width="match_parent"
- android:layout_height="match_parent"/>
- </FrameLayout>
-</FrameLayout> \ No newline at end of file
diff --git a/res/layout/contact_list_item.xml b/res/layout/contact_list_item.xml
new file mode 100644
index 00000000..e106ab2f
--- /dev/null
+++ b/res/layout/contact_list_item.xml
@@ -0,0 +1,102 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/contact_list_item_height">
+
+ <androidx.constraintlayout.widget.Guideline
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/contact_list_guideline_begin"
+ android:orientation="vertical"
+ app:layout_constraintGuide_begin="@dimen/contact_list_guideline_begin"/>
+
+ <androidx.constraintlayout.widget.Guideline
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/contact_list_guideline_end"
+ android:orientation="vertical"
+ app:layout_constraintGuide_end="@dimen/contact_list_guideline_end"/>
+
+ <View
+ android:id="@+id/call_action_id"
+ android:background="?android:attr/selectableItemBackground"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/contact_list_guideline_end"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"/>
+
+ <ImageView
+ android:id="@+id/icon"
+ android:layout_width="@dimen/avatar_icon_size"
+ android:layout_height="@dimen/avatar_icon_size"
+ android:layout_marginStart="@dimen/contact_list_item_padding"
+ android:scaleType="centerCrop"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"/>
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="@dimen/contact_list_text_margin_end"
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ android:singleLine="true"
+ app:layout_constraintVertical_chainStyle="packed"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toTopOf="@+id/text"
+ app:layout_constraintStart_toStartOf="@id/contact_list_guideline_begin"
+ app:layout_constraintEnd_toEndOf="@id/contact_list_guideline_end"/>
+
+ <TextView
+ android:id="@id/text"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="@dimen/contact_list_text_margin_end"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:singleLine="true"
+ app:layout_constraintTop_toBottomOf="@id/title"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="@id/contact_list_guideline_begin"
+ app:layout_constraintEnd_toEndOf="@id/contact_list_guideline_end"/>
+
+ <ImageView
+ android:id="@+id/show_contact_detail_id"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:src="@drawable/ic_arrow_right"
+ android:scaleType="center"
+ android:tint="@color/secondary_icon_color"
+ android:background="?android:attr/selectableItemBackground"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toEndOf="@id/contact_list_guideline_end"
+ app:layout_constraintEnd_toEndOf="parent"/>
+
+ <View
+ android:layout_width="@dimen/vertical_divider_width"
+ android:layout_height="match_parent"
+ android:background="@color/divider_color"
+ android:layout_marginTop="@dimen/vertical_divider_inset"
+ android:layout_marginBottom="@dimen/vertical_divider_inset"
+ app:layout_constraintStart_toStartOf="@id/contact_list_guideline_end"/>
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/res/layout/contact_result.xml b/res/layout/contact_result.xml
index ca36bb59..f6188f70 100644
--- a/res/layout/contact_result.xml
+++ b/res/layout/contact_result.xml
@@ -13,43 +13,34 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<!-- This FrameLayout is used to center the CardView since the CardView's container will be larger
- than its width. -->
-<FrameLayout
+
+<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/contact_result"
+ android:foreground="?android:attr/selectableItemBackground"
android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:focusable="true">
-
- <androidx.car.widget.ColumnCardView
- android:id="@+id/contact_result_card"
- android:foreground="@drawable/dialer_ripple_background"
- android:layout_gravity="center"
- android:layout_width="wrap_content"
- android:layout_height="@dimen/contact_result_card_height" >
-
- <!-- Using this FrameLayout to center the ImageView within a width of
- car_keyline_2. -->
- <FrameLayout
- android:layout_width="@dimen/car_keyline_2"
- android:layout_height="match_parent">
+ android:layout_height="@dimen/contact_result_height">
- <ImageView
- android:id="@+id/contact_picture"
- android:layout_gravity="center"
- android:layout_width="@dimen/call_log_icon_size"
- android:layout_height="@dimen/call_log_icon_size"
- android:scaleType="centerCrop" />
- </FrameLayout>
+ <ImageView
+ android:id="@+id/contact_picture"
+ android:layout_width="@dimen/avatar_icon_size"
+ android:layout_height="@dimen/avatar_icon_size"
+ android:layout_marginStart="@dimen/contact_result_avatar_margin"
+ android:scaleType="centerCrop"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"/>
- <TextView
- android:id="@+id/contact_name"
- android:layout_marginStart="@dimen/car_keyline_2"
- android:layout_gravity="center_vertical|start"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:ellipsize="end"
- android:maxLines="2"
- style="@style/TextAppearance.Car.Body1" />
- </androidx.car.widget.ColumnCardView>
-</FrameLayout>
+ <TextView
+ android:id="@+id/contact_name"
+ android:layout_marginStart="@dimen/contact_result_name_margin"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:textAppearance="@style/TextAppearance.ContactResultTitle"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"/>
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/res/layout/contact_search_activity.xml b/res/layout/contact_search_activity.xml
deleted file mode 100644
index b1302069..00000000
--- a/res/layout/contact_search_activity.xml
+++ /dev/null
@@ -1,105 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-<FrameLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:background="@color/phone_theme"
- android:orientation="vertical"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
-
- <FrameLayout
- android:background="@color/telecom_display_scrim"
- android:layout_width="match_parent"
- android:layout_height="match_parent" />
-
- <!-- In order for this FrameLayout's elevation to show up, there needs to be a background
- set on it that's not transparent. Therefore, set it as the same color as
- phone_theme. -->
- <FrameLayout
- android:id="@+id/search_container"
- android:background="@color/phone_theme"
- android:layout_width="match_parent"
- android:layout_height="@dimen/car_app_bar_height" >
-
- <FrameLayout
- android:background="@color/telecom_display_scrim"
- android:layout_width="match_parent"
- android:layout_height="match_parent" />
-
- <!-- This FrameLayout is used to center the ImageView in a space that is the width of
- car_keyline_1. -->
- <FrameLayout
- android:layout_width="@dimen/car_keyline_1"
- android:layout_height="match_parent">
-
- <ImageView
- android:id="@+id/back"
- android:background="@drawable/dialer_ripple_background"
- android:layout_gravity="center"
- android:layout_width="@dimen/car_touch_target_size"
- android:layout_height="@dimen/car_touch_target_size"
- android:src="@drawable/ic_arrow_back"
- android:scaleType="center"
- android:tint="@color/car_grey_50" />
- </FrameLayout>
-
- <androidx.car.widget.ColumnCardView
- android:layout_gravity="center"
- android:layout_width="match_parent"
- android:layout_height="@dimen/search_container_height"
- android:elevation="0dp"
- app:cardBackgroundColor="@color/car_card"
- app:cardCornerRadius="@dimen/search_container_radius">
-
- <ImageView
- android:layout_gravity="start|center_vertical"
- android:layout_width="@dimen/car_touch_target_size"
- android:layout_height="@dimen/car_touch_target_size"
- android:src="@drawable/ic_search"
- android:scaleType="center"
- android:tint="@color/search_container_controls_tint"/>
-
- <EditText
- android:id="@+id/search_field"
- android:layout_marginStart="@dimen/car_touch_target_size"
- android:layout_marginEnd="@dimen/car_touch_target_size"
- android:layout_gravity="center"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:inputType="textPersonName|textCapWords"
- android:hint="@string/search_hint"
- android:textColorHint="@color/search_container_text_hint"
- style="@style/TextAppearance.Car.Body1" />
-
- <ImageView
- android:id="@+id/clear"
- android:background="@drawable/dialer_ripple_background"
- android:layout_gravity="end|center_vertical"
- android:layout_width="@dimen/car_touch_target_size"
- android:layout_height="@dimen/car_touch_target_size"
- android:src="@drawable/ic_cancel"
- android:scaleType="center"
- android:tint="@color/search_container_controls_tint"/>
- </androidx.car.widget.ColumnCardView>
- </FrameLayout>
-
- <FrameLayout
- android:id="@+id/content_fragment_container"
- android:layout_marginTop="@dimen/car_app_bar_height"
- android:layout_width="match_parent"
- android:layout_height="match_parent" />
-</FrameLayout>
diff --git a/res/layout/dialer_fragment.xml b/res/layout/dialer_fragment.xml
deleted file mode 100644
index 94033109..00000000
--- a/res/layout/dialer_fragment.xml
+++ /dev/null
@@ -1,63 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2016 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<!-- There seems to be a bug in layout inflation where it can't use a resource to inflate a view
- group that sets layout_marginTop with a dimension. Work around by putting in a shell layout.
--->
-<FrameLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:paddingTop="@dimen/car_app_bar_height">
-
- <androidx.constraintlayout.widget.ConstraintLayout
- android:layout_width="match_parent"
- android:layout_height="match_parent">
-
- <FrameLayout
- android:id="@+id/dialpad_fragment_container"
- android:layout_height="wrap_content"
- android:layout_width="wrap_content"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintEnd_toStartOf="@+id/divider"
- app:layout_constraintTop_toTopOf="parent"/>
-
- <View
- android:id="@+id/divider"
- android:background="@color/car_list_divider"
- android:layout_gravity="end|top"
- android:layout_height="0dp"
- android:layout_width="@dimen/line_divider_height"
- app:layout_constraintBottom_toBottomOf="@+id/dialpad_fragment_container"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintTop_toTopOf="@+id/dialpad_fragment_container"/>
-
- <FrameLayout
- android:id="@+id/dialer_info_fragment_container"
- android:layout_height="0dp"
- android:layout_width="0dp"
- android:paddingStart="@dimen/car_keyline_1"
- android:paddingEnd="@dimen/car_keyline_1"
- app:layout_constraintBottom_toBottomOf="@+id/dialpad_fragment_container"
- app:layout_constraintStart_toEndOf="@+id/divider"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintTop_toTopOf="@+id/dialpad_fragment_container">
- <include layout="@layout/dialer_info_fragment"/>
- </FrameLayout>
- </androidx.constraintlayout.widget.ConstraintLayout>
-</FrameLayout>
diff --git a/res/layout/dialer_info_fragment.xml b/res/layout/dialer_info_fragment.xml
deleted file mode 100644
index 8a03249a..00000000
--- a/res/layout/dialer_info_fragment.xml
+++ /dev/null
@@ -1,91 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2018 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<androidx.constraintlayout.widget.ConstraintLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
-
- <TextView
- android:id="@+id/title"
- style="@style/TextAppearance.Car.Headline2"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:paddingLeft="@dimen/car_padding_4"
- android:paddingRight="@dimen/car_padding_4"
- android:paddingBottom="@dimen/dialer_number_view_padding"
- android:gravity="center"
- android:focusable="true"
- android:maxLines="1"
- android:text="@string/dial_a_number"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent"/>
-
- <TextView
- android:id="@+id/body"
- style="@style/TextAppearance.Car.Body1"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:paddingLeft="@dimen/car_padding_4"
- android:paddingRight="@dimen/car_padding_4"
- android:paddingTop="@dimen/car_padding_3"
- android:gravity="center"
- android:visibility="gone"
- android:maxLines="1"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/title"/>
-
- <ImageButton
- android:id="@+id/call_button"
- style="@style/DialpadPrimaryButton"
- android:src="@drawable/ic_phone"
- android:backgroundTint="@color/phone_call"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toStartOf="@+id/delete_button"
- app:layout_constraintHorizontal_chainStyle="packed"
- app:layout_constraintStart_toStartOf="parent"/>
-
- <ImageButton
- android:id="@+id/delete_button"
- style="@style/DialpadSecondaryButton"
- android:layout_marginStart="@dimen/car_padding_6"
- android:src="@drawable/ic_backspace"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toEndOf="@+id/call_button"/>
-
- <ImageButton
- android:id="@+id/end_call_button"
- style="@style/DialpadPrimaryButton"
- android:src="@drawable/ic_call_end"
- android:visibility="gone"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toStartOf="@+id/mute_button"
- app:layout_constraintHorizontal_chainStyle="packed"
- app:layout_constraintStart_toStartOf="parent"/>
-
- <ImageButton
- android:id="@+id/mute_button"
- style="@style/DialpadSecondaryButton"
- android:layout_marginStart="@dimen/car_padding_6"
- android:src="@drawable/ic_mute_call_normal"
- android:visibility="gone"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toEndOf="@+id/end_call_button"/>
-</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file
diff --git a/res/layout/dialer_settings_activity.xml b/res/layout/dialer_settings_activity.xml
new file mode 100644
index 00000000..c67eefb7
--- /dev/null
+++ b/res/layout/dialer_settings_activity.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <com.android.car.apps.common.ClickThroughToolbar
+ android:id="@+id/settings_toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="?android:attr/actionBarSize"
+ app:layout_constraintTop_toTopOf="parent"/>
+
+ <FrameLayout
+ android:id="@+id/settings_fragment_container"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_marginStart="@dimen/preference_list_margin"
+ android:layout_marginEnd="@dimen/preference_list_margin"
+ app:layout_constraintTop_toBottomOf="@id/settings_toolbar"
+ app:layout_constraintBottom_toBottomOf="parent">
+ </FrameLayout>
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/res/layout/dialpad.xml b/res/layout/dialpad.xml
deleted file mode 100644
index d450694c..00000000
--- a/res/layout/dialpad.xml
+++ /dev/null
@@ -1,135 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2016 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<GridLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:id="@+id/dialpad_layout"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center"
- android:columnCount="5">
-
- <!-- Row 1 -->
- <com.android.car.dialer.DialpadButton
- android:id="@+id/one"
- app:numberText="@string/one"
- app:image="@drawable/ic_voicemail"
- style="@style/DialpadKeyButtonStyle"/>
- <Space
- android:layout_width="@dimen/in_call_card_dialpad_horizontal_spacing"
- android:layout_height="0dp"/>
- <com.android.car.dialer.DialpadButton
- android:id="@+id/two"
- app:numberText="@string/two"
- app:letterText="@string/two_letters"
- style="@style/DialpadKeyButtonStyle"/>
- <Space
- android:layout_width="@dimen/in_call_card_dialpad_horizontal_spacing"
- android:layout_height="0dp"/>
- <com.android.car.dialer.DialpadButton
- android:id="@+id/three"
- app:numberText="@string/three"
- app:letterText="@string/three_letters"
- style="@style/DialpadKeyButtonStyle"/>
-
- <!-- Vertical spacing between rows. -->
- <Space
- android:layout_height="@dimen/in_call_card_dialpad_vertical_spacing"
- android:layout_width="0dp"
- android:layout_columnSpan="5" />
-
- <!-- Row 2 -->
- <com.android.car.dialer.DialpadButton
- android:id="@+id/four"
- app:numberText="@string/four"
- app:letterText="@string/four_letters"
- style="@style/DialpadKeyButtonStyle"/>
- <Space
- android:layout_width="@dimen/in_call_card_dialpad_horizontal_spacing"
- android:layout_height="0dp"/>
- <com.android.car.dialer.DialpadButton
- android:id="@+id/five"
- app:numberText="@string/five"
- app:letterText="@string/five_letters"
- style="@style/DialpadKeyButtonStyle"/>
- <Space
- android:layout_width="@dimen/in_call_card_dialpad_horizontal_spacing"
- android:layout_height="0dp"/>
- <com.android.car.dialer.DialpadButton
- android:id="@+id/six"
- app:numberText="@string/six"
- app:letterText="@string/six_letters"
- style="@style/DialpadKeyButtonStyle"/>
-
- <!-- Vertical spacing between rows. -->
- <Space
- android:layout_height="@dimen/in_call_card_dialpad_vertical_spacing"
- android:layout_width="0dp"
- android:layout_columnSpan="5" />
-
- <!-- Row 3 -->
- <com.android.car.dialer.DialpadButton
- android:id="@+id/seven"
- app:numberText="@string/seven"
- app:letterText="@string/seven_letters"
- style="@style/DialpadKeyButtonStyle"/>
- <Space
- android:layout_width="@dimen/in_call_card_dialpad_horizontal_spacing"
- android:layout_height="0dp"/>
- <com.android.car.dialer.DialpadButton
- android:id="@+id/eight"
- app:numberText="@string/eight"
- app:letterText="@string/eight_letters"
- style="@style/DialpadKeyButtonStyle"/>
- <Space
- android:layout_width="@dimen/in_call_card_dialpad_horizontal_spacing"
- android:layout_height="0dp"/>
- <com.android.car.dialer.DialpadButton
- android:id="@+id/nine"
- app:numberText="@string/nine"
- app:letterText="@string/nine_letters"
- style="@style/DialpadKeyButtonStyle"/>
-
- <!-- Vertical spacing between rows. -->
- <Space
- android:layout_height="@dimen/in_call_card_dialpad_vertical_spacing"
- android:layout_width="0dp"
- android:layout_columnSpan="5" />
-
- <!-- Row 4 -->
- <com.android.car.dialer.DialpadButton
- android:id="@+id/star"
- app:numberText="@string/star"
- app:letterText="@string/star_letters"
- style="@style/DialpadKeyButtonStyle"/>
- <Space
- android:layout_width="@dimen/in_call_card_dialpad_horizontal_spacing"
- android:layout_height="0dp"/>
- <com.android.car.dialer.DialpadButton
- android:id="@+id/zero"
- app:numberText="@string/zero"
- app:letterText="@string/zero_letters"
- style="@style/DialpadKeyButtonStyle"/>
- <Space
- android:layout_width="@dimen/in_call_card_dialpad_horizontal_spacing"
- android:layout_height="0dp"/>
- <com.android.car.dialer.DialpadButton
- android:id="@+id/pound"
- app:numberText="@string/pound"
- app:letterText="@string/pound_letters"
- style="@style/DialpadKeyButtonStyle"/>
-
-</GridLayout>
diff --git a/res/layout/dialpad_fragment.xml b/res/layout/dialpad_fragment.xml
new file mode 100644
index 00000000..6627e704
--- /dev/null
+++ b/res/layout/dialpad_fragment.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <fragment
+ android:id="@+id/dialpad_fragment"
+ android:name="com.android.car.dialer.ui.dialpad.KeypadFragment"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/divider"/>
+
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/divider"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:layout_constraintGuide_percent="0.5"/>
+
+ <FrameLayout
+ android:layout_height="0dp"
+ android:layout_width="0dp"
+ app:layout_constraintDimensionRatio="W, 1:1"
+ app:layout_constraintTop_toTopOf="@id/dialpad_fragment"
+ app:layout_constraintBottom_toBottomOf="@id/dialpad_fragment"
+ app:layout_constraintStart_toEndOf="@id/divider"
+ app:layout_constraintEnd_toEndOf="parent">
+ <include layout="@layout/dialpad_info"/>
+ </FrameLayout>
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/res/layout/dialpad_info.xml b/res/layout/dialpad_info.xml
new file mode 100644
index 00000000..93eaf778
--- /dev/null
+++ b/res/layout/dialpad_info.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <androidx.constraintlayout.widget.Guideline
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/dialpad_info_guideline"
+ android:orientation="horizontal"
+ app:layout_constraintGuide_begin="@dimen/dialpad_info_guideline"/>
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:textAppearance="@style/TextAppearance.DialNumber"
+ android:gravity="center"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="@id/dialpad_info_guideline"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/delete_button"/>
+
+ <ImageButton
+ android:id="@+id/delete_button"
+ style="@style/DialpadSecondaryButton"
+ android:src="@drawable/ic_backspace"
+ android:layout_marginStart="@dimen/dialpad_info_title_padding_size"
+ app:layout_constraintTop_toTopOf="@id/title"
+ app:layout_constraintBottom_toBottomOf="@id/title"
+ app:layout_constraintStart_toEndOf="@id/title"
+ app:layout_constraintEnd_toEndOf="parent"/>
+
+ <TextView
+ android:id="@+id/display_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="@style/TextAppearance.DialpadDisplayName"
+ android:singleLine="true"
+ android:layout_marginTop="@dimen/display_name_padding"
+ app:layout_constraintTop_toBottomOf="@id/title"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"/>
+
+ <ImageView
+ android:id="@+id/call_button"
+ style="@style/DialpadPrimaryButton"
+ android:src="@drawable/icon_call_button"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"/>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/res/layout/favorite_contact_list_item.xml b/res/layout/favorite_contact_list_item.xml
new file mode 100644
index 00000000..82002fe1
--- /dev/null
+++ b/res/layout/favorite_contact_list_item.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/text_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:gravity="center_horizontal"
+ android:background="?android:attr/selectableItemBackground">
+
+ <ImageView
+ android:id="@+id/icon"
+ android:layout_width="@dimen/large_avatar_icon_size"
+ android:layout_height="@dimen/large_avatar_icon_size"
+ android:layout_marginBottom="@dimen/favorites_avatar_margin_bottom"
+ android:scaleType="centerCrop"/>
+
+ <TextView
+ android:id="@+id/title"
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"/>
+
+ <TextView
+ android:id="@+id/text"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"/>
+</LinearLayout>
diff --git a/res/layout/favorite_fragment.xml b/res/layout/favorite_fragment.xml
new file mode 100644
index 00000000..f21bc489
--- /dev/null
+++ b/res/layout/favorite_fragment.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <fragment
+ android:id="@+id/favorite_list_fragment"
+ android:name="com.android.car.dialer.ui.favorite.FavoriteListFragment"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent"/>
+
+ <LinearLayout
+ android:id="@+id/empty_page_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center"
+ android:orientation="vertical">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ android:text="@string/favorites_empty"
+ android:layout_marginBottom="@dimen/favorite_add_button_and_text_separation"/>
+
+ <TextView
+ android:id="@+id/add_favorite_button"
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/touch_target_size"
+ android:text="@string/add_favorite_button"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:background="@drawable/hero_button_background"
+ android:paddingStart="@dimen/favorite_add_button_padding"
+ android:paddingEnd="@dimen/favorite_add_button_padding"
+ android:gravity="center"/>
+
+ </LinearLayout>
+
+</FrameLayout> \ No newline at end of file
diff --git a/res/layout/contact_result_fragment.xml b/res/layout/in_call_activity.xml
index 3dcb87d1..882b083a 100644
--- a/res/layout/contact_result_fragment.xml
+++ b/res/layout/in_call_activity.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2017 The Android Open Source Project
+<!-- Copyright (C) 2019 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -15,16 +15,17 @@
-->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
+ <fragment
+ android:name="com.android.car.dialer.ui.activecall.OngoingCallFragment"
+ android:id="@+id/ongoing_call_fragment"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
- <androidx.car.widget.PagedListView
- android:id="@+id/contact_result_list"
+ <fragment
+ android:name="com.android.car.dialer.ui.activecall.IncomingCallFragment"
+ android:id="@+id/incoming_call_fragment"
android:layout_width="match_parent"
- android:layout_height="match_parent"
- app:showPagedListViewDivider="true"
- app:dividerStartMargin="@dimen/car_keyline_2"
- app:alignDividerEndTo="@id/contact_result_card"
- app:alignDividerStartTo="@id/contact_result_card" />
+ android:layout_height="match_parent"/>
</FrameLayout>
diff --git a/res/layout/in_call_fragment.xml b/res/layout/in_call_fragment.xml
deleted file mode 100644
index 0b26936a..00000000
--- a/res/layout/in_call_fragment.xml
+++ /dev/null
@@ -1,50 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2018 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<LinearLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="@color/phone_theme_secondary"
- android:orientation="vertical">
-
- <FrameLayout
- android:id="@+id/dialer_container"
- android:layout_width="match_parent"
- android:layout_height="0dp"
- android:visibility="gone"
- android:layout_weight="1"/>
-
- <FrameLayout
- android:id="@+id/user_profile_container"
- android:layout_width="match_parent"
- android:gravity="center"
- android:layout_height="0dp"
- android:layout_weight="1">
- <include layout="@layout/user_profile_large"/>
- </FrameLayout>
-
- <androidx.cardview.widget.CardView
- android:id="@+id/controller_bar_container"
- android:layout_width="match_parent"
- android:layout_height="@dimen/car_action_bar_height"
- android:background="@android:color/white"
- android:layout_marginStart="@dimen/car_margin"
- android:layout_marginEnd="@dimen/car_margin"
- android:layout_marginBottom="@dimen/car_padding_1"
- app:cardCornerRadius="@dimen/car_radius_3"
- app:cardElevation="@dimen/car_action_bar_elevation"/>
-</LinearLayout> \ No newline at end of file
diff --git a/res/layout/incall_dialpad_fragment.xml b/res/layout/incall_dialpad_fragment.xml
new file mode 100644
index 00000000..1d1dc4c8
--- /dev/null
+++ b/res/layout/incall_dialpad_fragment.xml
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <fragment
+ android:id="@+id/dialpad_fragment"
+ android:name="com.android.car.dialer.ui.dialpad.KeypadFragment"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/divider"/>
+
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/divider"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:layout_constraintGuide_percent="0.5"/>
+
+ <View
+ android:layout_height="0dp"
+ android:layout_width="0dp"
+ android:id="@+id/dialpad_info_boundaries"
+ app:layout_constraintDimensionRatio="W, 1:1"
+ app:layout_constraintTop_toTopOf="@id/dialpad_fragment"
+ app:layout_constraintBottom_toBottomOf="@id/dialpad_fragment"
+ app:layout_constraintStart_toEndOf="@id/divider"
+ app:layout_constraintEnd_toEndOf="parent"/>
+
+ <Chronometer
+ android:id="@+id/call_state"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:textAppearance="@style/TextAppearance.InCallState"
+ android:focusable="true"
+ android:singleLine="true"
+ android:gravity="center"
+ android:layout_marginBottom="@dimen/call_state_padding"
+ app:layout_constraintVertical_chainStyle="packed"
+ app:layout_constraintTop_toTopOf="@id/dialpad_info_boundaries"
+ app:layout_constraintBottom_toTopOf="@+id/title"
+ app:layout_constraintStart_toStartOf="@id/dialpad_info_boundaries"
+ app:layout_constraintEnd_toEndOf="@id/dialpad_info_boundaries"/>
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:focusable="true"
+ android:singleLine="true"
+ android:textAppearance="@style/TextAppearance.DialNumber"
+ android:gravity="end"
+ app:layout_constrainedWidth="true"
+ app:layout_constraintTop_toBottomOf="@id/call_state"
+ app:layout_constraintBottom_toBottomOf="@id/dialpad_info_boundaries"
+ app:layout_constraintStart_toStartOf="@id/dialpad_info_boundaries"
+ app:layout_constraintEnd_toEndOf="@id/dialpad_info_boundaries"/>
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/res/layout/incoming_call_fragment.xml b/res/layout/incoming_call_fragment.xml
new file mode 100644
index 00000000..7c72ff7f
--- /dev/null
+++ b/res/layout/incoming_call_fragment.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <com.android.car.apps.common.BackgroundImageView
+ android:id="@+id/background_image"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+
+ <include
+ layout="@layout/user_profile_large"
+ android:id="@+id/user_profile_container"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toTopOf="@+id/ringing_call_controller_bar"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"/>
+
+ <fragment
+ android:id="@+id/ringing_call_controller_bar"
+ android:name="com.android.car.dialer.ui.activecall.RingingCallControllerBarFragment"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/in_call_controller_bar_height"
+ android:layout_marginStart="@dimen/in_call_controller_bar_margin"
+ android:layout_marginEnd="@dimen/in_call_controller_bar_margin"
+ android:layout_marginBottom="@dimen/in_call_controller_bar_margin_bottom"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"/>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/res/layout/keypad.xml b/res/layout/keypad.xml
new file mode 100644
index 00000000..42e004e4
--- /dev/null
+++ b/res/layout/keypad.xml
@@ -0,0 +1,201 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <!-- Row 1 -->
+ <!-- Put back when voicemail is supported: app:image="@drawable/ic_voicemail" -->
+ <com.android.car.dialer.ui.dialpad.KeypadButton
+ android:id="@+id/one"
+ app:numberText="@string/one"
+ app:letterText="@string/one_letters"
+ style="@style/KeypadButtonStyle"
+ app:layout_constraintHorizontal_chainStyle="packed"
+ app:layout_constraintVertical_chainStyle="packed"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toTopOf="@+id/four"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/two" />
+
+ <com.android.car.dialer.ui.dialpad.KeypadButton
+ android:id="@+id/two"
+ app:numberText="@string/two"
+ app:letterText="@string/two_letters"
+ style="@style/KeypadButtonStyle"
+ app:layout_constraintVertical_chainStyle="packed"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toTopOf="@+id/five"
+ app:layout_constraintStart_toEndOf="@id/one"
+ app:layout_constraintEnd_toStartOf="@+id/three"/>
+
+ <com.android.car.dialer.ui.dialpad.KeypadButton
+ android:id="@+id/three"
+ app:numberText="@string/three"
+ app:letterText="@string/three_letters"
+ style="@style/KeypadButtonStyle"
+ app:layout_constraintVertical_chainStyle="packed"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toTopOf="@+id/six"
+ app:layout_constraintStart_toEndOf="@id/two"
+ app:layout_constraintEnd_toEndOf="parent"/>
+
+ <!-- Row 2 -->
+ <com.android.car.dialer.ui.dialpad.KeypadButton
+ android:id="@+id/four"
+ app:numberText="@string/four"
+ app:letterText="@string/four_letters"
+ style="@style/KeypadButtonStyle"
+ app:layout_constraintHorizontal_chainStyle="packed"
+ app:layout_constraintTop_toBottomOf="@id/one"
+ app:layout_constraintBottom_toTopOf="@+id/seven"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/five"/>
+
+ <com.android.car.dialer.ui.dialpad.KeypadButton
+ android:id="@+id/five"
+ app:numberText="@string/five"
+ app:letterText="@string/five_letters"
+ style="@style/KeypadButtonStyle"
+ app:layout_constraintTop_toBottomOf="@id/two"
+ app:layout_constraintBottom_toTopOf="@+id/eight"
+ app:layout_constraintStart_toEndOf="@id/four"
+ app:layout_constraintEnd_toStartOf="@+id/six" />
+
+ <com.android.car.dialer.ui.dialpad.KeypadButton
+ android:id="@+id/six"
+ app:numberText="@string/six"
+ app:letterText="@string/six_letters"
+ style="@style/KeypadButtonStyle"
+ app:layout_constraintTop_toBottomOf="@id/three"
+ app:layout_constraintBottom_toTopOf="@+id/nine"
+ app:layout_constraintStart_toEndOf="@id/five"
+ app:layout_constraintEnd_toEndOf="parent"/>
+
+
+ <!-- Row 3 -->
+ <com.android.car.dialer.ui.dialpad.KeypadButton
+ android:id="@+id/seven"
+ app:numberText="@string/seven"
+ app:letterText="@string/seven_letters"
+ style="@style/KeypadButtonStyle"
+ app:layout_constraintHorizontal_chainStyle="packed"
+ app:layout_constraintTop_toBottomOf="@id/four"
+ app:layout_constraintBottom_toTopOf="@+id/star"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/eight"/>
+
+ <com.android.car.dialer.ui.dialpad.KeypadButton
+ android:id="@+id/eight"
+ app:numberText="@string/eight"
+ app:letterText="@string/eight_letters"
+ style="@style/KeypadButtonStyle"
+ app:layout_constraintTop_toBottomOf="@id/five"
+ app:layout_constraintBottom_toTopOf="@+id/zero"
+ app:layout_constraintStart_toEndOf="@id/seven"
+ app:layout_constraintEnd_toStartOf="@+id/nine"/>
+
+ <com.android.car.dialer.ui.dialpad.KeypadButton
+ android:id="@+id/nine"
+ app:numberText="@string/nine"
+ app:letterText="@string/nine_letters"
+ style="@style/KeypadButtonStyle"
+ app:layout_constraintTop_toBottomOf="@id/six"
+ app:layout_constraintBottom_toTopOf="@+id/pound"
+ app:layout_constraintStart_toEndOf="@id/eight"
+ app:layout_constraintEnd_toEndOf="parent"/>
+
+ <!-- Row 4 -->
+ <com.android.car.dialer.ui.dialpad.KeypadButton
+ android:id="@+id/star"
+ app:numberText="@string/star"
+ app:letterText="@string/star_letters"
+ style="@style/KeypadButtonStyle"
+ app:layout_constraintHorizontal_chainStyle="packed"
+ app:layout_constraintTop_toBottomOf="@id/seven"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/zero"/>
+
+ <com.android.car.dialer.ui.dialpad.KeypadButton
+ android:id="@+id/zero"
+ app:numberText="@string/zero"
+ app:letterText="@string/zero_letters"
+ style="@style/KeypadButtonStyle"
+ app:layout_constraintTop_toBottomOf="@id/eight"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toEndOf="@id/star"
+ app:layout_constraintEnd_toStartOf="@+id/pound"/>
+
+ <com.android.car.dialer.ui.dialpad.KeypadButton
+ android:id="@+id/pound"
+ app:numberText="@string/pound"
+ app:letterText="@string/pound_letters"
+ style="@style/KeypadButtonStyle"
+ app:layout_constraintTop_toBottomOf="@id/nine"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toEndOf="@id/zero"
+ app:layout_constraintEnd_toEndOf="parent"/>
+
+ <!-- Add horizontal dividers -->
+ <View
+ android:background="@color/divider_color"
+ android:layout_height="@dimen/dialpad_line_divider_height"
+ android:layout_width="0dp"
+ app:layout_constraintTop_toBottomOf="@id/one"
+ app:layout_constraintBottom_toTopOf="@id/four"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"/>
+
+ <View
+ android:background="@color/divider_color"
+ android:layout_height="@dimen/dialpad_line_divider_height"
+ android:layout_width="0dp"
+ app:layout_constraintTop_toBottomOf="@id/four"
+ app:layout_constraintBottom_toTopOf="@id/seven"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"/>
+ <View
+ android:background="@color/divider_color"
+ android:layout_height="@dimen/dialpad_line_divider_height"
+ android:layout_width="0dp"
+ app:layout_constraintTop_toBottomOf="@id/seven"
+ app:layout_constraintBottom_toTopOf="@id/star"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"/>
+
+ <!-- Add vertical dividers-->
+ <View
+ android:background="@color/divider_color"
+ android:layout_height="0dp"
+ android:layout_width="@dimen/dialpad_line_divider_height"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toEndOf="@id/one"
+ app:layout_constraintEnd_toStartOf="@id/two"/>
+
+ <View
+ android:background="@color/divider_color"
+ android:layout_height="0dp"
+ android:layout_width="@dimen/dialpad_line_divider_height"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toEndOf="@id/two"
+ app:layout_constraintEnd_toStartOf="@id/three"/>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/res/layout/dialpad_button.xml b/res/layout/keypad_button.xml
index 993953a7..ac1c793a 100644
--- a/res/layout/dialpad_button.xml
+++ b/res/layout/keypad_button.xml
@@ -17,29 +17,28 @@ limitations under the License.
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_gravity="center"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content">
+ android:layout_width="@dimen/keypad_minimum_size"
+ android:layout_height="@dimen/keypad_minimum_size">
<TextView
- android:id="@+id/dialpad_number"
+ android:id="@+id/keypad_number"
android:layout_gravity="center_horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginBottom="-8dp"
android:visibility="gone"
- style="@style/TextAppearance.Car.Key1" />
+ style="@style/KeypadNumber" />
<TextView
- android:id="@+id/dialpad_letters"
+ android:id="@+id/keypad_letters"
android:layout_gravity="center_horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAllCaps="true"
android:visibility="gone"
- style="@style/TextAppearance.Car.Key2" />
+ style="@style/KeypadLetter" />
<ImageView
- android:id="@+id/dialpad_image"
+ android:id="@+id/keypad_image"
android:layout_gravity="center_horizontal"
android:layout_width="32dp"
android:layout_height="32dp"
diff --git a/res/layout/list_fragment.xml b/res/layout/list_fragment.xml
new file mode 100644
index 00000000..10b5403d
--- /dev/null
+++ b/res/layout/list_fragment.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<com.android.car.apps.common.widget.PagedRecyclerView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/list_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clipToPadding="false"/>
diff --git a/res/layout/menu_action_view.xml b/res/layout/menu_action_view.xml
new file mode 100644
index 00000000..c1955211
--- /dev/null
+++ b/res/layout/menu_action_view.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center">
+ <ImageView
+ android:id="@+id/menu_icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="@dimen/menu_item_margin_x"
+ android:background="?android:attr/actionBarItemBackground"
+ style="?android:attr/actionButtonStyle"/>
+</LinearLayout>
diff --git a/res/layout/no_hfp.xml b/res/layout/no_hfp.xml
index 2a7909ac..115c9bba 100644
--- a/res/layout/no_hfp.xml
+++ b/res/layout/no_hfp.xml
@@ -13,25 +13,62 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<RelativeLayout
+<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/no_hfp_error_container"
android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="@color/telecom_display_scrim">
+ android:layout_height="match_parent">
- <ImageView
- android:id="@+id/error_icon"
+ <Button
+ android:id="@+id/emergency_call_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_centerInParent="true"
- android:src="@drawable/car_ic_bluetooth_disable_large" />
+ android:text="@string/emergency_button_text"
+ android:minWidth="@dimen/emergency_button_min_width"
+ android:minHeight="@dimen/emergency_button_min_height"
+ android:background="?android:attr/selectableItemBackground"
+ android:textColor="@color/emergency_text_color"
+ android:layout_marginBottom="@dimen/emergency_button_bottom_margin"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"/>
+
+ <ImageView
+ android:id="@+id/error_icon"
+ android:layout_width="@dimen/no_hfp_icon_size"
+ android:layout_height="@dimen/no_hfp_icon_size"
+ android:tint="@color/primary_icon_color"
+ android:src="@drawable/ic_bluetooth_disable"
+ app:layout_constraintVertical_chainStyle="packed"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toTopOf="@id/error_string"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"/>
<TextView
- android:id="@+id/error_string"
- android:layout_width="@dimen/apps_max_content_width"
+ android:id="@id/error_string"
+ android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_below="@id/error_icon"
- android:layout_centerHorizontal="true"
android:text="@string/no_hfp"
- style="@style/NoHfpText"/>
-</RelativeLayout>
+ style="@style/NoHfpText"
+ app:layout_constraintTop_toBottomOf="@id/error_icon"
+ app:layout_constraintBottom_toTopOf="@+id/connect_bluetooth_button"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"/>
+
+ <com.android.car.apps.common.UxrButton
+ android:id="@+id/connect_bluetooth_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/connect_bluetooth_button_text"
+ android:minWidth="@dimen/connect_bluetooth_button_min_width"
+ android:minHeight="@dimen/connect_bluetooth_button_min_height"
+ android:background="?android:attr/selectableItemBackground"
+ android:textColor="@color/connect_bluetooth_text_color"
+ app:carUxRestrictions="UX_RESTRICTIONS_FULLY_RESTRICTED"
+ app:layout_constraintTop_toBottomOf="@+id/error_string"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"/>
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/res/layout/on_going_call_controller_bar_fragment.xml b/res/layout/on_going_call_controller_bar_fragment.xml
index 4afe545c..787e489a 100644
--- a/res/layout/on_going_call_controller_bar_fragment.xml
+++ b/res/layout/on_going_call_controller_bar_fragment.xml
@@ -16,85 +16,63 @@ limitations under the License.
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
- xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
- android:layout_height="@dimen/car_action_bar_height"
- android:background="@color/phone_theme"
- android:elevation = "@dimen/in_call_card_elevation">
+ android:layout_height="@dimen/in_call_controller_bar_height">
<ImageView
android:id="@+id/mute_button"
android:layout_width="@dimen/in_call_button_size"
android:layout_height="@dimen/in_call_button_size"
android:scaleType="center"
- android:src="@drawable/ic_mute"
- android:tint="@color/primary_icon_color"
+ android:src="@drawable/ic_mute_activatable"
+ android:background="@drawable/dialer_ripple_background"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/toggle_dialpad_button"
app:layout_constraintTop_toTopOf="parent"
- tools:src="@color/contact_badge"/>
+ app:layout_constraintHorizontal_chainStyle="spread_inside"/>
<ImageView
android:id="@+id/toggle_dialpad_button"
android:layout_width="@dimen/in_call_button_size"
android:layout_height="@dimen/in_call_button_size"
android:scaleType="center"
- android:src="@drawable/ic_dialpad"
- android:tint="@color/primary_icon_color"
+ android:src="@drawable/ic_dialpad_activatable"
+ android:background="@drawable/dialer_ripple_background"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/mute_button"
app:layout_constraintEnd_toStartOf="@+id/end_call_button"
- app:layout_constraintTop_toTopOf="parent"
- tools:src="@color/contact_badge"/>
+ app:layout_constraintTop_toTopOf="parent"/>
<ImageView
android:id="@+id/end_call_button"
- android:layout_width="@dimen/fab_button_size"
- android:layout_height="@dimen/fab_button_size"
- android:scaleType="center"
- android:src="@drawable/ic_call_end"
- android:tint="@color/phone_theme_light"
+ style="@style/DialpadPrimaryButton"
+ android:src="@drawable/ic_call_end_button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/toggle_dialpad_button"
app:layout_constraintEnd_toStartOf="@+id/voice_channel_button"
- app:layout_constraintTop_toTopOf="parent"
- tools:src="@color/contact_badge"/>
+ app:layout_constraintTop_toTopOf="parent"/>
<ImageView
android:id="@+id/voice_channel_button"
android:layout_width="@dimen/in_call_button_size"
android:layout_height="@dimen/in_call_button_size"
+ android:background="@drawable/dialer_ripple_background"
android:scaleType="center"
- android:src="@drawable/ic_bluetooth"
- android:tint="@color/primary_icon_color"
+ android:src="@drawable/ic_bluetooth_activatable"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/end_call_button"
app:layout_constraintEnd_toStartOf="@+id/pause_button"
- app:layout_constraintTop_toTopOf="parent"
- tools:src="@color/contact_badge"/>
-
- <ImageView
- android:id="@+id/voice_channel_chevron"
- android:layout_width="@dimen/car_secondary_icon_size"
- android:layout_height="@dimen/car_secondary_icon_size"
- android:scaleType="fitCenter"
- android:src="@drawable/ic_arrow_down"
- android:tint="@color/primary_icon_color"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintStart_toEndOf="@+id/voice_channel_button"
- app:layout_constraintTop_toTopOf="parent"
- tools:src="@color/contact_badge"/>
+ app:layout_constraintTop_toTopOf="parent"/>
<ImageView
android:id="@+id/pause_button"
android:layout_width="@dimen/in_call_button_size"
android:layout_height="@dimen/in_call_button_size"
+ android:background="@drawable/dialer_ripple_background"
android:scaleType="center"
- android:src="@drawable/ic_call_state_switch"
- android:tint="@color/primary_icon_color"
+ android:src="@drawable/ic_pause_activatable"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/voice_channel_button"
app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintTop_toTopOf="parent"
- tools:src="@color/contact_badge"/>
+ app:layout_constraintTop_toTopOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/res/layout/ongoing_call.xml b/res/layout/ongoing_call.xml
deleted file mode 100644
index 8527e65d..00000000
--- a/res/layout/ongoing_call.xml
+++ /dev/null
@@ -1,240 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2015 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<FrameLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:layout_width="match_parent"
- android:layout_height="match_parent" >
-
- <ImageView
- android:id="@+id/large_contact_photo"
- android:layout_width="@dimen/in_call_large_contact_photo_size"
- android:layout_height="match_parent"
- android:layout_gravity="end"
- android:alpha=".20"
- android:scaleType="centerCrop" />
-
- <View
- android:layout_width="@dimen/in_call_large_contact_photo_size"
- android:layout_height="wrap_content"
- android:layout_gravity="end"
- android:background="@drawable/contact_photo_gradient" />
-
- <FrameLayout
- android:background="@color/telecom_display_scrim"
- android:layout_width="match_parent"
- android:layout_height="match_parent" />
-
- <RelativeLayout
- android:id="@+id/secondary_call_container"
- android:layout_width="@dimen/in_call_large_contact_photo_size"
- android:layout_height="match_parent"
- android:layout_gravity="right"
- android:paddingTop="128dp"
- android:paddingStart="48dp"
- android:background="@color/secondary_call_scrim"
- android:visibility="gone" >
- <TextView
- android:id="@+id/name_secondary"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:maxWidth="@dimen/in_call_text_max_width"
- android:textAppearance="@style/TextAppearance.Car.Headline2"
- android:textColor="@color/car_headline1_light" />
- <TextView
- android:id="@+id/info_secondary"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginTop="4dp"
- android:layout_below="@id/name_secondary"
- android:maxWidth="@dimen/in_call_text_max_width"
- android:alpha="0.6"
- style="@style/TextAppearance.Car.Body2"
- android:textColor="@color/car_grey_100" />
- </RelativeLayout>
-
- <!-- In call dialpad -->
- <androidx.cardview.widget.CardView
- android:id="@+id/dialpad_container"
- android:layout_width="@dimen/in_call_card_dialpad_width"
- android:layout_height="match_parent"
- android:layout_gravity="top|start"
- android:layout_marginTop="@dimen/car_app_bar_height"
- app:cardBackgroundColor="@color/car_card"
- app:cardCornerRadius="@dimen/in_call_card_dialpad_corner_radius"
- app:cardElevation="@dimen/in_call_card_elevation"
- android:visibility="gone" >
- <include layout="@layout/dialpad" />
- </androidx.cardview.widget.CardView>
-
- <!-- In call card -->
- <FrameLayout
- android:id="@+id/card"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="@drawable/in_call_card_background"
- android:layout_marginTop="@dimen/car_app_bar_height"
- android:layout_marginEnd="@dimen/in_call_card_margin_right"
- android:paddingStart="@dimen/in_call_content_card_margin_start" >
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="horizontal">
- <RelativeLayout
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:layout_marginTop="@dimen/in_call_info_margin_top" >
- <TextView
- android:id="@+id/name"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:ellipsize="end"
- android:maxLines="2"
- android:visibility="gone"
- style="@style/TextAppearance.Car.Headline2" />
- <TextView
- android:id="@+id/info"
- android:layout_below="@id/name"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:visibility="gone"
- android:textColor="@color/car_caption"
- style="@style/TextAppearance.Car.Body1" />
- </RelativeLayout>
- <ImageView
- android:id="@+id/small_contact_photo"
- android:background="@drawable/avatar_rounded_bg"
- android:layout_width="@dimen/in_call_small_contact_photo_size"
- android:layout_height="@dimen/in_call_small_contact_photo_size"
- android:layout_gravity="end|top"
- android:layout_marginTop="@dimen/in_call_info_margin_top"
- android:layout_marginStart="@dimen/small_contact_photo_margin_start"
- android:layout_marginEnd="@dimen/small_contact_photo_margin_end"
- android:scaleType="centerCrop" />
- </LinearLayout>
- <View
- android:id="@+id/content_separator"
- android:layout_width="match_parent"
- android:layout_height="1dp"
- android:layout_gravity="bottom"
- android:layout_marginBottom="@dimen/car_action_bar_height"
- android:background="@color/separator" />
-
- <!-- TODO: Consider flattening out this part and updating logic. -->
- <FrameLayout
- android:id="@+id/controls_container"
- android:layout_width="match_parent"
- android:layout_height="@dimen/car_action_bar_height"
- android:layout_gravity="bottom" >
-
- <LinearLayout
- android:id="@+id/ringing_call_controls"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="horizontal"
- android:visibility="gone" >
- <ImageButton
- android:id="@+id/answer_call_button"
- android:layout_width="@dimen/dialer_fab_size"
- android:layout_height="@dimen/dialer_fab_size"
- android:layout_gravity="center_vertical"
- android:scaleType="center"
- android:elevation="8dp"
- android:src="@drawable/ic_phone" />
- <ImageButton
- android:id="@+id/reject_call_button"
- android:layout_width="@dimen/dialer_fab_size"
- android:layout_height="@dimen/dialer_fab_size"
- android:layout_marginStart="@dimen/in_call_button_spacing"
- android:layout_gravity="center_vertical"
- android:scaleType="center"
- android:src="@drawable/ic_call_end"
- android:tint="#ffff1744"
- android:background="@drawable/ongoing_call_action_background" />
- </LinearLayout>
-
- <LinearLayout
- android:id="@+id/active_call_controls"
- android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:gravity="center_vertical"
- android:orientation="horizontal"
- android:visibility="gone" >
- <ImageButton
- android:id="@+id/end_call"
- android:layout_width="@dimen/dialer_fab_size"
- android:layout_height="@dimen/dialer_fab_size"
- android:layout_gravity="center_vertical"
- android:scaleType="center"
- android:elevation="8dp"
- android:src="@drawable/ic_call_end" />
- <ImageButton
- android:id="@+id/unhold_call"
- android:layout_width="@dimen/dialer_fab_size"
- android:layout_height="@dimen/dialer_fab_size"
- android:layout_gravity="center_vertical"
- android:scaleType="center"
- android:elevation="8dp"
- android:src="@drawable/ic_pause"
- android:visibility="gone" />
- <ImageButton
- android:id="@+id/mute"
- android:layout_width="@dimen/dialer_fab_size"
- android:layout_height="@dimen/dialer_fab_size"
- android:layout_marginStart="@dimen/in_call_button_spacing"
- android:scaleType="center"
- android:src="@drawable/ic_mute"
- android:tint="@color/car_tint"
- android:background="@drawable/ongoing_call_action_background" />
- <ImageButton
- android:id="@+id/toggle_dialpad"
- android:layout_width="@dimen/dialer_fab_size"
- android:layout_height="@dimen/dialer_fab_size"
- android:layout_marginStart="@dimen/in_call_button_spacing"
- android:scaleType="center"
- android:src="@drawable/ic_dialpad"
- android:tint="@color/car_tint"
- android:background="@drawable/ongoing_call_action_background" />
- </LinearLayout>
- </FrameLayout>
- </FrameLayout>
-
- <LinearLayout
- android:id="@+id/secondary_call_controls"
- android:layout_width="@dimen/in_call_large_contact_photo_size"
- android:layout_height="wrap_content"
- android:layout_gravity="end|bottom"
- android:layout_marginBottom="@dimen/card_margin"
- android:orientation="horizontal"
- android:gravity="center"
- android:visibility="gone" >
- <ImageButton
- android:id="@+id/swap"
- android:layout_width="@dimen/dialer_fab_size"
- android:layout_height="@dimen/dialer_fab_size"
- android:scaleType="center"
- android:src="@drawable/ic_swap_calls"
- android:background="@drawable/ongoing_call_secondary_action_background" />
- <ImageButton
- android:id="@+id/merge"
- android:layout_width="@dimen/dialer_fab_size"
- android:layout_height="@dimen/dialer_fab_size"
- android:scaleType="center"
- android:src="@drawable/ic_call_merge"
- android:background="@drawable/ongoing_call_secondary_action_background" />
- </LinearLayout>
-</FrameLayout>
diff --git a/res/layout/ongoing_call_fragment.xml b/res/layout/ongoing_call_fragment.xml
new file mode 100644
index 00000000..007531b1
--- /dev/null
+++ b/res/layout/ongoing_call_fragment.xml
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <com.android.car.apps.common.BackgroundImageView
+ android:id="@+id/background_image"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+
+ <!-- This ConstraintLayout is to make a full-screen transparent background -->
+ <!-- so that the ripple effects in the controller bar buttons work. -->
+ <!-- If you put the transparent background on the root element of -->
+ <!-- in_call_fragment, the BackgroundImageView will cover up the ripples. -->
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ android:background="@android:color/transparent">
+
+ <fragment
+ android:name="com.android.car.dialer.ui.dialpad.InCallDialpadFragment"
+ android:id="@+id/incall_dialpad_fragment"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toTopOf="@+id/ongoing_call_control_bar"/>
+
+ <include
+ layout="@layout/user_profile_large"
+ android:id="@+id/user_profile_container"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toTopOf="@+id/ongoing_call_control_bar"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"/>
+
+ <fragment
+ android:id="@+id/onhold_user_profile"
+ android:name="com.android.car.dialer.ui.activecall.OnHoldCallUserProfileFragment"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/onhold_user_info_height"
+ android:layout_marginTop="@dimen/onhold_profile_margin_y"
+ android:layout_marginStart="@dimen/onhold_profile_margin_x"
+ android:layout_marginEnd="@dimen/onhold_profile_margin_x"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"/>
+
+ <fragment
+ android:id="@+id/ongoing_call_control_bar"
+ android:name="com.android.car.dialer.ui.activecall.OnGoingCallControllerBarFragment"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/in_call_controller_bar_height"
+ android:layout_marginStart="@dimen/in_call_controller_bar_margin"
+ android:layout_marginEnd="@dimen/in_call_controller_bar_margin"
+ android:layout_marginBottom="@dimen/in_call_controller_bar_margin_bottom"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"/>
+ </androidx.constraintlayout.widget.ConstraintLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/res/layout/onhold_user_profile.xml b/res/layout/onhold_user_profile.xml
new file mode 100644
index 00000000..806226bb
--- /dev/null
+++ b/res/layout/onhold_user_profile.xml
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<androidx.cardview.widget.CardView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ app:cardBackgroundColor="@color/onhold_call_background"
+ app:cardCornerRadius="@dimen/onhold_profile_corner_radius">
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <androidx.constraintlayout.widget.Guideline
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/guideline"
+ android:orientation="vertical"
+ app:layout_constraintGuide_begin="@dimen/onhold_profile_guideline"/>
+
+ <ImageView
+ android:id="@+id/icon"
+ android:layout_width="@dimen/avatar_icon_size"
+ android:layout_height="@dimen/avatar_icon_size"
+ android:scaleType="centerCrop"
+ android:layout_marginStart="@dimen/onhold_profile_avatar_margin"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"/>
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ android:singleLine="true"
+ app:layout_constraintVertical_chainStyle="packed"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toTopOf="@+id/text"
+ app:layout_constraintStart_toStartOf="@id/guideline"
+ app:layout_constraintEnd_toStartOf="@+id/swap_calls_button"/>
+
+ <TextView
+ android:id="@id/text"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:text="@string/onhold_call_label"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:singleLine="true"
+ app:layout_constraintTop_toBottomOf="@id/title"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="@id/guideline"
+ app:layout_constraintEnd_toStartOf="@+id/swap_calls_button"/>
+
+ <ImageView
+ android:id="@+id/swap_calls_button"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:src="@drawable/ic_swap_calls"
+ android:scaleType="center"
+ android:tint="@color/secondary_icon_color"
+ android:background="?android:attr/selectableItemBackground"
+ android:paddingLeft="@dimen/swap_call_button_margin"
+ android:paddingRight="@dimen/swap_call_button_margin"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"/>
+ </androidx.constraintlayout.widget.ConstraintLayout>
+</androidx.cardview.widget.CardView>
diff --git a/res/layout/phone_number_list_item.xml b/res/layout/phone_number_list_item.xml
new file mode 100644
index 00000000..04e1b61b
--- /dev/null
+++ b/res/layout/phone_number_list_item.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<com.android.car.dialer.widget.CheckableRelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingStart="?android:listPreferredItemPaddingStart"
+ android:paddingEnd="?android:listPreferredItemPaddingEnd">
+
+ <RadioButton
+ android:id="@+id/phone_number_checkbox"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ android:layout_marginEnd="@dimen/phone_number_radio_list_padding"/>
+ <TextView
+ android:id="@+id/phone_number"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_toEndOf="@id/phone_number_checkbox"
+ android:textAppearance="?android:attr/textAppearanceMedium"/>
+ <TextView
+ android:id="@+id/phone_number_description"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/phone_number"
+ android:layout_toEndOf="@id/phone_number_checkbox"
+ android:textAppearance="?android:attr/textAppearanceSmall"/>
+
+</com.android.car.dialer.widget.CheckableRelativeLayout>
diff --git a/res/layout/ringing_call_controller_bar_fragment.xml b/res/layout/ringing_call_controller_bar_fragment.xml
index 4613020d..b6868808 100644
--- a/res/layout/ringing_call_controller_bar_fragment.xml
+++ b/res/layout/ringing_call_controller_bar_fragment.xml
@@ -17,14 +17,14 @@ limitations under the License.
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
- android:layout_height="@dimen/car_action_bar_height"
- android:background="@color/phone_theme"
- android:elevation = "@dimen/in_call_card_elevation">
+ android:layout_height="@dimen/in_call_controller_bar_height"
+ android:background="@android:color/transparent">
<ImageView
android:id="@+id/answer_call_button"
- android:layout_width="@dimen/car_touch_target_size"
- android:layout_height="@dimen/car_touch_target_size"
+ android:layout_width="@dimen/ringing_call_button_touch_target_size"
+ android:layout_height="@dimen/ringing_call_button_touch_target_size"
+ android:background="@drawable/dialer_ripple_background"
android:scaleType="center"
android:src="@drawable/ic_phone"
android:tint="@color/phone_call"
@@ -38,9 +38,9 @@ limitations under the License.
android:id="@+id/answer_call_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginStart="@dimen/car_padding_4"
+ android:layout_marginStart="@dimen/ringing_call_text_margin"
android:text="@string/answer_call"
- style="@style/TextAppearance.Car.Body1"
+ android:textAppearance="?android:attr/textAppearanceLarge"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/answer_call_button"
app:layout_constraintEnd_toStartOf="@+id/mid_line"
@@ -55,8 +55,9 @@ limitations under the License.
<ImageView
android:id="@+id/end_call_button"
- android:layout_width="@dimen/car_touch_target_size"
- android:layout_height="@dimen/car_touch_target_size"
+ android:layout_width="@dimen/ringing_call_button_touch_target_size"
+ android:layout_height="@dimen/ringing_call_button_touch_target_size"
+ android:background="@drawable/dialer_ripple_background"
android:scaleType="center"
android:src="@drawable/ic_call_end"
android:tint="@color/phone_end_call"
@@ -70,9 +71,9 @@ limitations under the License.
android:id="@+id/end_call_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginStart="@dimen/car_padding_4"
+ android:layout_marginStart="@dimen/ringing_call_text_margin"
android:text="@string/decline_call"
- style="@style/TextAppearance.Car.Body1"
+ android:textAppearance="?android:attr/textAppearanceLarge"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/end_call_button"
app:layout_constraintEnd_toEndOf="parent"
diff --git a/res/drawable/car_list_item_background.xml b/res/layout/search_view.xml
index cbc97c7a..0832fa2c 100644
--- a/res/drawable/car_list_item_background.xml
+++ b/res/layout/search_view.xml
@@ -1,22 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- Copyright 2018, The Android Open Source Project
+ Copyright (C) 2019 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
+ http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
--->
-<ripple xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="@color/car_card_ripple_background">
- <item android:id="@android:id/mask">
- <color android:color="#ffffffff" />
- </item>
-</ripple>
+ -->
+
+<SearchView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/search_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
diff --git a/res/layout/strequents_fragment.xml b/res/layout/strequents_fragment.xml
deleted file mode 100644
index 2bb51333..00000000
--- a/res/layout/strequents_fragment.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2015 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<FrameLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="@color/phone_theme_secondary"
- android:clipChildren="false" >
-
- <androidx.car.widget.PagedListView
- android:id="@+id/list_view"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:layout_marginTop="@dimen/car_app_bar_height"
- android:clipChildren="false"
- app:showPagedListViewDivider="false" />
-</FrameLayout>
diff --git a/res/layout/telecom_activity.xml b/res/layout/telecom_activity.xml
index 68f83812..89828e67 100644
--- a/res/layout/telecom_activity.xml
+++ b/res/layout/telecom_activity.xml
@@ -13,31 +13,26 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
+
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="@color/telecom_activity_background_color">
-
- <FrameLayout
- android:layout_gravity="top|end"
- android:layout_width="@dimen/car_keyline_1"
- android:layout_height="@dimen/car_app_bar_height">
+ android:layout_height="match_parent">
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:id="@+id/content_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <!-- The main content view. Fragments will be added here. -->
+ <FrameLayout
+ android:id="@+id/content_fragment_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
- <ImageView
- android:id="@+id/search"
- android:background="@drawable/dialer_ripple_background"
- android:layout_gravity="center"
- android:layout_width="@dimen/car_touch_target_size"
- android:layout_height="@dimen/car_touch_target_size"
- android:src="@drawable/ic_search"
- android:scaleType="center"
- android:tint="@color/car_grey_50"/>
- </FrameLayout>
+ <include layout="@layout/top_bar_with_tabs"/>
+ </androidx.constraintlayout.widget.ConstraintLayout>
<FrameLayout
- android:id="@+id/content_fragment_container"
+ android:id="@+id/overlay_container"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
-
</FrameLayout>
diff --git a/res/layout/top_bar_with_tabs.xml b/res/layout/top_bar_with_tabs.xml
new file mode 100644
index 00000000..681b0b7e
--- /dev/null
+++ b/res/layout/top_bar_with_tabs.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<com.android.car.apps.common.ClickThroughToolbar
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/car_toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="?android:attr/actionBarSize"
+ android:background="@color/app_bar_background_color">
+ <com.android.car.apps.common.widget.CarTabLayout
+ android:id="@+id/tab_layout"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"/>
+</com.android.car.apps.common.ClickThroughToolbar>
diff --git a/res/layout/user_profile_large.xml b/res/layout/user_profile_large.xml
index 193b671e..84e8e392 100644
--- a/res/layout/user_profile_large.xml
+++ b/res/layout/user_profile_large.xml
@@ -14,29 +14,40 @@ limitations under the License.
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:gravity="center"
- android:orientation="vertical">
+ android:gravity="start|center_vertical"
+ android:orientation="horizontal"
+ android:paddingStart="@dimen/in_call_user_profile_margin"
+ android:paddingEnd="@dimen/in_call_user_profile_margin">
<ImageView
- android:id="@+id/avatar"
- android:layout_width="@dimen/car_large_avatar_size"
- android:layout_height="@dimen/car_large_avatar_size"
- android:layout_gravity="center"
- android:scaleType="fitCenter" />
- <TextView
- android:id="@+id/title"
- style="@style/TextAppearance.Car.Headline2"
- android:layout_width="match_parent"
+ android:id="@+id/user_profile_avatar"
+ android:layout_width="@dimen/in_call_avatar_icon_size"
+ android:layout_height="@dimen/in_call_avatar_icon_size"
+ android:scaleType="fitCenter"/>
+
+ <LinearLayout
android:layout_height="wrap_content"
- android:paddingTop="@dimen/car_padding_3"
- android:focusable="true"
- android:maxLines="1"
- android:gravity="center"/>
- <TextView
- android:id="@+id/body"
- style="@style/TextAppearance.Car.Body2"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:paddingTop="@dimen/car_padding_3"
- android:gravity="center"
- android:maxLines="1"/>
-</LinearLayout> \ No newline at end of file
+ android:layout_width="wrap_content"
+ android:orientation="vertical"
+ android:paddingStart="@dimen/in_call_margin_between_avatar_and_text">
+ <TextView
+ android:id="@+id/user_profile_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="@style/TextAppearance.InCallUserTitle"
+ android:singleLine="true"/>
+ <TextView
+ android:id="@+id/user_profile_phone_number"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="@style/TextAppearance.InCallUserPhoneNumber"
+ android:singleLine="true"
+ android:layout_marginTop="@dimen/in_call_phone_number_margin_top"/>
+ <Chronometer
+ android:id="@+id/user_profile_call_state"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="@style/TextAppearance.InCallState"
+ android:singleLine="true"
+ android:layout_marginTop="@dimen/in_call_state_margin_top"/>
+ </LinearLayout>
+</LinearLayout>
diff --git a/res/menu/contact_edit.xml b/res/menu/contact_edit.xml
new file mode 100644
index 00000000..abbdabcd
--- /dev/null
+++ b/res/menu/contact_edit.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:id="@+id/menu_contact_default_number"
+ android:title="@string/set_default_number"
+ android:actionProviderClass="com.android.car.dialer.ui.contact.ContactDefaultNumberActionProvider"
+ android:showAsAction="never"/>
+</menu>
diff --git a/res/menu/main_menu.xml b/res/menu/main_menu.xml
new file mode 100644
index 00000000..c32e70c8
--- /dev/null
+++ b/res/menu/main_menu.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <!-- Main search menu doesn't have an action view, onClick navigates to the search result page-->
+ <item android:id="@+id/menu_contacts_search"
+ android:icon="@drawable/ic_search"
+ android:title="@string/search_title"
+ android:showAsAction="always"
+ android:actionProviderClass="com.android.car.dialer.ui.menu.MenuActionProvider"/>
+
+ <!-- Dialer setting button -->
+ <item android:id="@+id/menu_dialer_setting"
+ android:icon="@drawable/ic_setting"
+ android:title="@string/setting_title"
+ android:showAsAction="always"
+ android:actionProviderClass="com.android.car.dialer.ui.menu.MenuActionProvider"/>
+</menu>
diff --git a/res/values-h1200dp/bools.xml b/res/values-h1200dp/bools.xml
new file mode 100644
index 00000000..b737bbca
--- /dev/null
+++ b/res/values-h1200dp/bools.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources>
+ <bool name="screen_size_tall">true</bool>
+</resources>
diff --git a/res/values-h1200dp/dimens.xml b/res/values-h1200dp/dimens.xml
new file mode 100644
index 00000000..98423573
--- /dev/null
+++ b/res/values-h1200dp/dimens.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources>
+ <!-- Keypad dimensions -->
+ <dimen name="keypad_margin_x">48dp</dimen>
+ <dimen name="keypad_margin_y">@*android:dimen/car_padding_4</dimen>
+ <dimen name="keypad_margin">@*android:dimen/car_padding_6</dimen>
+
+ <dimen name="contact_details_title_margin_bottom">@*android:dimen/car_padding_5</dimen>
+</resources>
diff --git a/res/values-h420dp/dimens.xml b/res/values-h420dp/dimens.xml
deleted file mode 100644
index d2e19f1c..00000000
--- a/res/values-h420dp/dimens.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2016 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<resources>
- <dimen name="in_call_controls_container_height">192dp</dimen>
- <dimen name="in_call_info_margin_top">72dp</dimen>
- <dimen name="in_call_card_dialpad_vertical_spacing">@dimen/car_padding_2</dimen>
- <dimen name="small_contact_photo_margin_end">72dp</dimen>
-</resources>
diff --git a/res/values-h600dp/dimens.xml b/res/values-h600dp/dimens.xml
deleted file mode 100644
index 8de61922..00000000
--- a/res/values-h600dp/dimens.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2016 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<resources>
- <dimen name="avatar_rounded_radius">98dp</dimen>
- <dimen name="car_key1_size">50sp</dimen>
- <dimen name="car_key2_size">22sp</dimen>
-</resources>
diff --git a/res/values-h610dp/bools.xml b/res/values-h610dp/bools.xml
new file mode 100644
index 00000000..ab691bc8
--- /dev/null
+++ b/res/values-h610dp/bools.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources>
+ <bool name="screen_size_short">false</bool>
+</resources>
diff --git a/res/values-h610dp/dimens.xml b/res/values-h610dp/dimens.xml
new file mode 100644
index 00000000..1947548e
--- /dev/null
+++ b/res/values-h610dp/dimens.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources>
+ <!-- Keypad dimensions -->
+ <dimen name="keypad_margin_x">@*android:dimen/car_padding_4</dimen>
+ <dimen name="keypad_margin_y">@*android:dimen/car_padding_2</dimen>
+ <dimen name="keypad_margin">@*android:dimen/car_padding_5</dimen>
+
+ <dimen name="fab_outline_size">104dp</dimen>
+ <dimen name="fab_ripple_radius">52dp</dimen>
+
+ <dimen name="contact_details_avatar_size">126dp</dimen>
+</resources>
diff --git a/res/values-h720dp/dimens.xml b/res/values-h720dp/dimens.xml
deleted file mode 100644
index c97e184e..00000000
--- a/res/values-h720dp/dimens.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2016 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<resources>
- <dimen name="avatar_rounded_radius">118dp</dimen>
- <dimen name="in_call_controls_container_height">236dp</dimen>
- <dimen name="in_call_info_margin_top">96dp</dimen>
- <dimen name="small_contact_photo_margin_end">96dp</dimen>
- <dimen name="in_call_card_dialpad_horizontal_spacing">@dimen/car_padding_6</dimen>
- <dimen name="in_call_card_dialpad_vertical_spacing">@dimen/car_padding_5</dimen>
-</resources>
diff --git a/res/values-night/colors.xml b/res/values-night/colors.xml
index 09f85f74..abd5d8ee 100644
--- a/res/values-night/colors.xml
+++ b/res/values-night/colors.xml
@@ -14,13 +14,12 @@
limitations under the License.
-->
<resources>
- <color name="phone_theme">@color/phone_theme_dark</color>
- <color name="phone_theme_secondary">@color/phone_theme_secondary_dark</color>
-
- <color name="telecom_display_scrim">#52000000</color> <!-- 32% black -->
- <color name="search_container_controls_tint">@color/car_grey_100</color>
- <color name="search_container_text_hint">@color/car_body2_light</color>
- <color name="car_caption">@color/car_caption_light</color>
- <color name="car_key1">@color/car_key1_light</color>
<color name="car_key2">@color/car_key2_light</color>
+
+ <!-- InCall page -->
+ <color name="onhold_call_background">@*android:color/car_grey_900</color>
+
+ <!-- Components -->
+ <color name="divider_color">@color/divider_color_dark</color>
+ <color name="secondary_icon_color">@color/secondary_icon_color_dark</color>
</resources>
diff --git a/res/values-w748dp/dimens.xml b/res/values-port/dimens.xml
index 1a856737..d210834f 100644
--- a/res/values-w748dp/dimens.xml
+++ b/res/values-port/dimens.xml
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='UTF-8'?>
-<!-- Copyright (C) 2015 The Android Open Source Project
+<!-- Copyright (C) 2019 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,6 +14,10 @@
limitations under the License.
-->
<resources>
- <!-- The max width of content in apps for adaptive responsive -->
- <dimen name="apps_max_content_width">748dp</dimen>
+ <dimen name="in_call_margin_between_avatar_and_text">48dp</dimen>
+ <dimen name="call_button_bottom_margin">@*android:dimen/car_padding_2</dimen>
+ <dimen name="call_button_height">@dimen/control_bar_height</dimen>
+ <dimen name="fab_outline_size">84dp</dimen>
+
+ <dimen name="keypad_margin">@*android:dimen/car_padding_4</dimen>
</resources>
diff --git a/res/values-port/strings.xml b/res/values-port/strings.xml
new file mode 100644
index 00000000..572bb37b
--- /dev/null
+++ b/res/values-port/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources>
+ <!-- Titles -->
+ <!-- Toolbar title for tabbed pages -->
+ <string name="default_toolbar_title">@string/phone_app_name</string>
+</resources> \ No newline at end of file
diff --git a/res/values-w1024dp/dimens.xml b/res/values-w1024dp/dimens.xml
deleted file mode 100644
index 9a833b1a..00000000
--- a/res/values-w1024dp/dimens.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version='1.0' encoding='UTF-8'?>
-<!-- Copyright (C) 2016 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<resources>
- <dimen name="line_divider_height">1dp</dimen>
-
- <dimen name="dialer_number_view_height">128dp</dimen>
- <dimen name="dialer_number_view_padding">32dp</dimen>
- <dimen name="dialer_dialed_number_container">640dp</dimen>
-
- <dimen name="call_fab_elevation">8dp</dimen>
- <dimen name="dial_number_call_button_width">160dp</dimen>
-
- <dimen name="in_call_card_corner_radius">160dp</dimen>
- <dimen name="in_call_large_contact_photo_size">632dp</dimen>
- <dimen name="in_call_card_margin_right">304dp</dimen>
- <dimen name="in_call_content_card_margin_start">@dimen/car_keyline_1</dimen>
- <dimen name="in_call_text_max_width">460dp</dimen>
- <dimen name="in_call_button_spacing">48dp</dimen>
- <dimen name="in_call_card_dialpad_width">468dp</dimen>
-
- <dimen name="car_card_view_corner_radius">0dp</dimen>
-
- <dimen name="icon_container_height">@dimen/call_log_item_height</dimen>
-
- <dimen name="call_log_icon_size">76dp</dimen>
- <dimen name="call_log_last_card_bottom_margin">@dimen/call_log_card_overlap</dimen>
-</resources>
diff --git a/res/values-w1200dp/dimens.xml b/res/values-w1200dp/dimens.xml
deleted file mode 100644
index 1a8920db..00000000
--- a/res/values-w1200dp/dimens.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2016 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<resources>
- <dimen name="in_call_card_dialpad_width">520dp</dimen>
- <dimen name="in_call_large_contact_photo_size">800dp</dimen>
- <dimen name="in_call_card_margin_right">544dp</dimen>
-</resources>
diff --git a/res/values-wheel/bools.xml b/res/values-wheel/bools.xml
deleted file mode 100644
index 03ef2244..00000000
--- a/res/values-wheel/bools.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-<?xml version='1.0' encoding='UTF-8'?>
-<!-- Copyright (C) 2015 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<resources>
-</resources> \ No newline at end of file
diff --git a/res/values-wheel/colors.xml b/res/values-wheel/colors.xml
deleted file mode 100644
index 03ef2244..00000000
--- a/res/values-wheel/colors.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-<?xml version='1.0' encoding='UTF-8'?>
-<!-- Copyright (C) 2015 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<resources>
-</resources> \ No newline at end of file
diff --git a/res/values/arrays.xml b/res/values/arrays.xml
new file mode 100644
index 00000000..9526d33d
--- /dev/null
+++ b/res/values/arrays.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources>
+ <!--The string constants are defined in {@link com.android.car.dialer.ui.TelecomPageTab.Page}
+ and they are used as key mapping to fragments. The array can only be a subset of predefined
+ strings in any order. Tabs will be added in the same order as defined in the array.-->
+ <string-array name="tabs_config">
+ <!--Add favorite tab back when we have favorite functionality-->
+ <!--<item>FAVORITE</item>-->
+ <item>CALL_HISTORY</item>
+ <item>CONTACTS</item>
+ <item>DIAL_PAD</item>
+ </string-array>
+
+ <string-array name="tabs_title">
+ <!-- This array is mapped to tabs_config. -->
+ <!-- And it should be consistent with the mapping in the TelecomPageTab.Factory -->
+ <!--Add favorite tab back when we have favorite functionality-->
+ <!--<item>@string/favorites_title</item>-->
+ <item>@string/call_history_title</item>
+ <item>@string/contacts_title</item>
+ <item>@string/dialpad_title</item>
+ </string-array>
+
+ <string-array name="contact_order_entry_values">
+ <item>given_name</item>
+ <item>family_name</item>
+ </string-array>
+
+ <string-array name="contact_order_entries">
+ <!-- This array is mapped to contact_order_keys. -->
+ <item>@string/give_name_first_title</item>
+ <item>@string/family_name_first_title</item>
+ </string-array>
+</resources>
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 4c0636bc..6ddffe9f 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -14,9 +14,9 @@
limitations under the License.
-->
<resources>
- <!-- The configurable text for a dialpad button. -->
- <declare-styleable name="DialpadButton">
- <!-- The dialpad number, such as "1". -->
+ <!-- The configurable text for a keypad button. -->
+ <declare-styleable name="KeypadButton">
+ <!-- The keypad number, such as "1". -->
<attr name="numberText" format="string" />
<!-- The letters below the number, such as "ABC". -->
diff --git a/res/drawable/rotary_in_call_dialpad_button_background.xml b/res/values/bools.xml
index bc0964bc..6cc068da 100644
--- a/res/drawable/rotary_in_call_dialpad_button_background.xml
+++ b/res/values/bools.xml
@@ -13,5 +13,8 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<ripple xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="@color/car_card_ripple_background" /> \ No newline at end of file
+
+<resources>
+ <bool name="screen_size_short">true</bool>
+ <bool name="screen_size_tall">false</bool>
+</resources> \ No newline at end of file
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 046bb201..2b99d101 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -14,59 +14,33 @@
limitations under the License.
-->
<resources>
- <!-- rail -->
- <color name="phone_theme">@color/phone_theme_light</color>
- <color name="phone_theme_secondary">@color/phone_theme_secondary_light</color>
+ <!-- InCall page -->
+ <color name="phone_call">@*android:color/car_green_700</color>
+ <color name="phone_end_call">@*android:color/car_red_500a</color>
+ <color name="onhold_call_background">@*android:color/car_grey_868</color>
- <color name="phone_theme_light">@color/car_grey_50</color>
- <color name="phone_theme_dark">@color/car_dark_blue_grey_700</color>
- <color name="phone_theme_secondary_light">@color/car_grey_200</color>
- <color name="phone_theme_secondary_dark">@color/car_dark_blue_grey_800</color>
+ <!-- Dialpad page -->
+ <color name="call_button_outline">@*android:color/car_green_500</color>
- <color name="primary_icon_color">@color/car_tint</color>
+ <!--Contact details-->
+ <color name="contact_details_icon_tint">@color/primary_icon_color</color>
- <color name="phone_status_bar_theme">@android:color/transparent</color>
- <color name="phone_call">@color/car_green_700</color>
- <color name="phone_end_call">@color/car_red_500a</color>
- <color name="phone_secondary_call_scrim">#990288d1</color>
- <color name="separator">#1f000000</color>
-
- <color name="call_history_list_item_color">@color/phone_theme</color>
- <color name="call_list_fragment_background">@color/phone_theme</color>
-
- <color name="contact_fragment_background">@color/phone_theme</color>
- <color name="contact_list_item_color">@color/phone_theme</color>
-
- <color name="dialer_fragment_background_color">@color/phone_theme</color>
- <color name="telecom_activity_background_color">@color/phone_theme</color>
-
- <!-- phone -->
- <color name="secondary_call_scrim">#990288d1</color>
-
- <!-- A scrim that covers the background of activities and darkens them for night mode. -->
- <color name="telecom_display_scrim">@android:color/transparent</color>
-
- <!-- The color of the icons in the search field. -->
- <color name="search_container_controls_tint">@color/car_grey_800</color>
-
- <!-- The color of the icons in the contact details page. -->
- <color name="contact_details_icon_tint">@color/search_container_controls_tint</color>
- <color name="contact_detail_fragment_background_color">@color/phone_theme_secondary</color>
- <color name="contact_detail_card_background_color">@color/phone_theme</color>
-
- <!-- The color of the hint text in the search field. -->
- <color name="search_container_text_hint">@color/car_grey_400</color>
-
- <!-- TextAppearances -->
- <color name="car_caption_light">@color/car_grey_400</color>
- <color name="car_caption_dark">@color/car_grey_700</color>
- <color name="car_caption">@color/car_caption_dark</color>
-
- <color name="car_key1_light">@color/car_grey_100</color>
- <color name="car_key1_dark">@color/car_light_blue_700</color>
- <color name="car_key1">@color/car_key1_dark</color>
-
- <color name="car_key2_light">@color/car_grey_400</color>
- <color name="car_key2_dark">@color/car_grey_650</color>
<color name="car_key2">@color/car_key2_dark</color>
+ <color name="car_key2_light">@*android:color/car_grey_400</color>
+ <color name="car_key2_dark">@*android:color/car_grey_700</color>
+
+ <color name="emergency_text_color">@*android:color/car_red_500a</color>
+ <color name="connect_bluetooth_text_color">@*android:color/accent_device_default_light</color>
+
+ <!-- Components -->
+ <color name="divider_color">@color/divider_color_light</color>
+ <color name="divider_color_dark">#1EFFFFFF</color>
+ <color name="divider_color_light">#38FFFFFF</color>
+ <color name="primary_icon_color">@*android:color/car_tint</color>
+ <color name="secondary_icon_color">@color/secondary_icon_color_light</color>
+ <color name="secondary_icon_color_dark">#8ADADCE0</color>
+ <color name="secondary_icon_color_light">#99FFFFFF</color>
+ <color name="hero_button_background_color">@*android:color/car_grey_868</color>
+ <color name="add_favorite_background_color">@*android:color/car_grey_868</color>
+
</resources>
diff --git a/res/values/configs.xml b/res/values/configs.xml
new file mode 100644
index 00000000..a189d136
--- /dev/null
+++ b/res/values/configs.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources>
+ <bool name="config_enable_dial_motion">true</bool>
+ <item name="config_dial_motion_scale_start" format="float" type="integer">1.5</item>
+ <integer name="config_dial_motion_duration">200</integer>
+</resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index e319b79a..2a9f5c42 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -15,97 +15,133 @@
-->
<resources>
<!-- Dialer -->
- <dimen name="dial_container_vertical_margin">48dp</dimen>
- <dimen name="dial_number_call_button_width">80dp</dimen>
-
<dimen name="dialer_card_elevation">2dp</dimen>
- <dimen name="dialer_dialpad_top_margin">-16dp</dimen>
- <dimen name="dialer_back_button_top_margin">-4dp</dimen>
- <dimen name="dialer_dialed_number_container">320dp</dimen>
- <dimen name="dialer_dialed_number_bottom_padding">16dp</dimen>
- <dimen name="dialer_menu_icon_container_width">64dp</dimen>
-
- <!-- In-call dimensions. -->
- <dimen name="in_call_card_margin_right">304dp</dimen>
- <dimen name="in_call_text_max_width">362dp</dimen>
- <dimen name="in_call_card_dialpad_width">300dp</dimen>
- <dimen name="in_call_large_contact_photo_size">480dp</dimen>
- <dimen name="in_call_content_card_margin_start">@dimen/car_keyline_1</dimen>
- <dimen name="avatar_rounded_radius">60dp</dimen>
- <dimen name="in_call_small_contact_photo_size">@dimen/car_touch_target_size</dimen>
- <dimen name="in_call_toggle_button_image_offset">30dp</dimen>
- <dimen name="in_call_info_margin_top">72dp</dimen>
- <dimen name="in_call_card_elevation">12dp</dimen>
- <dimen name="in_call_card_height">200dp</dimen>
-
- <!-- This should be -1 * in_call_card_right_margin -->
- <dimen name="in_call_card_dialpad_translation_x">-308dp</dimen>
- <dimen name="in_call_card_dialpad_corner_radius">16dp</dimen>
- <dimen name="in_call_card_dialpad_horizontal_spacing">@dimen/car_padding_5</dimen>
- <dimen name="in_call_card_dialpad_vertical_spacing">@dimen/car_padding_4</dimen>
-
- <dimen name="in_call_button_spacing">32dp</dimen>
- <dimen name="in_call_card_corner_radius">@dimen/car_card_view_corner_radius</dimen>
-
- <dimen name="small_contact_photo_margin_start">48dp</dimen>
- <dimen name="small_contact_photo_margin_end">0dp</dimen>
-
- <dimen name="card_margin">16dp</dimen>
-
- <!-- Touch Keyboard -->
- <dimen name="keyboard_key_letter_height">26dp</dimen>
-
- <dimen name="apps_max_content_width">748dp</dimen>
-
- <!-- The width of the dialpad. It's left should align with the beginning of the in call
- card's right rounded corner and extend to card margin. Therefore, this value should be
- in_call_card_right_margin + car_card_view_corner_radius - card_margin. -->
- <dimen name="call_log_card_overlap">-4dp</dimen>
- <dimen name="call_log_icon_margin">4dp</dimen>
- <dimen name="call_log_item_height">@dimen/car_single_line_list_item_height</dimen>
- <dimen name="call_log_icon_size">@dimen/car_primary_icon_size</dimen>
- <dimen name="car_card_view_corner_radius">2dp</dimen>
- <dimen name="favorite_card_corner_radius">@dimen/car_radius_5</dimen>
- <dimen name="favorite_card_space">@dimen/car_padding_1</dimen>
-
- <dimen name="icon_container_height">@dimen/car_primary_icon_size</dimen>
- <dimen name="avatar_icon_size">@dimen/car_avatar_size</dimen>
-
- <dimen name="contact_detail_image_size">@dimen/car_primary_icon_size</dimen>
- <dimen name="contact_detail_vertical_margin">16dp</dimen>
- <dimen name="contact_detail_name_card_height">192dp</dimen>
- <dimen name="contact_detail_card_corner_radius">@dimen/car_radius_3</dimen>
-
- <!-- The height of the container that contains the contact search field. -->
- <dimen name="search_container_height">128dp</dimen>
-
- <!-- The corner radius of the contact search container. -->
- <dimen name="search_container_radius">16dp</dimen>
- <!-- The elevation of the container when the contact list scrolls underneath it. -->
- <dimen name="search_container_elevation">8dp</dimen>
-
- <!-- The height of each of the cards that list the contact search results. -->
- <dimen name="contact_result_card_height">128dp</dimen>
-
- <dimen name="call_log_last_card_bottom_margin">@dimen/call_log_card_overlap</dimen>
-
- <dimen name="line_divider_height">1dp</dimen>
+ <!-- Audio route dimentions -->
+ <dimen name="audio_route_height">@dimen/control_bar_height</dimen>
+ <dimen name="audio_route_content_padding">@dimen/list_item_padding</dimen>
+ <dimen name="audio_route_icon_size">@dimen/primary_icon_size</dimen>
+ <dimen name="audio_route_constraint_guide_begin">@dimen/list_item_guideline</dimen>
+
+ <!-- Call list dimentions -->
+ <dimen name="call_history_item_height">@dimen/list_item_height</dimen>
+ <dimen name="call_history_item_padding">@dimen/list_item_padding</dimen>
+ <dimen name="call_history_guideline_begin">@dimen/list_item_guideline_begin</dimen>
+ <dimen name="call_history_guideline_end">@dimen/list_item_guideline_end</dimen>
+ <dimen name="call_history_text_margin_end">@dimen/list_item_text_margin_end</dimen>
+ <dimen name="call_history_icons_margin">@*android:dimen/car_padding_1</dimen>
+ <dimen name="call_log_icon_margin">4dp</dimen>
- <dimen name="dialer_number_view_height">128dp</dimen>
- <dimen name="dialer_number_view_padding">32dp</dimen>
+ <!-- Contact details dimensions -->
+ <dimen name="contact_details_avatar_margin_top">@*android:dimen/car_padding_4</dimen>
+ <dimen name="contact_details_title_margin_top">@*android:dimen/car_padding_4</dimen>
+ <dimen name="contact_details_title_margin_bottom">48dp</dimen>
+ <dimen name="contact_details_avatar_size">@dimen/primary_icon_size</dimen>
+ <dimen name="contact_details_item_height">@dimen/list_item_height</dimen>
+ <dimen name="contact_details_number_padding_start">@*android:dimen/car_keyline_1</dimen>
+ <dimen name="contact_details_number_padding_end">@*android:dimen/car_padding_2</dimen>
+ <dimen name="contact_details_text_button_guideline">@*android:dimen/car_keyline_3</dimen>
+ <dimen name="contact_details_action_bar_name_margin">@*android:dimen/car_padding_2</dimen>
+
+ <!-- Contact list dimensions -->
+ <dimen name="contact_list_item_height">@dimen/list_item_height</dimen>
+ <dimen name="contact_list_item_padding">@dimen/list_item_padding</dimen>
+ <dimen name="contact_list_guideline_begin">@dimen/list_item_guideline_begin</dimen>
+ <dimen name="contact_list_guideline_end">@dimen/list_item_guideline_end</dimen>
+ <dimen name="contact_list_text_margin_end">@dimen/list_item_text_margin_end</dimen>
+
+ <!-- Contact result dimensions -->
+ <dimen name="contact_result_height">@dimen/list_item_height</dimen>
+ <dimen name="contact_result_avatar_margin">@dimen/list_item_padding</dimen>
+ <dimen name="contact_result_name_margin">@dimen/list_item_guideline_begin</dimen>
+
+ <!-- In-call dimensions -->
+ <dimen name="in_call_controller_bar_height">@dimen/control_bar_height</dimen>
+ <dimen name="in_call_controller_bar_margin">@*android:dimen/car_padding_5</dimen>
+ <dimen name="in_call_controller_bar_margin_bottom">@*android:dimen/car_padding_2</dimen>
+ <dimen name="in_call_avatar_icon_size">196dp</dimen>
+ <dimen name="in_call_phone_number_margin_top">@*android:dimen/car_padding_2</dimen>
+ <dimen name="in_call_state_margin_top">@*android:dimen/car_padding_2</dimen>
+ <dimen name="in_call_margin_between_avatar_and_text">48dp</dimen>
+ <dimen name="in_call_user_profile_margin">@*android:dimen/car_margin</dimen>
+ <dimen name="onhold_user_info_height">@dimen/list_item_height</dimen>
+ <dimen name="onhold_profile_margin_x">@dimen/list_item_padding</dimen>
+ <dimen name="onhold_profile_margin_y">@*android:dimen/car_padding_3</dimen>
+ <dimen name="onhold_profile_corner_radius">8dp</dimen>
+ <dimen name="onhold_profile_avatar_margin">@*android:dimen/car_keyline_1</dimen>
+ <dimen name="onhold_profile_guideline">@dimen/list_item_guideline</dimen>
+ <dimen name="swap_call_button_margin">@*android:dimen/car_keyline_1</dimen>
+
+ <!-- Ringing call dimensions -->
+ <dimen name="ringing_call_button_touch_target_size">@dimen/touch_target_size</dimen>
+ <dimen name="ringing_call_text_margin">@*android:dimen/car_padding_4</dimen>
+
+ <!-- Dialpad dimensions -->
+ <dimen name="dialpad_line_divider_height">@dimen/vertical_divider_width</dimen>
+ <dimen name="dialpad_info_title_padding_size">@*android:dimen/car_padding_4</dimen>
+ <dimen name="dialpad_info_edge_padding_size">@*android:dimen/car_padding_2</dimen>
+ <dimen name="dialpad_info_guideline">@dimen/touch_target_size</dimen>
+ <dimen name="display_name_padding">@*android:dimen/car_padding_3</dimen>
+ <dimen name="call_state_padding">@*android:dimen/car_padding_3</dimen>
+
+ <!-- Keypad dimensions -->
+ <dimen name="keypad_minimum_size">@dimen/touch_target_size</dimen>
+ <dimen name="keypad_margin_x">@*android:dimen/car_padding_2</dimen>
+ <dimen name="keypad_margin_y">0dp</dimen>
+
+ <!-- Favorites dimensions -->
+ <dimen name="favorite_card_space_horizontal">@*android:dimen/car_padding_3</dimen>
+ <dimen name="favorite_card_space_vertical">@*android:dimen/car_padding_4</dimen>
+ <dimen name="favorites_avatar_margin_bottom">@*android:dimen/car_padding_3</dimen>
+ <dimen name="favorite_add_button_and_text_separation">@*android:dimen/car_padding_5</dimen>
+ <dimen name="favorite_add_button_padding">@*android:dimen/car_padding_4</dimen>
- <dimen name="dialer_fab_size">80dp</dimen>
<dimen name="call_fab_elevation">8dp</dimen>
- <dimen name="bksp_button_width">@dimen/car_touch_target_size</dimen>
- <!-- Fab button size is touch target (76dp) + stroke width (8dp) * 2-->
- <dimen name="fab_button_size">92dp</dimen>
+ <dimen name="bksp_button_width">@dimen/touch_target_size</dimen>
- <dimen name="in_call_button_size">@dimen/car_touch_target_size</dimen>
+ <dimen name="fab_button_size">@dimen/fab_outline_size</dimen>
+ <dimen name="fab_outline_size">84dp</dimen>
+ <dimen name="fab_outline_thickness">4dp</dimen>
+ <dimen name="fab_ripple_radius">42dp</dimen>
- <dimen name="user_profile_title_margin_top">@dimen/car_padding_3</dimen>
- <dimen name="user_profile_body_margin_top">@dimen/car_padding_3</dimen>
+ <dimen name="in_call_button_size">@dimen/touch_target_size</dimen>
- <dimen name="car_key1_size">40sp</dimen>
+ <dimen name="car_key1_size">32sp</dimen>
<dimen name="car_key2_size">18sp</dimen>
+
+ <!-- NoHfp error message page -->
+ <dimen name="no_hfp_icon_size">@dimen/primary_icon_size</dimen>
+ <dimen name="emergency_button_min_height">@dimen/touch_target_size</dimen>
+ <dimen name="emergency_button_min_width">@dimen/touch_target_width</dimen>
+ <dimen name="connect_bluetooth_button_min_height">@dimen/touch_target_size</dimen>
+ <dimen name="connect_bluetooth_button_min_width">@dimen/touch_target_width</dimen>
+ <dimen name="emergency_button_bottom_margin">@*android:dimen/car_padding_4</dimen>
+
+ <dimen name="list_top_padding">@*android:dimen/car_padding_2</dimen>
+ <!-- Components -->
+ <dimen name="list_item_height">@*android:dimen/car_single_line_list_item_height</dimen>
+ <dimen name="list_item_guideline">@*android:dimen/car_keyline_3</dimen>
+ <dimen name="list_item_guideline_begin">@*android:dimen/car_keyline_4</dimen>
+ <dimen name="list_item_guideline_end">@*android:dimen/car_keyline_3</dimen>
+ <dimen name="list_item_text_margin_end">@*android:dimen/car_padding_2</dimen>
+ <dimen name="list_item_padding">@*android:dimen/car_keyline_1</dimen>
+ <dimen name="list_divider_inset">@*android:dimen/car_keyline_1</dimen>
+ <dimen name="list_divider_height">@*android:dimen/car_list_divider_height</dimen>
+ <dimen name="vertical_divider_inset">@*android:dimen/car_padding_2</dimen>
+ <dimen name="vertical_divider_width">2dp</dimen>
+ <dimen name="primary_icon_size">@*android:dimen/car_primary_icon_size</dimen>
+ <dimen name="avatar_icon_size">76dp</dimen>
+ <dimen name="large_avatar_icon_size">96dp</dimen>
+ <dimen name="primary_icon_enclosing_circle_size">64dp</dimen>
+ <dimen name="inline_icon_size">24dp</dimen>
+ <dimen name="preference_list_margin">@*android:dimen/car_margin</dimen>
+ <dimen name="hero_button_corner_radius">38dp</dimen>
+ <dimen name="contact_avatar_corner_radius_percent" format="float">0.5</dimen>
+ <dimen name="touch_target_width">156dp</dimen>
+ <dimen name="phone_number_radio_list_padding">@*android:dimen/car_padding_2</dimen>
+
+ <!-- Toolbar -->
+ <!-- Half of the difference between car_margin and touch_target_size-->
+ <dimen name="menu_item_margin_x">18dp</dimen>
+
</resources>
diff --git a/res/values/integer.xml b/res/values/integer.xml
index b836fe40..12151478 100644
--- a/res/values/integer.xml
+++ b/res/values/integer.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources >
- <integer name="favorite_fragment_grid_column">2</integer>
+ <integer name="favorite_fragment_grid_column">3</integer>
</resources> \ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 9699a4bf..bc7996ab 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -17,59 +17,30 @@
<!-- Name of the phone dialer application [CHAR LIMIT=30]-->
<string name="phone_app_name">Phone</string>
<!-- Full-screen error text for when the phone can not be used because Bluetooth isn't connected [CHAR LIMIT=NONE]-->
- <string name="no_hfp">To make or receive calls, connect your phone to your car via Bluetooth.</string>
+ <string name="no_hfp">To complete your call, first connect your phone to your car via Bluetooth.</string>
+ <!-- Error text shown on the dialer facet when bluetooth is not supported -->
+ <string name="bluetooth_unavailable">Bluetooth is not available.</string>
<!-- Error text shown on the dialer facet for when bluetooth is disabled -->
<string name="bluetooth_disabled">To make or receive calls, turn Bluetooth on.</string>
<!-- Error text shown on the dialer facet for when bluetooth is not paired -->
<string name="bluetooth_unpaired">To make or receive calls, pair your phone with the car.</string>
- <!-- Toast text when a call failed because the network isn't available [CHAR LIMIT=40] -->
- <string name="error_network_not_available">Network not available.</string>
- <!-- Toast text when a call failed because the phone is in airplane mode [CHAR LIMIT=40] -->
- <string name="error_airplane_mode">Phone is in airplane mode.</string>
- <!-- Toast text when a call failed because bluetooth hfp is not available [CHAR LIMIT=40] -->
- <string name="error_no_hfp">Bluetooth is not connected.</string>
- <!-- Call status shown while a call is ongoing [CHAR LIMIT=40] -->
- <string name="phone_label_with_info">
- <xliff:g id="label" example="Mobile">%1$s</xliff:g>
- " \u00B7 "
- <xliff:g id="duration" example="1:20">%2$s</xliff:g>
- </string>
- <!-- Text displayed when the call log is empty. [CHAR LIMIT=40] -->
- <string name="recent_calls_empty">Your call log is empty.</string>
- <!-- Text displayed when the speed dial is empty. [CHAR LIMIT=100] -->
- <string name="speed_dial_empty">Speed Dial provides one-touch dialing for favorites and numbers you call often</string>
-
- <!-- Status label for phone state [CHAR LIMIT=20] -->
- <string name="call_state_connecting">Connecting</string>
- <!-- Status label for phone state [CHAR LIMIT=20] -->
- <string name="call_state_dialing">Dialing</string>
- <!-- Status label for phone state [CHAR LIMIT=20] -->
- <string name="call_state_hold">On Hold</string>
- <!-- Status label for phone state [CHAR LIMIT=20] -->
- <string name="call_state_call_ended">Call Ended</string>
- <!-- Status label for phone state [CHAR LIMIT=20] -->
- <string name="call_state_call_active">Connected</string>
- <!-- Status label for phone state [CHAR LIMIT=20] -->
- <string name="call_state_call_ringing">Ringing</string>
- <!-- Status label for phone state [CHAR LIMIT=20] -->
- <string name="call_state_call_ending">Disconnecting</string>
+ <!-- Button text for connecting to Bluetooth [CHAR LIMIT=40] -->
+ <string name="connect_bluetooth_button_text">Connect to Bluetooth</string>
+ <!-- Button text for making emergency call [CHAR LIMIT=40] -->
+ <string name="emergency_button_text">Emergency</string>
+ <!-- Text indicating emergency call [CHAR LIMIT=40] -->
+ <string name="emergency_call_description">Emergency call</string>
+ <!-- Message informing user that the contact has already been deleted [CHAR LIMIT=120] -->
+ <string name="error_contact_deleted">This contact might have been deleted.</string>
+ <!-- Toast text when user has inputted an invalid phone number [CHAR LIMIT=NONE] -->
+ <string name="error_invalid_phone_number">Can\'t dial this number. Check it and try again.</string>
<!-- Decline an in coming call [CHAR LIMIT=20] -->
<string name="decline_call">Decline</string>
<!-- Answer an in coming call [CHAR LIMIT=20] -->
<string name="answer_call">Answer</string>
-
- <!-- Label for voicemail [CHAR LIMIT=30] -->
- <string name="voicemail">Voicemail</string>
- <!-- Label for current phone call [CHAR LIMIT=30] -->
- <!-- Label for incoming call [CHAR LIMIT=30] -->
- <!-- Label for a recent call card [CHAR LIMIT=30] -->
- <!-- Label for button to answer a phone call [CHAR LIMIT=30] -->
- <!-- Label for button to reject a phone call [CHAR LIMIT=30] -->
- <!-- Label for when a call is coming from an unknown caller [CHAR LIMIT=30] -->
- <string name="unknown">Unknown</string>
- <!-- Label for when a call is a conference call [CHAR LIMIT=30] -->
- <string name="conference_call">Conference call</string>
+ <!-- Call back a missed call [CHAR LIMIT=20] -->
+ <string name="call_back">Call back</string>
<!-- Audio route -->
<!-- Label for routing phone audio to the vehicle [CHAR LIMIT=30] -->
@@ -79,66 +50,100 @@
<!-- Label for routing phone audio to the phone earpiece. [CHAR LIMIT=30] -->
<string name="audio_route_handset">Handset</string>
- <!-- Menu label for the call history [CHAR LIMIT=30] -->
- <string name="calllog_all">Call History</string>
- <!-- Menu label for the missed call history [CHAR LIMIT=30] -->
- <string name="calllog_missed">Missed</string>
- <!-- Menu label for the favorites [CHAR LIMIT=30] -->
- <string name="calllog_favorites">Favorites</string>
- <!-- Menu label for dial a number [CHAR LIMIT=30] -->
- <string name="calllog_dial_number">Dialpad</string>
<!-- Button label to dial a manually entered phone number -->
<string name="dial_a_number">Dial a number</string>
- <!-- Menu label for the call history [CHAR LIMIT=30] -->
- <string name="contact_menu_label">Contact</string>
-
<!-- Titles -->
<!-- Title for the favorites [CHAR LIMIT=30] -->
<string name="favorites_title">Favorites</string>
<!-- Title for the call history [CHAR LIMIT=30] -->
<string name="call_history_title">Call History</string>
- <!-- Title for the missed call history [CHAR LIMIT=30] -->
- <string name="missed_call_title">Missed</string>
<!-- Title for the contacts [CHAR LIMIT=30] -->
<string name="contacts_title">Contacts</string>
<!-- Title for the dialpad [CHAR LIMIT=30] -->
<string name="dialpad_title">Dialpad</string>
- <!-- Title for the in call [CHAR LIMIT=30] -->
- <string name="in_call_title">In Call</string>
-
- <!-- TODO: we do not need to localize this, figure out how to build ignoring it -->
- <string name="one">1</string>
- <string name="two">2</string>
- <string name="three">3</string>
- <string name="four">4</string>
- <string name="five">5</string>
- <string name="six">6</string>
- <string name="seven">7</string>
- <string name="eight">8</string>
- <string name="nine">9</string>
- <string name="zero">0</string>
- <string name="star">*</string>
- <string name="pound">#</string>
-
- <string name="two_letters">ABC</string>
- <string name="three_letters">DEF</string>
- <string name="four_letters">GHI</string>
- <string name="five_letters">JKL</string>
- <string name="six_letters">MNO</string>
- <string name="seven_letters">PQRS</string>
- <string name="eight_letters">TUV</string>
- <string name="nine_letters">WXYZ</string>
- <string name="zero_letters">+</string>
+ <!-- Toolbar title for tabbed pages -->
+ <string name="default_toolbar_title" translatable="false"></string>
+
+ <!-- Button to add start choosing a contact to add as a new favorite [CHAR_LIMIT=50] -->
+ <string name="add_favorite_button">Add a favorite</string>
+ <!-- Error message shown when on the favorites page without any favorites added [CHAR_LIMIT=80] -->
+ <string name="favorites_empty">You haven\'t added any favorites yet</string>
+
+ <!-- Keypad strings-->
+ <string name="one" translatable="false">1</string>
+ <string name="two" translatable="false">2</string>
+ <string name="three" translatable="false">3</string>
+ <string name="four" translatable="false">4</string>
+ <string name="five" translatable="false">5</string>
+ <string name="six" translatable="false">6</string>
+ <string name="seven" translatable="false">7</string>
+ <string name="eight" translatable="false">8</string>
+ <string name="nine" translatable="false">9</string>
+ <string name="zero" translatable="false">0</string>
+ <string name="star" translatable="false">*</string>
+ <string name="pound" translatable="false">#</string>
+
+ <string name="two_letters" translatable="false">ABC</string>
+ <string name="three_letters" translatable="false">DEF</string>
+ <string name="four_letters" translatable="false">GHI</string>
+ <string name="five_letters" translatable="false">JKL</string>
+ <string name="six_letters" translatable="false">MNO</string>
+ <string name="seven_letters" translatable="false">PQRS</string>
+ <string name="eight_letters" translatable="false">TUV</string>
+ <string name="nine_letters" translatable="false">WXYZ</string>
+ <string name="zero_letters" translatable="false">+</string>
<!-- Using spaces here so that the positioning of the number is consistent. -->
- <string name="star_letters"> </string>
- <string name="pound_letters"> </string>
+ <string name="one_letters" translatable="false"></string>
+ <string name="star_letters" translatable="false"></string>
+ <string name="pound_letters" translatable="false"></string>
<string name="search_title">Search contacts</string>
<string name="search_hint">Search contacts</string>
- <string name="type_home">Home</string>
- <string name="type_work">Work</string>
- <string name="type_mobile">Mobile</string>
- <string name="type_other">Other</string>
+ <string name="type_multiple">Multiple</string>
+
+ <!-- Title for choose a phone number dialog to make a call or send sms [CHAR LIMIT=60] -->
+ <string name="select_number_dialog_title">Choose a phone number</string>
+ <!-- Button for choose a phone number dialog to select the number for just once [CHAR LIMIT=30] -->
+ <string name="select_number_dialog_just_once_button">Just once</string>
+ <!-- Button for choose a phone number dialog to set the selected number as default [CHAR LIMIT=30] -->
+ <string name="select_number_dialog_always_button">Always</string>
+ <!-- Title for set default phone number dialog [CHAR LIMIT=60]-->
+ <string name="set_default_number">Set default phone number</string>
+ <!-- Description for the default phone number of the contact [CHAR LIMIT=30] -->
+ <string name="primary_number_description">
+ <xliff:g id="label" example="Mobile">%1$s</xliff:g>
+ " - Default"
+ </string>
+
+ <!-- Heads Up Notification -->
+ <!-- Name of incoming call notification channel in app info [CHAR LIMIT=50] -->
+ <string name="in_call_notification_channel_name">Incoming call notification</string>
+ <!-- Text for incoming call notification [CHAR LIMIT=40]-->
+ <string name="notification_incoming_call">Incoming call</string>
+ <!-- Name of missed call notification channel in app info [CHAR LIMIT=50] -->
+ <string name="missed_call_notification_channel_name">Missed call notification</string>
+ <!-- Title for missed call notification [CHAR LIMIT=40]-->
+ <string name="notification_missed_call">Missed call</string>
+
+ <!-- Onhold User Profile Info -->
+ <!-- Text to show the call is onhold [CHAR LIMIT=40]-->
+ <string name="onhold_call_label">On Hold</string>
+
+ <!-- Dialer Setting -->
+ <!-- Title of the settings page [CHAR LIMIT=30]-->
+ <string name="setting_title">Settings</string>
+ <!-- Title of the settings to change the start page [CHAR LIMIT=40]-->
+ <string name="pref_start_page_title">Set start page</string>
+ <string name="pref_start_page_key" translatable="false">set_start_page</string>
+
+ <!-- Title of the settings to sort contact order [CHAR LIMIT=40]-->
+ <string name="sort_order_title">Contact Order</string>
+ <string name="sort_order_key" translatable="false">contact_order</string>
+ <!-- Title of the settings for sorting Contacts in different orders -->
+ <!-- Title of given name [CHAR LIMIT=40]-->
+ <string name="give_name_first_title">First name</string>
+ <!-- Title of family name [CHAR LIMIT=40]-->
+ <string name="family_name_first_title">Last name</string>
</resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index ab9e5967..7d4fff98 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -14,26 +14,35 @@
limitations under the License.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android">
- <style name="TextAppearance.Car.Key1" >
- <item name="android:textStyle">normal</item>
+ <!-- Dialpad -->
+ <style name="TextAppearance.DialNumber" parent="@style/TextAppearance.Display3"/>
+ <style name="TextAppearance.EmergencyDialNumber" parent="@style/TextAppearance.DialNumber">
+ <item name="android:textColor">@color/emergency_text_color</item>
+ </style>
+ <style name="TextAppearance.DialpadDisplayName" parent="@style/TextAppearance.Body1"/>
+
+ <style name="KeypadNumber">
+ <item name="android:textAppearance">?android:attr/textAppearanceLarge</item>
<item name="android:textSize">@dimen/car_key1_size</item>
- <item name="android:textColor">@color/car_key1</item>
</style>
- <style name="TextAppearance.Car.Key2" >
- <item name="android:textStyle">normal</item>
+ <style name="KeypadLetter">
+ <item name="android:textAppearance">?android:attr/textAppearanceSmall</item>
<item name="android:textSize">@dimen/car_key2_size</item>
- <item name="android:textColor">@color/car_key2</item>
</style>
<!-- Phone -->
- <style name="DialpadKeyButtonStyle">
+ <style name="KeypadButtonStyle">
<item name="android:clickable">true</item>
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
- <item name="android:minWidth">@dimen/car_touch_target_size</item>
- <item name="android:minHeight">@dimen/car_touch_target_size</item>
- <item name="android:background">@drawable/dialpad_button_background</item>
+ <item name="android:paddingStart">@dimen/keypad_margin_x</item>
+ <item name="android:paddingEnd">@dimen/keypad_margin_x</item>
+ <item name="android:paddingTop">@dimen/keypad_margin_y</item>
+ <item name="android:paddingBottom">@dimen/keypad_margin_y</item>
+ <item name="android:minWidth">@dimen/touch_target_size</item>
+ <item name="android:minHeight">@dimen/touch_target_size</item>
+ <item name="android:background">?android:attr/selectableItemBackground</item>
<item name="android:focusable">true</item>
</style>
@@ -47,24 +56,86 @@
<style name="DialpadSecondaryButton">
<item name="android:layout_width">@dimen/bksp_button_width</item>
<item name="android:layout_height">@dimen/bksp_button_width</item>
- <item name="android:background">@drawable/dialpad_delete_button_background</item>
+ <item name="android:background">@drawable/dialer_ripple_background</item>
<item name="android:scaleType">centerInside</item>
- <item name="android:tint">@color/car_tint</item>
+ <item name="android:tint">@color/primary_icon_color</item>
</style>
- <style name="InCallDialpad">
- <item name="android:focusable">true</item>
- <item name="android:background">@drawable/rotary_in_call_dialpad_button_background</item>
+ <style name="NoHfpText">
+ <item name="android:textAppearance">?android:attr/textAppearanceMedium</item>
<item name="android:gravity">center</item>
- <item name="android:fontFamily">sans-serif-condensed</item>
- <item name="android:textStyle">normal</item>
- <item name="android:textSize">@dimen/keyboard_key_letter_height</item>
- <item name="android:textColor">@color/car_headline1</item>
+ <item name="android:maxLines">3</item>
</style>
- <style name="NoHfpText" parent="TextAppearance.Car.Body2">
- <item name="android:textColor">@color/car_body1_light</item>
- <item name="android:gravity">center</item>
- <item name="android:maxLines">3</item>
+ <!--TODO: fix it system wide instead of creating override-->
+ <style name="Widget.Dialer.Toolbar" parent="*android:Widget.DeviceDefault.Toolbar">
+ <!-- No padding for navigation button which is car_margin wide -->
+ <item name="android:paddingStart">0dp</item>
+ <!-- Menu item has padding/margin to define the end distance from toolbar edge -->
+ <item name="android:paddingEnd">0dp</item>
+ <item name="android:contentInsetStart">0dp</item>
+ <item name="android:contentInsetEnd">@*android:dimen/car_margin</item>
+ <!-- Override the max height of navigation button and menu button -->
+ <item name="android:maxButtonHeight">?android:attr/actionBarSize</item>
+ <item name="android:navigationIcon">@drawable/ic_arrow_back</item>
+ <item name="android:titleTextAppearance">@style/TextAppearance.Dialer.Widget.Toolbar.Title
+ </item>
+ <!-- Navigation button style -->
+ <item name="*android:navigationButtonStyle">@style/Widget.Dialer.Navigation</item>
+ </style>
+
+ <style name="TextAppearance.Dialer.Widget.Toolbar.Title"
+ parent="*android:TextAppearance.DeviceDefault.Widget.Toolbar.Title">
+ <item name="android:textSize">@*android:dimen/car_body1_size</item>
+ </style>
+
+ <style name="Widget.Dialer.Navigation"
+ parent="android:Widget.Material.Toolbar.Button.Navigation">
+ <item name="android:minWidth">@*android:dimen/car_margin</item>
+ <item name="android:background">?android:attr/actionBarItemBackground</item>
+ </style>
+
+ <style name="Widget.Dialer.ActionButton" parent="android:Widget.DeviceDefault.ActionButton">
+ <item name="android:minWidth">@dimen/touch_target_size</item>
+ <item name="android:minHeight">@dimen/touch_target_size</item>
+ </style>
+
+ <style name="Widget.Dialer.ActionButton.Overflow" parent="android:Widget.DeviceDefault.ActionButton.Overflow">
+ <item name="android:src">@drawable/ic_overflow</item>
+ <item name="android:minWidth">@dimen/touch_target_size</item>
+ <item name="android:minHeight">@dimen/touch_target_size</item>
+ <item name="android:paddingStart">0dp</item>
+ <item name="android:paddingEnd">@dimen/menu_item_margin_x</item>
+ <item name="android:background">@android:color/transparent</item>
+ </style>
+
+ <!-- In-call styling for everything except short landscape screens -->
+ <style name="TextAppearance.InCallUserTitle" parent="@style/TextAppearance.Display2"/>
+ <style name="TextAppearance.InCallUserPhoneNumber" parent="@style/TextAppearance.Body1">
+ <item name="android:textColor">#B8FFFFFF</item>
+ </style>
+ <style name="TextAppearance.InCallState" parent="@style/TextAppearance.Body1">
+ <item name="android:textColor">#B8FFFFFF</item>
</style>
+
+ <!-- Call history -->
+ <style name="TextAppearance.CallLogTitleDefault" parent="@style/TextAppearance.Body1"/>
+
+ <!-- Customized text color for missed calls can be added here -->
+ <style name="TextAppearance.CallLogTitleMissedCall" parent="@style/TextAppearance.Body1"/>
+
+ <!-- Contact details -->
+ <style name="TextAppearance.ContactDetailsTitle" parent="@style/TextAppearance.Display2"/>
+
+ <style name="TextAppearance.ContactResultTitle" parent="@style/TextAppearance.Body1"/>
+
+ <!-- Display options defined for ActionBar-->
+ <style name="RootToolbarDisplayOptions">
+ <item name="android:displayOptions">useLogo|showHome|showTitle|showCustom</item>
+ </style>
+
+ <style name="HomeAsUpDisplayOptions">
+ <item name="android:displayOptions">showTitle|homeAsUp|showCustom</item>
+ </style>
+
</resources>
diff --git a/res/drawable/ic_play.xml b/res/values/styles_preference.xml
index d0c77c1e..d9d0ebac 100644
--- a/res/drawable/ic_play.xml
+++ b/res/values/styles_preference.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2018 The Android Open Source Project
+<!-- 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.
@@ -13,7 +13,16 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
-<vector android:height="44dp" android:viewportHeight="48.0"
- android:viewportWidth="48.0" android:width="44dp" xmlns:android="http://schemas.android.com/apk/res/android">
- <path android:fillColor="#000000" android:pathData="M16,10v28l22,-14z"/>
-</vector>
+
+<resources>
+ <style name="PreferenceFragmentList.Settings">
+ <item name="android:paddingTop">0dp</item>
+ <item name="android:paddingBottom">0dp</item>
+ <item name="android:paddingStart">0dp</item>
+ <item name="android:paddingEnd">0dp</item>
+ <item name="android:paddingLeft">0dp</item>
+ <item name="android:paddingRight">0dp</item>
+ </style>
+
+</resources>
+
diff --git a/res/values/themes.xml b/res/values/themes.xml
index d09bffb0..9317fb88 100644
--- a/res/values/themes.xml
+++ b/res/values/themes.xml
@@ -14,20 +14,23 @@ See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
- <!-- The theme for the TelecomActivity. -->
- <style name="TelecomActivityTheme" parent="@style/Theme.Car.Light.NoActionBar.Drawer" >
- <item name="android:statusBarColor">@color/phone_status_bar_theme</item>
- <item name="android:colorPrimary">@android:color/transparent</item>
- <item name="android:colorPrimaryDark">@color/phone_status_bar_theme</item>
- <item name="android:textColorPrimary">@android:color/black</item>
- <item name="drawerHeaderColor">@color/car_title2</item>
- <item name="drawerBackgroundColor">@color/phone_theme</item>
+ <!-- The theme for the Dialer app. -->
+ <style name="Theme.Dialer" parent="android:Theme.DeviceDefault.NoActionBar">
+ <item name="android:actionBarItemBackground">@drawable/action_button_background</item>
+ <!-- Menu button style -->
+ <item name="android:actionButtonStyle">@style/Widget.Dialer.ActionButton</item>
+ <item name="android:actionOverflowButtonStyle">@style/Widget.Dialer.ActionButton.Overflow
+ </item>
+ <item name="android:listDivider">@drawable/list_divider</item>
+ <item name="android:toolbarStyle">@style/Widget.Dialer.Toolbar</item>
</style>
- <!-- The theme for the contact search page. -->
- <style name="ContactSearchActivityTheme" parent="@android:style/Theme.Material.Light.NoActionBar">
- <item name="android:statusBarColor">@color/phone_status_bar_theme</item>
- <item name="android:colorPrimary">@android:color/transparent</item>
- <item name="android:colorPrimaryDark">@color/phone_status_bar_theme</item>
+ <style name="Theme.Dialer.Setting" parent="Theme.Dialer">
+ <item name="preferenceTheme">@style/PreferenceTheme</item>
</style>
+
+ <style name="PreferenceTheme">
+ <item name="preferenceFragmentListStyle">@style/PreferenceFragmentList.Settings</item>
+ </style>
+
</resources>
diff --git a/res/drawable/ongoing_call_secondary_action_background.xml b/res/xml/searchable.xml
index 3b3c3caa..c7f4995f 100644
--- a/res/drawable/ongoing_call_secondary_action_background.xml
+++ b/res/xml/searchable.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2015 The Android Open Source Project
+<!-- Copyright (C) 2019 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -13,8 +13,8 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<inset
+
+<searchable
xmlns:android="http://schemas.android.com/apk/res/android"
- android:inset="32dp" >
- <ripple android:color="@color/car_card_ripple_background_dark" />
-</inset>
+ android:label="@string/search_title"
+ android:hint="@string/search_hint"/>
diff --git a/res/xml/settings_page.xml b/res/xml/settings_page.xml
new file mode 100644
index 00000000..19876fef
--- /dev/null
+++ b/res/xml/settings_page.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<PreferenceScreen
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:title="@string/setting_title">
+
+ <ListPreference
+ android:key="@string/pref_start_page_key"
+ android:title="@string/pref_start_page_title"
+ android:entries="@array/tabs_title"
+ android:entryValues="@array/tabs_config"/>
+
+ <ListPreference
+ android:key="@string/sort_order_key"
+ android:title="@string/sort_order_title"
+ android:entries="@array/contact_order_entries"
+ android:entryValues="@array/contact_order_entry_values"/>
+</PreferenceScreen>
diff --git a/src/com/android/car/dialer/BitmapWorkerTask.java b/src/com/android/car/dialer/BitmapWorkerTask.java
deleted file mode 100644
index 04adfe1d..00000000
--- a/src/com/android/car/dialer/BitmapWorkerTask.java
+++ /dev/null
@@ -1,118 +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.car.dialer;
-
-import android.content.ContentResolver;
-import android.graphics.Bitmap;
-import android.os.AsyncTask;
-import android.widget.ImageView;
-
-import com.android.car.dialer.telecom.TelecomUtils;
-
-import java.lang.ref.WeakReference;
-
-/**
- * AsyncTask for handling getting contact photo from number.
- */
-public class BitmapWorkerTask extends AsyncTask<Void, Void, Bitmap> {
- private final WeakReference<ImageView> imageViewReference;
- private final WeakReference<ContentResolver> contentResolverReference;
- private final String mNumber;
- private final BitmapRunnable mRunnable;
-
- public BitmapWorkerTask(
- ContentResolver contentResolver, ImageView imageView,
- String number, BitmapRunnable runnable) {
- imageViewReference = new WeakReference<>(imageView);
- contentResolverReference = new WeakReference<>(contentResolver);
- mNumber = number;
- mRunnable = runnable;
- }
-
- @Override
- protected Bitmap doInBackground(Void... voids) {
- final ContentResolver contentResolver = contentResolverReference.get();
- if (contentResolver != null) {
- return TelecomUtils.getContactPhotoFromNumber(contentResolver, mNumber);
- }
- return null;
- }
-
- @Override
- protected void onPostExecute(Bitmap bitmap) {
- if (isCancelled()) {
- return;
- }
-
- final ImageView imageView = imageViewReference.get();
- if (imageView != null) {
- final BitmapWorkerTask bitmapWorkerTask = (BitmapWorkerTask) imageView.getTag();
- if (this == bitmapWorkerTask) {
- mRunnable.setBitmap(bitmap);
- mRunnable.setImageView(imageView);
- mRunnable.setNumber(mNumber);
- mRunnable.run();
- }
- }
- }
-
- public static void loadBitmap(
- ContentResolver contentResolver, ImageView imageView,
- String number, BitmapRunnable runnable) {
- if (cancelPotentialWork(number, imageView)) {
- final BitmapWorkerTask task =
- new BitmapWorkerTask(contentResolver, imageView, number, runnable);
- imageView.setTag(task);
- imageView.setImageResource(0);
- task.execute();
- }
- }
-
- private static boolean cancelPotentialWork(String number, ImageView imageView) {
- final BitmapWorkerTask bitmapWorkerTask = (BitmapWorkerTask) imageView.getTag();
- if (bitmapWorkerTask != null) {
- if (bitmapWorkerTask.mNumber != number) {
- bitmapWorkerTask.cancel(true);
- } else {
- // The same work is already in progress
- return false;
- }
- }
-
- return true;
- }
-
- /**
- * Generate interface for handling logic after getting bitmap.
- */
- public static abstract class BitmapRunnable implements Runnable {
- protected String mNumber;
- protected Bitmap mBitmap;
- protected ImageView mImageView;
-
- public void setBitmap(Bitmap bitmap) {
- mBitmap = bitmap;
- }
-
- public void setImageView(ImageView imageView) {
- mImageView = imageView;
- }
-
- public void setNumber(String number) {
- mNumber = number;
- }
- }
-}
diff --git a/src/com/android/car/dialer/CallListener.java b/src/com/android/car/dialer/CallListener.java
deleted file mode 100644
index b09ec8d4..00000000
--- a/src/com/android/car/dialer/CallListener.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package com.android.car.dialer;
-
-import com.android.car.dialer.telecom.UiCall;
-
-/** Interface for listening to call state changes. */
-public interface CallListener {
- void onAudioStateChanged(boolean isMuted, int route, int supportedRouteMask);
-
- void onCallStateChanged(UiCall call, int state);
-
- void onCallUpdated(UiCall call);
-
- void onCallAdded(UiCall call);
-
- void onCallRemoved(UiCall call);
-}
diff --git a/src/com/android/car/dialer/CallLogViewHolder.java b/src/com/android/car/dialer/CallLogViewHolder.java
deleted file mode 100644
index 1ef8ad07..00000000
--- a/src/com/android/car/dialer/CallLogViewHolder.java
+++ /dev/null
@@ -1,55 +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.car.dialer;
-
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.FrameLayout;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import androidx.recyclerview.widget.RecyclerView;
-
-/**
- * A {@link androidx.recyclerview.widget.RecyclerView.ViewHolder} that will hold layouts that
- * are inflated by {@link StrequentsAdapter}.
- */
-public class CallLogViewHolder extends RecyclerView.ViewHolder {
- public final FrameLayout iconContainer;
- public final ImageView icon;
- public final TextView title;
- public final TextView text;
- public final View card;
- public final ViewGroup container;
- public final LinearLayout callType;
- public final CallTypeIconsView callTypeIconsView;
- public final ImageView smallIcon;
-
- public CallLogViewHolder(View v) {
- super(v);
-
- icon = v.findViewById(R.id.icon);
- iconContainer = v.findViewById(R.id.icon_container);
- title = v.findViewById(R.id.title);
- text = v.findViewById(R.id.text);
- card = v.findViewById(R.id.call_log_card);
- callType = v.findViewById(R.id.call_type);
- callTypeIconsView = v.findViewById(R.id.call_type_icons);
- smallIcon = v.findViewById(R.id.small_icon);
- container = v.findViewById(R.id.container);
- }
-}
diff --git a/src/com/android/car/dialer/CallTypeIconsView.java b/src/com/android/car/dialer/CallTypeIconsView.java
deleted file mode 100644
index caa0e289..00000000
--- a/src/com/android/car/dialer/CallTypeIconsView.java
+++ /dev/null
@@ -1,132 +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.car.dialer;
-
-import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.PorterDuff;
-import android.graphics.drawable.Drawable;
-import android.util.AttributeSet;
-import android.view.View;
-
-import com.android.car.dialer.telecom.PhoneLoader;
-
-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.
- *
- * TODO(mcrico): Move to shared.
- */
-public class CallTypeIconsView extends View {
- private List<Integer> mCallTypes = new ArrayList<>(MAX_CALL_TYPE_ICONS);
- private Resources mResources;
- private int mWidth;
- private int mHeight;
-
- public static final int MAX_CALL_TYPE_ICONS = 3;
-
- public CallTypeIconsView(Context context) {
- this(context, null);
- }
-
- public CallTypeIconsView(Context context, AttributeSet attrs) {
- super(context, attrs);
- mResources = new Resources(context);
- mResources.voicemail.setColorFilter(
- getResources().getColor(R.color.car_tint), PorterDuff.Mode.SRC_IN);
- }
-
- public void clear() {
- mCallTypes.clear();
- mWidth = 0;
- mHeight = 0;
- requestLayout();
- }
-
- public void add(int callType) {
- mCallTypes.add(callType);
-
- final Drawable drawable = getCallTypeDrawable(callType);
- mWidth += drawable.getIntrinsicWidth() + mResources.iconMargin;
- mHeight = Math.max(mHeight, drawable.getIntrinsicHeight());
- requestLayout();
- }
-
- public int getCount() {
- return mCallTypes.size();
- }
-
- public int getCallType(int index) {
- return mCallTypes.get(index);
- }
-
- private Drawable getCallTypeDrawable(int callType) {
- switch (callType) {
- case PhoneLoader.INCOMING_TYPE:
- return mResources.incoming;
- case PhoneLoader.OUTGOING_TYPE:
- return mResources.outgoing;
- case PhoneLoader.MISSED_TYPE:
- return mResources.missed;
- case PhoneLoader.VOICEMAIL_TYPE:
- return mResources.voicemail;
- 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 mResources.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 + mResources.iconMargin;
- }
- }
-
- private static class Resources {
- public final Drawable incoming;
- public final Drawable outgoing;
- public final Drawable missed;
- public final Drawable voicemail;
- public final int iconMargin;
-
- public Resources(Context context) {
- final android.content.res.Resources r = context.getResources();
- incoming = r.getDrawable(R.drawable.ic_call_received);
- outgoing = r.getDrawable(R.drawable.ic_call_made);
- missed = r.getDrawable(R.drawable.ic_call_missed);
- voicemail = r.getDrawable(R.drawable.ic_call_voicemail);
- iconMargin = r.getDimensionPixelSize(R.dimen.call_log_icon_margin);
- }
- }
-}
diff --git a/src/com/android/car/dialer/Constants.java b/src/com/android/car/dialer/Constants.java
new file mode 100644
index 00000000..a3cc08f6
--- /dev/null
+++ b/src/com/android/car/dialer/Constants.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer;
+
+import com.android.car.dialer.ui.TelecomActivity;
+
+/** Dialer constants. */
+public final class Constants {
+ private Constants() {
+ }
+
+ /** Constants used to build {@link android.content.Intent}s. */
+ public static class Intents {
+ /** Intent action for {@link TelecomActivity} to show a tabbed page. */
+ public static final String ACTION_SHOW_PAGE = "com.android.car.dialer.ACTION_SHOW_PAGE";
+ /** Intent extra for {@link TelecomActivity} to show a tabbed page. */
+ public static final String EXTRA_SHOW_PAGE = "com.android.car.dialer.EXTRA_SHOW_PAGE";
+ /** Intent extra flag to mark unread missed calls as read. */
+ public static final String EXTRA_ACTION_READ_MISSED =
+ "com.android.car.dialer.EXTRA_ACTION_READ_MISSED";
+ /** Intent extra flag to show incoming call. */
+ public static final String EXTRA_SHOW_INCOMING_CALL = "show_incoming_call";
+ }
+
+ /** Constants used by {@link androidx.core.app.JobIntentService}s. */
+ public static class JobIds {
+ public static final int NOTIFICATION_SERVICE = 2019;
+ }
+}
diff --git a/src/com/android/car/dialer/ContactDetailsFragment.java b/src/com/android/car/dialer/ContactDetailsFragment.java
deleted file mode 100644
index 72de11da..00000000
--- a/src/com/android/car/dialer/ContactDetailsFragment.java
+++ /dev/null
@@ -1,338 +0,0 @@
-/*
- * 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.car.dialer;
-
-import android.content.Intent;
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.Bundle;
-import android.provider.ContactsContract;
-import android.util.Log;
-import android.util.Pair;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import com.android.car.dialer.telecom.TelecomUtils;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import androidx.annotation.ColorInt;
-import androidx.annotation.Nullable;
-import androidx.car.utils.ListItemBackgroundResolver;
-import androidx.car.widget.DayNightStyle;
-import androidx.car.widget.PagedListView;
-import androidx.fragment.app.Fragment;
-import androidx.loader.app.LoaderManager;
-import androidx.loader.content.CursorLoader;
-import androidx.loader.content.Loader;
-import androidx.recyclerview.widget.RecyclerView;
-
-/**
- * A fragment that shows the name of the contact, the photo and all listed phone numbers. It is
- * primarily used to respond to the results of search queries but supplyig it with the content://
- * uri of a contact should work too.
- */
-public class ContactDetailsFragment extends Fragment
- implements LoaderManager.LoaderCallbacks<Cursor> {
- private static final String TAG = "ContactDetailsFragment";
- private static final String TELEPHONE_URI_PREFIX = "tel:";
-
- private static final int DETAILS_LOADER_QUERY_ID = 1;
- private static final int PHONE_LOADER_QUERY_ID = 2;
-
- private static final String KEY_URI = "uri";
-
- private static final String[] CONTACT_DETAILS_PROJECTION = {
- ContactsContract.Contacts._ID,
- ContactsContract.Contacts.DISPLAY_NAME,
- ContactsContract.Contacts.PHOTO_URI,
- ContactsContract.Contacts.HAS_PHONE_NUMBER
- };
-
- private PagedListView mListView;
- private List<RecyclerView.OnScrollListener> mOnScrollListeners = new ArrayList<>();
-
- public static ContactDetailsFragment newInstance(Uri uri,
- @Nullable RecyclerView.OnScrollListener listener) {
- ContactDetailsFragment fragment = new ContactDetailsFragment();
- if (listener != null) {
- fragment.addOnScrollListener(listener);
- }
-
- Bundle args = new Bundle();
- args.putParcelable(KEY_URI, uri);
- fragment.setArguments(args);
-
- return fragment;
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- return inflater.inflate(R.layout.contact_details, container, false);
- }
-
- @Override
- public void onViewCreated(View view, Bundle savedInstanceState) {
- mListView = view.findViewById(R.id.list_view);
- mListView.setDayNightStyle(DayNightStyle.ALWAYS_LIGHT);
-
- RecyclerView recyclerView = mListView.getRecyclerView();
- for (RecyclerView.OnScrollListener listener : mOnScrollListeners) {
- recyclerView.addOnScrollListener(listener);
- }
-
- mOnScrollListeners.clear();
- }
-
- @Override
- public void onActivityCreated(Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
- getLoaderManager().initLoader(DETAILS_LOADER_QUERY_ID, null, this);
- }
-
- /**
- * Adds a {@link androidx.recyclerview.widget.RecyclerView.OnScrollListener} to be notified when
- * the contact details are scrolled.
- *
- * @see RecyclerView#addOnScrollListener(RecyclerView.OnScrollListener)
- */
- public void addOnScrollListener(RecyclerView.OnScrollListener onScrollListener) {
- // If the view has not been created yet, then queue the setting of the scroll listener.
- if (mListView == null) {
- mOnScrollListeners.add(onScrollListener);
- return;
- }
-
- mListView.getRecyclerView().addOnScrollListener(onScrollListener);
- }
-
- @Override
- public void onDestroy() {
- // Clear all scroll listeners.
- mListView.getRecyclerView().removeOnScrollListener(null);
- super.onDestroy();
- }
-
- @Override
- public Loader<Cursor> onCreateLoader(int id, Bundle args) {
- if (vdebug()) {
- Log.d(TAG, "onCreateLoader id=" + id);
- }
-
- if (id != DETAILS_LOADER_QUERY_ID) {
- return null;
- }
-
- Uri contactUri = getArguments().getParcelable(KEY_URI);
- return new CursorLoader(getContext(), contactUri, CONTACT_DETAILS_PROJECTION,
- null /* selection */, null /* selectionArgs */, null /* sortOrder */);
- }
-
- @Override
- public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
- if (vdebug()) {
- Log.d(TAG, "onLoadFinished");
- }
-
- if (cursor.moveToFirst()) {
- mListView.setAdapter(new ContactDetailsAdapter(cursor));
- }
- }
-
- @Override
- public void onLoaderReset(Loader loader) {
- }
-
- private boolean vdebug() {
- return Log.isLoggable(TAG, Log.DEBUG);
- }
-
- private class ContactDetailViewHolder extends RecyclerView.ViewHolder {
- public View card;
- public ImageView leftIcon;
- public TextView title;
- public TextView text;
- public ImageView avatar;
- public View divier;
-
- public ContactDetailViewHolder(View v) {
- super(v);
- card = v.findViewById(R.id.card);
- leftIcon = v.findViewById(R.id.icon);
- title = v.findViewById(R.id.title);
- text = v.findViewById(R.id.text);
- avatar = v.findViewById(R.id.avatar);
- divier = v.findViewById(R.id.divider);
- }
- }
-
- private class ContactDetailsAdapter extends RecyclerView.Adapter<ContactDetailViewHolder>
- implements PagedListView.ItemCap {
-
- private static final int ID_HEADER = 1;
- private static final int ID_CONTENT = 2;
-
- private final String mContactName;
- @ColorInt
- private int mIconTint;
-
- private List<Pair<String, String>> mPhoneNumbers = new ArrayList<>();
-
- public ContactDetailsAdapter(Cursor cursor) {
- super();
-
- mIconTint = getContext().getColor(R.color.contact_details_icon_tint);
-
- int idColIdx = cursor.getColumnIndex(ContactsContract.Contacts._ID);
- String contactId = cursor.getString(idColIdx);
- int nameColIdx = cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME);
- mContactName = cursor.getString(nameColIdx);
- int hasPhoneColIdx = cursor.getColumnIndex(ContactsContract.Contacts.HAS_PHONE_NUMBER);
- boolean hasPhoneNumber = Integer.parseInt(cursor.getString(hasPhoneColIdx)) > 0;
-
- if (!hasPhoneNumber) {
- return;
- }
-
- // Fetch the phone number from the contacts db using another loader.
- LoaderManager.getInstance(ContactDetailsFragment.this).initLoader(PHONE_LOADER_QUERY_ID,
- null,
- new LoaderManager.LoaderCallbacks<Cursor>() {
- @Override
- public Loader<Cursor> onCreateLoader(int id, Bundle args) {
- return new CursorLoader(getContext(),
- ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
- null, /* All columns **/
- ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = ?",
- new String[]{contactId},
- null /* sortOrder */);
- }
-
- public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
- while (cursor.moveToNext()) {
- int typeColIdx = cursor.getColumnIndex(
- ContactsContract.CommonDataKinds.Phone.TYPE);
- int type = cursor.getInt(typeColIdx);
- int numberColIdx = cursor.getColumnIndex(
- ContactsContract.CommonDataKinds.Phone.NUMBER);
- String number = cursor.getString(numberColIdx);
- String numberType;
- switch (type) {
- case ContactsContract.CommonDataKinds.Phone.TYPE_HOME:
- numberType = getString(R.string.type_home);
- break;
- case ContactsContract.CommonDataKinds.Phone.TYPE_WORK:
- numberType = getString(R.string.type_work);
- break;
- case ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE:
- numberType = getString(R.string.type_mobile);
- break;
- default:
- numberType = getString(R.string.type_other);
- }
- mPhoneNumbers.add(new Pair<>(numberType,
- TelecomUtils.getFormattedNumber(getContext(), number)));
- notifyItemInserted(mPhoneNumbers.size());
- }
- notifyDataSetChanged();
- }
-
- public void onLoaderReset(Loader loader) {
- }
- });
- }
-
- @Override
- public int getItemViewType(int position) {
- return position == 0 ? ID_HEADER : ID_CONTENT;
- }
-
- @Override
- public void setMaxItems(int maxItems) {
- // Ignore.
- }
-
- @Override
- public int getItemCount() {
- return mPhoneNumbers.size() + 1; // +1 for the header row.
- }
-
- @Override
- public ContactDetailViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
- int layoutResId;
- switch (viewType) {
- case ID_HEADER:
- layoutResId = R.layout.contact_detail_name_image;
- break;
- case ID_CONTENT:
- layoutResId = R.layout.contact_details_number;
- break;
- default:
- Log.e(TAG, "Unknown view type " + viewType);
- return null;
- }
-
- View view = LayoutInflater.from(parent.getContext()).inflate(layoutResId, parent,
- false);
- return new ContactDetailViewHolder(view);
- }
-
- @Override
- public void onBindViewHolder(ContactDetailViewHolder viewHolder, int position) {
- switch (viewHolder.getItemViewType()) {
- case ID_HEADER:
- viewHolder.title.setText(mContactName);
- if (!mPhoneNumbers.isEmpty()) {
- String firstNumber = mPhoneNumbers.get(0).second;
- TelecomUtils.setContactBitmapAsync(getContext(), viewHolder.avatar,
- mContactName, firstNumber);
- }
- // Just in case a viewholder object gets recycled.
- viewHolder.card.setOnClickListener(null);
- break;
- case ID_CONTENT:
- Pair<String, String> data = mPhoneNumbers.get(position - 1);
- viewHolder.title.setText(data.second); // Type.
- viewHolder.text.setText(data.first); // Number.
- viewHolder.leftIcon.setImageResource(R.drawable.ic_phone);
- viewHolder.leftIcon.setColorFilter(mIconTint);
- viewHolder.card.setOnClickListener(v -> {
- Intent callIntent = new Intent(Intent.ACTION_CALL);
- callIntent.setData(Uri.parse(TELEPHONE_URI_PREFIX + data.second));
- getContext().startActivity(callIntent);
- });
- break;
- default:
- Log.e(TAG, "Unknown view type " + viewHolder.getItemViewType());
- return;
- }
-
- if (position == (getItemCount() - 1)) {
- // hide divider for last item.
- viewHolder.divier.setVisibility(View.GONE);
- } else {
- viewHolder.divier.setVisibility(View.VISIBLE);
- }
- ListItemBackgroundResolver.setBackground(viewHolder.card,
- viewHolder.getAdapterPosition(), getItemCount());
- }
- }
-}
diff --git a/src/com/android/car/dialer/ContactEntry.java b/src/com/android/car/dialer/ContactEntry.java
deleted file mode 100644
index aa3d337e..00000000
--- a/src/com/android/car/dialer/ContactEntry.java
+++ /dev/null
@@ -1,274 +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.car.dialer;
-
-import android.content.Context;
-import android.database.Cursor;
-import android.provider.ContactsContract;
-import android.text.TextUtils;
-
-import com.android.car.dialer.telecom.PhoneLoader;
-import com.android.car.dialer.telecom.TelecomUtils;
-
-import androidx.annotation.Nullable;
-
-/**
- * Encapsulates data about a phone Contact entry. Typically loaded from the local Contact store.
- */
-// TODO: Refactor to use Builder design pattern.
-public class ContactEntry implements Comparable<ContactEntry> {
- private final Context mContext;
-
- /**
- * An unique primary key for searching an entry.
- */
- private int mId;
-
- /**
- * Whether this contact entry is starred by user.
- */
- private boolean mIsStarred;
-
- /**
- * Contact-specific information about whether or not a contact has been pinned by the user at
- * a particular position within the system contact application's user interface.
- */
- private int mPinnedPosition;
-
- /**
- * Phone number.
- */
- private String mNumber;
-
- /**
- * The display name.
- */
- @Nullable
- private String mDisplayName;
-
- /**
- * A URI that can be used to retrieve a thumbnail of the contact's photo.
- */
- private String mAvatarThumbnailUri;
-
- /**
- * A URI that can be used to retrieve the contact's full-size photo.
- */
- private String mAvatarUri;
-
- /**
- * An opaque value that contains hints on how to find the contact if its row id changed
- * as a result of a sync or aggregation
- */
- private String mLookupKey;
-
- /**
- * The type of data, for example Home or Work.
- */
- private int mType;
-
- /**
- * The user defined label for the the contact method.
- */
- private String mLabel;
-
- /**
- * Parses a Contact entry for a Cursor loaded from the OS Strequents DB.
- */
- public static ContactEntry fromCursor(Cursor cursor, Context context) {
- int idColumnIndex = PhoneLoader.getIdColumnIndex(cursor);
- int starredColumn = cursor.getColumnIndex(ContactsContract.Contacts.STARRED);
- int pinnedColumn = cursor.getColumnIndex("pinned");
- int displayNameColumnIndex = cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME);
- int avatarUriColumnIndex = cursor.getColumnIndex(ContactsContract.Contacts.PHOTO_URI);
- int avatarThumbnailColumnIndex = cursor.getColumnIndex(
- ContactsContract.Contacts.PHOTO_THUMBNAIL_URI);
- int lookupKeyColumnIndex = cursor.getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY);
- int typeColumnIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DATA2);
- int labelColumnIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DATA3);
-
- String name = cursor.getString(displayNameColumnIndex);
- String number = PhoneLoader.getPhoneNumber(cursor, context.getContentResolver());
- int starred = cursor.getInt(starredColumn);
- int pinnedPosition = cursor.getInt(pinnedColumn);
- ContactEntry contactEntry = new ContactEntry(context, name, number, starred > 0,
- pinnedPosition);
- contactEntry.setId(cursor.getInt(idColumnIndex));
- contactEntry.setAvatarUri(cursor.getString(avatarUriColumnIndex));
- contactEntry.setAvatarThumbnailUri(cursor.getString(avatarThumbnailColumnIndex));
- contactEntry.setLookupKey(cursor.getString(lookupKeyColumnIndex));
- contactEntry.setType(cursor.getInt(typeColumnIndex));
- contactEntry.setLabel(cursor.getString(labelColumnIndex));
- return contactEntry;
- }
-
- public ContactEntry(
- Context context, String name, String number, boolean isStarred, int pinnedPosition) {
- mContext = context;
- this.mDisplayName = name;
- this.mNumber = number;
- this.mIsStarred = isStarred;
- this.mPinnedPosition = pinnedPosition;
- }
-
- /**
- * Retrieves a best-effort contact name ready for display to the user.
- * It takes into account the number associated with a name for fail cases.
- */
- public String getDisplayName() {
- if (!TextUtils.isEmpty(mDisplayName)) {
- return mDisplayName;
- }
- if (isVoicemail()) {
- return mContext.getResources().getString(R.string.voicemail);
- } else {
- String displayName = TelecomUtils.getFormattedNumber(mContext, mNumber);
- if (TextUtils.isEmpty(displayName)) {
- displayName = mContext.getString(R.string.unknown);
- }
- return displayName;
- }
- }
-
- public boolean isVoicemail() {
- return mNumber.equals(TelecomUtils.getVoicemailNumber(mContext));
- }
-
- @Override
- public int compareTo(ContactEntry strequentContactEntry) {
- if (mIsStarred == strequentContactEntry.mIsStarred) {
- if (mPinnedPosition == strequentContactEntry.mPinnedPosition) {
- if (mDisplayName == strequentContactEntry.mDisplayName) {
- return compare(mNumber, strequentContactEntry.mNumber);
- }
- return compare(mDisplayName, strequentContactEntry.mDisplayName);
- } else {
- if (mPinnedPosition > 0 && strequentContactEntry.mPinnedPosition > 0) {
- return mPinnedPosition - strequentContactEntry.mPinnedPosition;
- }
-
- if (mPinnedPosition > 0) {
- return -1;
- }
-
- return 1;
- }
- }
-
- if (mIsStarred) {
- return -1;
- }
-
- return 1;
- }
-
- @Override
- public boolean equals(Object obj) {
- if (obj instanceof ContactEntry) {
- ContactEntry other = (ContactEntry) obj;
- if (compare(mDisplayName, other.mDisplayName) == 0
- && compare(mNumber, other.mNumber) == 0
- && mIsStarred == other.mIsStarred
- && mPinnedPosition == other.mPinnedPosition) {
- return true;
- }
- }
- return false;
- }
-
- @Override
- public int hashCode() {
- int result = 17;
- result = 31 * result + (mIsStarred ? 1 : 0);
- result = 31 * result + mPinnedPosition;
- result = 31 * result + (mDisplayName == null ? 0 : mDisplayName.hashCode());
- result = 31 * result + (mNumber == null ? 0 : mNumber.hashCode());
- return result;
- }
-
- private int compare(final String one, final String two) {
- if (one == null ^ two == null) {
- return (one == null) ? -1 : 1;
- }
-
- if (one == null && two == null) {
- return 0;
- }
-
- return one.compareTo(two);
- }
-
- public int getId() {
- return mId;
- }
-
- private void setId(int id) {
- mId = id;
- }
-
- public String getAvatarUri() {
- return mAvatarUri;
- }
-
- private void setAvatarUri(String avatarUri) {
- mAvatarUri = avatarUri;
- }
-
- public String getLookupKey() {
- return mLookupKey;
- }
-
- private void setLookupKey(String lookupKey) {
- mLookupKey = lookupKey;
- }
-
- public int getType() {
- return mType;
- }
-
- private void setType(int type) {
- mType = type;
- }
-
- public String getLabel() {
- return mLabel;
- }
-
- private void setLabel(String label) {
- mLabel = label;
- }
-
- public String getAvatarThumbnailUri() {
- return mAvatarThumbnailUri;
- }
-
- private void setAvatarThumbnailUri(String avatarThumbnailUri) {
- mAvatarThumbnailUri = avatarThumbnailUri;
- }
-
- public String getNumber() {
- return mNumber;
- }
-
- public boolean isStarred() {
- return mIsStarred;
- }
-
- public int getPinnedPosition() {
- return mPinnedPosition;
- }
-
-}
diff --git a/src/com/android/car/dialer/ContactResultViewHolder.java b/src/com/android/car/dialer/ContactResultViewHolder.java
deleted file mode 100644
index 8b78cbae..00000000
--- a/src/com/android/car/dialer/ContactResultViewHolder.java
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- * 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.car.dialer;
-
-import android.content.Context;
-import android.content.Intent;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.net.Uri;
-import android.view.View;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import com.android.car.apps.common.LetterTileDrawable;
-import com.android.car.dialer.ui.CircleBitmapDrawable;
-
-import java.io.FileNotFoundException;
-import java.io.InputStream;
-
-import androidx.annotation.Nullable;
-import androidx.car.utils.ListItemBackgroundResolver;
-import androidx.recyclerview.widget.RecyclerView;
-
-/**
- * A {@link androidx.recyclerview.widget.RecyclerView.ViewHolder} that will parse relevant
- * views out of a {@code contact_result} layout.
- */
-public class ContactResultViewHolder extends RecyclerView.ViewHolder {
- private final Context mContext;
- private final View mContactCard;
- private final TextView mContactName;
- private final ImageView mContactPicture;
-
- public ContactResultViewHolder(View view) {
- super(view);
- mContext = view.getContext();
- mContactCard = view.findViewById(R.id.contact_result_card);
- mContactName = view.findViewById(R.id.contact_name);
- mContactPicture = view.findViewById(R.id.contact_picture);
- }
-
- /**
- * Populates the view that is represented by this ViewHolder with the information in the
- * provided {@link ContactDetails}.
- */
- public void bind(ContactDetails details, int itemCount) {
- ListItemBackgroundResolver.setBackground(mContactCard, getAdapterPosition(), itemCount);
-
- mContactCard.setOnClickListener(v -> {
- Intent intent = new Intent();
- intent.setAction(TelecomIntents.ACTION_SHOW_CONTACT_DETAILS);
- intent.putExtra(TelecomIntents.CONTACT_LOOKUP_URI_EXTRA, details.lookupUri.toString());
- mContext.startActivity(intent);
- });
-
- mContactName.setText(details.displayName);
-
- if (details.photoUri == null) {
- setLetterDrawableForContact(details);
- return;
- }
-
- Bitmap bitmap = getContactBitmapFromUri(mContext, details.photoUri);
- if (bitmap == null) {
- setLetterDrawableForContact(details);
- } else {
- mContactPicture.setScaleType(ImageView.ScaleType.CENTER_CROP);
- mContactPicture.setImageDrawable(
- new CircleBitmapDrawable(mContext.getResources(), bitmap));
- }
- }
-
- /**
- * Sets the contact picture to be a rounded, colored circle that has the first letter of the
- * contact's name in it.
- */
- private void setLetterDrawableForContact(ContactDetails details) {
- mContactPicture.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
- LetterTileDrawable letterTileDrawable = new LetterTileDrawable(mContext.getResources());
- letterTileDrawable.setContactDetails(details.displayName, details.displayName);
- letterTileDrawable.setIsCircular(true);
- mContactPicture.setImageDrawable(letterTileDrawable);
- }
-
- /**
- * Retrieves the picture that is specified by the given {@link Uri}.
- */
- @Nullable
- private static Bitmap getContactBitmapFromUri(Context context, Uri uri) {
- try {
- InputStream input = context.getContentResolver().openInputStream(uri);
- return input == null ? null : BitmapFactory.decodeStream(input);
- } catch (FileNotFoundException e) {
- return null;
- }
- }
-
- /**
- * A struct that holds the details for a contact row.
- */
- public static class ContactDetails {
- public final String displayName;
- public final Uri photoUri;
- public final Uri lookupUri;
-
- public ContactDetails(String displayName, String photoUri, Uri lookupUri) {
- this.displayName = displayName;
- this.photoUri = photoUri == null ? null : Uri.parse(photoUri);
- this.lookupUri = lookupUri;
- }
- }
-}
diff --git a/src/com/android/car/dialer/ContactResultsFragment.java b/src/com/android/car/dialer/ContactResultsFragment.java
deleted file mode 100644
index 03728ada..00000000
--- a/src/com/android/car/dialer/ContactResultsFragment.java
+++ /dev/null
@@ -1,196 +0,0 @@
-/*
- * 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.car.dialer;
-
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.Bundle;
-import android.provider.ContactsContract.Contacts;
-import android.text.TextUtils;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import androidx.annotation.Nullable;
-import androidx.car.widget.DayNightStyle;
-import androidx.car.widget.PagedListView;
-import androidx.fragment.app.Fragment;
-import androidx.loader.app.LoaderManager;
-import androidx.loader.content.CursorLoader;
-import androidx.loader.content.Loader;
-import androidx.recyclerview.widget.RecyclerView;
-
-/**
- * A fragment that will take a search query, look up contacts that match and display those
- * results as a list.
- */
-public class ContactResultsFragment extends Fragment implements
- LoaderManager.LoaderCallbacks<Cursor> {
- private static final String TAG = "ContactResultsFragment";
-
- private static final String KEY_INITIAL_SEARCH_QUERY = "initial_search_query";
-
- private static final String[] CONTACT_DETAILS_PROJECTION = {
- Contacts._ID,
- Contacts.LOOKUP_KEY,
- Contacts.DISPLAY_NAME,
- Contacts.PHOTO_URI
- };
-
- private final ContactResultsAdapter mAdapter = new ContactResultsAdapter();
- private PagedListView mContactResultList;
- private String mSearchQuery;
-
- private List<RecyclerView.OnScrollListener> mOnScrollListeners = new ArrayList<>();
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- Bundle args = getArguments();
- if (args != null) {
- setSearchQuery(args.getString(KEY_INITIAL_SEARCH_QUERY));
- }
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- return inflater.inflate(R.layout.contact_result_fragment, container, false);
- }
-
- @Override
- public void onViewCreated(View view, Bundle savedInstanceState) {
- mContactResultList = view.findViewById(R.id.contact_result_list);
- mContactResultList.setDayNightStyle(DayNightStyle.ALWAYS_LIGHT);
- mContactResultList.setAdapter(mAdapter);
-
- RecyclerView recyclerView = mContactResultList.getRecyclerView();
- for (RecyclerView.OnScrollListener listener : mOnScrollListeners) {
- recyclerView.addOnScrollListener(listener);
- }
-
- mOnScrollListeners.clear();
- }
-
- /**
- * Adds a {@link androidx.recyclerview.widget.RecyclerView.OnScrollListener} to be notified when
- * the contact list is scrolled.
- *
- * @see RecyclerView#addOnScrollListener(RecyclerView.OnScrollListener)
- */
- public void addOnScrollListener(RecyclerView.OnScrollListener onScrollListener) {
- // If the view has not been created yet, then queue the setting of the scroll listener.
- if (mContactResultList == null) {
- mOnScrollListeners.add(onScrollListener);
- return;
- }
-
- mContactResultList.getRecyclerView().addOnScrollListener(onScrollListener);
- }
-
- /**
- * Clears any results from a previous query and displays an empty list.
- */
- public void clearResults() {
- mSearchQuery = null;
- mAdapter.clear();
- }
-
- /**
- * Sets the search query that should be used to filter contacts.
- */
- public void setSearchQuery(String query) {
- mSearchQuery = query;
-
- if (!TextUtils.isEmpty(mSearchQuery)) {
- // Calling restartLoader so that the loader is always re-created with the new
- // search query.
- LoaderManager.getInstance(this).restartLoader(0, null /* args */, this /* callback */);
- }
- }
-
- @Override
- public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "onLoadFinished(); count: " + data.getCount());
- }
-
- mAdapter.setData(data);
- data.close();
- }
-
- /**
- * Finds the contacts with any field that matches the search query. Typically, the search
- * criteria appears to be matching the beginning of the value in that data field (name, phone
- * number, etc.)
- */
- @Override
- public Loader<Cursor> onCreateLoader(int loaderId, Bundle args) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "onCreateLoader(); loaderId: " + loaderId + " with query: " + mSearchQuery);
- }
-
- /* To lookup against all fields, just append the search query to the content filter uri
- * and perform a lookup without any selection
- */
- Uri lookupUri = Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI,
- Uri.encode(mSearchQuery));
-
- return new CursorLoader(getContext(), lookupUri,
- CONTACT_DETAILS_PROJECTION, null /* selection */,
- null /* selectionArgs */, null /* sortOrder */);
- }
-
- @Override
- public void onLoaderReset(Loader<Cursor> loader) {
- }
-
- @Override
- public void onDestroy() {
- // Clear all scroll listeners.
- mContactResultList.getRecyclerView().removeOnScrollListener(null);
- super.onDestroy();
- }
-
- /**
- * Creates a new instance of the {@link ContactResultsFragment}.
- *
- * @param listener A scroll listener that will be notified when the list of search
- * results has
- * been scrolled.
- * @param initialSearchQuery An optional search query that will be inputted when the fragment
- * starts up.
- */
- public static ContactResultsFragment newInstance(RecyclerView.OnScrollListener listener,
- @Nullable String initialSearchQuery) {
- ContactResultsFragment fragment = new ContactResultsFragment();
- fragment.addOnScrollListener(listener);
-
- if (!TextUtils.isEmpty(initialSearchQuery)) {
- Bundle args = new Bundle();
- args.putString(KEY_INITIAL_SEARCH_QUERY, initialSearchQuery);
- fragment.setArguments(args);
- }
-
- return fragment;
- }
-}
diff --git a/src/com/android/car/dialer/ContactSearchActivity.java b/src/com/android/car/dialer/ContactSearchActivity.java
deleted file mode 100644
index 4dcc8776..00000000
--- a/src/com/android/car/dialer/ContactSearchActivity.java
+++ /dev/null
@@ -1,274 +0,0 @@
-/*
- * 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.car.dialer;
-
-import android.animation.ValueAnimator;
-import android.app.Activity;
-import android.app.SearchManager;
-import android.content.Intent;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.Handler;
-import android.text.Editable;
-import android.text.TextWatcher;
-import android.view.View;
-import android.view.inputmethod.InputMethodManager;
-import android.widget.EditText;
-
-import androidx.annotation.Nullable;
-import androidx.fragment.app.Fragment;
-import androidx.fragment.app.FragmentActivity;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-
-/**
- * An activity that manages contact searching. This activity will display the result of a search
- * as well as show the details of a contact when that contact is clicked.
- */
-public class ContactSearchActivity extends FragmentActivity {
- private static final String CONTENT_FRAGMENT_TAG = "CONTENT_FRAGMENT_TAG";
- private static final int ANIMATION_DURATION_MS = 100;
-
- /**
- * A delay before actually starting a contact search. This ensures that there are not too many
- * queries happening when the user is still typing.
- */
- private static final int CONTACT_SEARCH_DELAY = 400;
-
- private final Handler mHandler = new Handler();
- private Runnable mCurrentSearch;
-
- private View mSearchContainer;
- private EditText mSearchField;
-
- private float mContainerElevation;
- private ValueAnimator mRemoveElevationAnimator;
-
- /**
- * Whether or not it is safe to make transactions on the {@link android.app.FragmentManager}.
- * This variable prevents a possible exception when calling commit() on the FragmentManager.
- *
- * <p>The default value is {@code true} because it is only after
- * {@link #onSaveInstanceState(Bundle)} that fragment commits are not allowed.
- */
- private boolean mAllowFragmentCommits = true;
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.contact_search_activity);
-
- mSearchContainer = findViewById(R.id.search_container);
- mSearchField = findViewById(R.id.search_field);
-
- mSearchField.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) {
- if (!(getCurrentFragment() instanceof ContactResultsFragment)) {
- showContactResultList(s.toString());
- return;
- }
-
- // Cancel any pending searches.
- if (mCurrentSearch != null) {
- mHandler.removeCallbacks(mCurrentSearch);
- }
-
- // Queue up a new search. This will be cancelled if the user types within the
- // time frame specified by CONTACT_SEARCH_DELAY.
- mCurrentSearch = new SearchRunnable(s.toString());
- mHandler.postDelayed(mCurrentSearch, CONTACT_SEARCH_DELAY);
- }
- });
-
- mContainerElevation = getResources()
- .getDimension(R.dimen.search_container_elevation);
-
- mRemoveElevationAnimator = ValueAnimator.ofFloat(mContainerElevation, 0.f);
- mRemoveElevationAnimator
- .setDuration(ANIMATION_DURATION_MS)
- .addUpdateListener(animation -> mSearchContainer.setElevation(
- (float) animation.getAnimatedValue()));
-
- findViewById(R.id.back).setOnClickListener(v -> finish());
- findViewById(R.id.clear).setOnClickListener(v -> {
- mSearchField.getText().clear();
-
- Fragment currentFragment = getCurrentFragment();
- if (currentFragment instanceof ContactResultsFragment) {
- ((ContactResultsFragment) currentFragment).clearResults();
- }
- });
-
- handleIntent(getIntent());
- }
-
- @Override
- protected void onNewIntent(Intent intent) {
- setIntent(intent);
- handleIntent(intent);
- }
-
- /**
- * Inspects the Action within the given intent and loads up the appropriate fragment based on
- * this.
- */
- private void handleIntent(Intent intent) {
- if (intent == null || intent.getAction() == null) {
- showContactResultList(null /* query */);
- return;
- }
-
- switch (intent.getAction()) {
- case Intent.ACTION_SEARCH:
- showContactResultList(intent.getStringExtra(SearchManager.QUERY));
- break;
-
- case TelecomIntents.ACTION_SHOW_CONTACT_DETAILS:
- // Hide the keyboard so there's room on the screen for the detail view.
- InputMethodManager imm =
- (InputMethodManager) getSystemService(Activity.INPUT_METHOD_SERVICE);
- imm.hideSoftInputFromWindow(mSearchField.getWindowToken(), 0);
- Uri contactUri = Uri.parse(intent.getStringExtra(
- TelecomIntents.CONTACT_LOOKUP_URI_EXTRA));
- setContentFragment(ContactDetailsFragment.newInstance(contactUri,
- new ContactScrollListener()));
- break;
-
- default:
- showContactResultList(null /* query */);
- }
- }
-
- /**
- * Displays the fragment that will show the results of a search. The given query is used as
- * the initial search to populate the list.
- */
- private void showContactResultList(@Nullable String query) {
- // Check that the result list is not already being displayed. If it is, then simply set the
- // search query.
- Fragment currentFragment = getCurrentFragment();
- if (currentFragment instanceof ContactResultsFragment) {
- ((ContactResultsFragment) currentFragment).setSearchQuery(query);
- return;
- }
-
- setContentFragment(ContactResultsFragment.newInstance(new ContactScrollListener(), query));
- }
-
- /**
- * Sets the fragment that will be shown as the main content of this Activity.
- */
- private void setContentFragment(Fragment fragment) {
- if (!mAllowFragmentCommits) {
- return;
- }
-
- // The search panel might have elevation added to it, so remove it when the fragment
- // changes since any lists in it will be reset to the top.
- resetSearchPanelElevation();
-
- getSupportFragmentManager().beginTransaction()
- .setCustomAnimations(R.animator.fade_in, R.animator.fade_out)
- .replace(R.id.content_fragment_container, fragment, CONTENT_FRAGMENT_TAG)
- .commitNow();
- }
-
- /**
- * Returns the fragment that is currently being displayed as the content view.
- */
- @Nullable
- private Fragment getCurrentFragment() {
- return getSupportFragmentManager().findFragmentByTag(CONTENT_FRAGMENT_TAG);
- }
-
- @Override
- protected void onStart() {
- super.onStart();
- // Fragment commits are not allowed once the Activity's state has been saved. Once
- // onStart() has been called, the FragmentManager should now allow commits.
- mAllowFragmentCommits = true;
- }
-
- @Override
- public void onSaveInstanceState(Bundle outState) {
- // A transaction can only be committed with this method prior to its containing activity
- // saving its state.
- mAllowFragmentCommits = false;
- super.onSaveInstanceState(outState);
- }
-
- /**
- * Checks if {@link #mSearchContainer} has an elevation set on it and if it does, animates the
- * removal of this elevation.
- */
- private void resetSearchPanelElevation() {
- if (mSearchContainer.getElevation() != 0.f) {
- mRemoveElevationAnimator.start();
- }
- }
-
- /**
- * A {@link Runnable} that will execute a contact search with the given {@link #mSearchQuery}.
- */
- private class SearchRunnable implements Runnable {
- private final String mSearchQuery;
-
- public SearchRunnable(String searchQuery) {
- mSearchQuery = searchQuery;
- }
-
- @Override
- public void run() {
- Fragment currentFragment = getCurrentFragment();
- if (currentFragment instanceof ContactResultsFragment) {
- ((ContactResultsFragment) currentFragment).setSearchQuery(mSearchQuery);
- }
- }
- }
-
- /**
- * Listener for scrolls in a fragment that has a list. It will will add elevation on the
- * container holding the search field. This elevation will give the illusion of the list
- * scrolling under that container.
- */
- public class ContactScrollListener extends RecyclerView.OnScrollListener {
- @Override
- public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
- // The default LayoutManager for PagedListView is a LinearLayoutManager. Dialer does
- // not change this.
- LinearLayoutManager layoutManager =
- (LinearLayoutManager) recyclerView.getLayoutManager();
-
- if (layoutManager.findFirstCompletelyVisibleItemPosition() == 0) {
- resetSearchPanelElevation();
- } else {
- // No animation needed when adding the elevation because the scroll masks the adding
- // of the elevation.
- mSearchContainer.setElevation(mContainerElevation);
- }
- }
- }
-}
diff --git a/src/com/android/car/dialer/DialerApplication.java b/src/com/android/car/dialer/DialerApplication.java
new file mode 100644
index 00000000..ef7e0d90
--- /dev/null
+++ b/src/com/android/car/dialer/DialerApplication.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer;
+
+import android.app.Application;
+
+import com.android.car.dialer.notification.InCallNotificationController;
+import com.android.car.dialer.notification.MissedCallNotificationController;
+import com.android.car.dialer.telecom.UiBluetoothMonitor;
+import com.android.car.dialer.telecom.UiCallManager;
+import com.android.car.telephony.common.InMemoryPhoneBook;
+
+public class DialerApplication extends Application {
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ InMemoryPhoneBook.init(this);
+ UiCallManager.init(this);
+ UiBluetoothMonitor.init(this);
+ InCallNotificationController.init(this);
+ MissedCallNotificationController.init(this);
+ }
+}
diff --git a/src/com/android/car/dialer/DialerFragment.java b/src/com/android/car/dialer/DialerFragment.java
deleted file mode 100644
index f0b416d5..00000000
--- a/src/com/android/car/dialer/DialerFragment.java
+++ /dev/null
@@ -1,93 +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.car.dialer;
-
-import android.os.Bundle;
-import android.text.TextUtils;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import com.android.car.dialer.log.L;
-import com.android.car.dialer.telecom.UiCallManager;
-import com.android.car.dialer.ui.DialerInfoController;
-import com.android.car.dialer.ui.DialpadFragment;
-
-import androidx.annotation.Nullable;
-import androidx.fragment.app.Fragment;
-
-/**
- * Fragment that controls the dialpad.
- */
-public class DialerFragment extends Fragment implements DialpadFragment.DialpadCallback {
- private static final String TAG = "Em.DialerFragment";
-
- private static final String DIAL_NUMBER_KEY = "DIAL_NUMBER_KEY";
- private static final String PLUS_DIGIT = "+";
-
- private DialerInfoController mDialerInfoController;
- private L logger = L.logger(TAG);
-
- /**
- * Creates a new instance of the {@link DialerFragment} and display the given number as the one
- * to dial.
- */
- static DialerFragment newInstance(@Nullable String dialNumber) {
- DialerFragment fragment = new DialerFragment();
-
- if (!TextUtils.isEmpty(dialNumber)) {
- Bundle args = new Bundle();
- args.putString(DIAL_NUMBER_KEY, dialNumber);
- fragment.setArguments(args);
- }
-
- return fragment;
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- logger.d("onCreateView");
- View view = inflater.inflate(R.layout.dialer_fragment, container, false);
-
- Fragment dialpadFragment = DialpadFragment.newInstance();
- getChildFragmentManager().beginTransaction()
- .replace(R.id.dialpad_fragment_container, dialpadFragment)
- .commit();
-
- View dialerInfoContainer = view.findViewById(R.id.dialer_info_fragment_container);
- mDialerInfoController = new DialerInfoController(getContext(), dialerInfoContainer);
-
- if (getArguments() != null) {
- mDialerInfoController.appendDialedNumber(getArguments().getString(DIAL_NUMBER_KEY));
- }
-
- return view;
- }
-
- @Override
- public void onDialVoiceMail() {
- UiCallManager.get().callVoicemail();
- }
-
- @Override
- public void onAppendDigit(String digit) {
- if (PLUS_DIGIT.equals(digit)) {
- mDialerInfoController.removeLastDigit();
- }
- mDialerInfoController.appendDialedNumber(digit);
- }
-}
diff --git a/src/com/android/car/dialer/OngoingCallFragment.java b/src/com/android/car/dialer/OngoingCallFragment.java
deleted file mode 100644
index 6a1ce28b..00000000
--- a/src/com/android/car/dialer/OngoingCallFragment.java
+++ /dev/null
@@ -1,667 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.car.dialer;
-
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Color;
-import android.os.Bundle;
-import android.os.Handler;
-import android.telecom.Call;
-import android.telecom.CallAudioState;
-import android.text.TextUtils;
-import android.text.format.DateUtils;
-import android.util.Log;
-import android.util.SparseArray;
-import android.view.KeyEvent;
-import android.view.LayoutInflater;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.animation.AccelerateDecelerateInterpolator;
-import android.view.animation.Animation;
-import android.view.animation.Transformation;
-import android.widget.ImageButton;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import com.android.car.apps.common.FabDrawable;
-import com.android.car.dialer.telecom.TelecomUtils;
-import com.android.car.dialer.telecom.UiCall;
-import com.android.car.dialer.telecom.UiCallManager;
-import com.android.car.dialer.ui.CircleBitmapDrawable;
-
-import java.util.Arrays;
-import java.util.List;
-import java.util.Objects;
-
-import androidx.fragment.app.Fragment;
-
-/**
- * A fragment that displays information about an on-going call with options to hang up.
- */
-@Deprecated
-public class OngoingCallFragment extends Fragment implements CallListener {
- private static final String TAG = "OngoingCall";
- private static final SparseArray<Character> mDialpadButtonMap = new SparseArray<>();
-
- static {
- mDialpadButtonMap.put(R.id.one, '1');
- mDialpadButtonMap.put(R.id.two, '2');
- mDialpadButtonMap.put(R.id.three, '3');
- mDialpadButtonMap.put(R.id.four, '4');
- mDialpadButtonMap.put(R.id.five, '5');
- mDialpadButtonMap.put(R.id.six, '6');
- mDialpadButtonMap.put(R.id.seven, '7');
- mDialpadButtonMap.put(R.id.eight, '8');
- mDialpadButtonMap.put(R.id.nine, '9');
- mDialpadButtonMap.put(R.id.zero, '0');
- mDialpadButtonMap.put(R.id.star, '*');
- mDialpadButtonMap.put(R.id.pound, '#');
- }
-
- private final Handler mHandler = new Handler();
-
- private UiCall mLastRemovedCall;
- private UiCallManager mUiCallManager;
- private View mRingingCallControls;
- private View mActiveCallControls;
- private ImageButton mEndCallButton;
- private ImageButton mUnholdCallButton;
- private ImageButton mMuteButton;
- private ImageButton mToggleDialpadButton;
- private ImageButton mSwapButton;
- private ImageButton mMergeButton;
- private ImageButton mAnswerCallButton;
- private ImageButton mRejectCallButton;
- private TextView mNameTextView;
- private TextView mSecondaryNameTextView;
- private TextView mStateTextView;
- private TextView mSecondaryStateTextView;
- private ImageView mLargeContactPhotoView;
- private ImageView mSmallContactPhotoView;
- private View mDialpadContainer;
- private View mSecondaryCallContainer;
- private View mSecondaryCallControls;
- private String mLoadedNumber;
- private CharSequence mCallInfoLabel;
- private UiBluetoothMonitor mUiBluetoothMonitor;
-
- static OngoingCallFragment newInstance(UiCallManager callManager,
- UiBluetoothMonitor btMonitor) {
- OngoingCallFragment fragment = new OngoingCallFragment();
- fragment.mUiCallManager = callManager;
- fragment.mUiBluetoothMonitor = btMonitor;
- return fragment;
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- mHandler.removeCallbacks(mUpdateDurationRunnable);
- mHandler.removeCallbacks(mStopDtmfToneRunnable);
- mLoadedNumber = null;
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- View view = inflater.inflate(R.layout.ongoing_call, container, false);
- initializeViews(view);
- initializeClickListeners();
-
- List<View> dialpadViews = Arrays.asList(
- mDialpadContainer.findViewById(R.id.one),
- mDialpadContainer.findViewById(R.id.two),
- mDialpadContainer.findViewById(R.id.three),
- mDialpadContainer.findViewById(R.id.four),
- mDialpadContainer.findViewById(R.id.five),
- mDialpadContainer.findViewById(R.id.six),
- mDialpadContainer.findViewById(R.id.seven),
- mDialpadContainer.findViewById(R.id.eight),
- mDialpadContainer.findViewById(R.id.nine),
- mDialpadContainer.findViewById(R.id.zero),
- mDialpadContainer.findViewById(R.id.pound),
- mDialpadContainer.findViewById(R.id.star));
-
- // In touch screen, we need to adjust the InCall card for the narrow screen to show the
- // full dial pad.
- for (View dialpadView : dialpadViews) {
- dialpadView.setOnTouchListener(mDialpadTouchListener);
- dialpadView.setOnKeyListener(mDialpadKeyListener);
- }
-
- updateCalls();
-
- return view;
- }
-
- private void initializeViews(View parent) {
- mRingingCallControls = parent.findViewById(R.id.ringing_call_controls);
- mActiveCallControls = parent.findViewById(R.id.active_call_controls);
- mEndCallButton = parent.findViewById(R.id.end_call);
- mUnholdCallButton = parent.findViewById(R.id.unhold_call);
- mMuteButton = parent.findViewById(R.id.mute);
- mToggleDialpadButton = parent.findViewById(R.id.toggle_dialpad);
- mDialpadContainer = parent.findViewById(R.id.dialpad_container);
- mNameTextView = parent.findViewById(R.id.name);
- mSecondaryNameTextView = parent.findViewById(R.id.name_secondary);
- mStateTextView = parent.findViewById(R.id.info);
- mSecondaryStateTextView = parent.findViewById(R.id.info_secondary);
- mLargeContactPhotoView = parent.findViewById(R.id.large_contact_photo);
- mSmallContactPhotoView = parent.findViewById(R.id.small_contact_photo);
- mSecondaryCallContainer = parent.findViewById(R.id.secondary_call_container);
- mSecondaryCallControls = parent.findViewById(R.id.secondary_call_controls);
- mSwapButton = parent.findViewById(R.id.swap);
- mMergeButton = parent.findViewById(R.id.merge);
- mAnswerCallButton = parent.findViewById(R.id.answer_call_button);
- mRejectCallButton = parent.findViewById(R.id.reject_call_button);
-
- Context context = getContext();
- FabDrawable drawable = new FabDrawable(context);
- drawable.setFabAndStrokeColor(context.getColor(R.color.phone_call));
- mAnswerCallButton.setBackground(drawable);
-
- drawable = new FabDrawable(context);
- drawable.setFabAndStrokeColor(context.getColor(R.color.phone_end_call));
- mEndCallButton.setBackground(drawable);
-
- drawable = new FabDrawable(context);
- drawable.setFabAndStrokeColor(context.getColor(R.color.phone_call));
- mUnholdCallButton.setBackground(drawable);
- }
-
- private void initializeClickListeners() {
- mAnswerCallButton.setOnClickListener((unusedView) -> {
- UiCall call = mUiCallManager.getCallWithState(Call.STATE_RINGING);
- if (call == null) {
- Log.w(TAG, "There is no incoming call to answer.");
- return;
- }
- mUiCallManager.answerCall(call);
- });
-
- mRejectCallButton.setOnClickListener((unusedView) -> {
- UiCall call = mUiCallManager.getCallWithState(Call.STATE_RINGING);
- if (call == null) {
- Log.w(TAG, "There is no incoming call to reject.");
- return;
- }
- mUiCallManager.rejectCall(call, false, null);
- });
-
- mEndCallButton.setOnClickListener((unusedView) -> {
- UiCall call = mUiCallManager.getPrimaryCall();
- if (call == null) {
- Log.w(TAG, "There is no active call to end.");
- return;
- }
- mUiCallManager.disconnectCall(call);
- });
-
- mUnholdCallButton.setOnClickListener((unusedView) -> {
- UiCall call = mUiCallManager.getPrimaryCall();
- if (call == null) {
- Log.w(TAG, "There is no active call to unhold.");
- return;
- }
- mUiCallManager.unholdCall(call);
- });
-
- mMuteButton.setOnClickListener(
- (unusedView) -> mUiCallManager.setMuted(!mUiCallManager.getMuted()));
-
- mSwapButton.setOnClickListener((unusedView) -> {
- UiCall call = mUiCallManager.getPrimaryCall();
- if (call == null) {
- Log.w(TAG, "There is no active call to hold.");
- return;
- }
- if (call.getState() == Call.STATE_HOLDING) {
- mUiCallManager.unholdCall(call);
- } else {
- mUiCallManager.holdCall(call);
- }
- });
-
- mMergeButton.setOnClickListener((unusedView) -> {
- UiCall call = mUiCallManager.getPrimaryCall();
- UiCall secondaryCall = mUiCallManager.getSecondaryCall();
- if (call == null || secondaryCall == null) {
- Log.w(TAG, "There aren't two call to merge.");
- return;
- }
-
- mUiCallManager.conference(call, secondaryCall);
- });
-
- mToggleDialpadButton.setOnClickListener((unusedView) -> {
- if (mToggleDialpadButton.isActivated()) {
- closeDialpad();
- } else {
- openDialpad(true /*animate*/);
- }
- });
- }
-
- @Override
- public void onDestroyView() {
- super.onDestroyView();
- }
-
- @Override
- public void onStart() {
- super.onStart();
- trySpeakerAudioRouteIfNecessary();
- }
-
- private void updateCalls() {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "updateCalls(); Primary call: " + mUiCallManager.getPrimaryCall()
- + "; Secondary call:" + mUiCallManager.getSecondaryCall());
- }
-
- mHandler.removeCallbacks(mUpdateDurationRunnable);
-
- UiCall primaryCall = mUiCallManager.getPrimaryCall();
- CharSequence disconnectCauseLabel = mLastRemovedCall == null
- ? null : mLastRemovedCall.getDisconnectCause();
- if (primaryCall == null && !TextUtils.isEmpty(disconnectCauseLabel)) {
- closeDialpad();
- setStateText(disconnectCauseLabel);
- return;
- }
-
- if (primaryCall == null || primaryCall.getState() == Call.STATE_DISCONNECTED) {
- closeDialpad();
- setStateText(getString(R.string.call_state_call_ended));
- mRingingCallControls.setVisibility(View.GONE);
- mActiveCallControls.setVisibility(View.GONE);
- return;
- }
-
- if (primaryCall.getState() == Call.STATE_RINGING) {
- mRingingCallControls.setVisibility(View.VISIBLE);
- mActiveCallControls.setVisibility(View.GONE);
- } else {
- mRingingCallControls.setVisibility(View.GONE);
- mActiveCallControls.setVisibility(View.VISIBLE);
- }
-
- loadContactPhotoForPrimaryNumber(primaryCall.getNumber());
-
- String displayName = TelecomUtils.getDisplayName(getContext(), primaryCall);
- mNameTextView.setText(displayName);
- mNameTextView.setVisibility(TextUtils.isEmpty(displayName) ? View.GONE : View.VISIBLE);
-
- Context context = getContext();
- switch (primaryCall.getState()) {
- case Call.STATE_NEW:
- // Since the content resolver call is only cached when a contact is found,
- // this should only be called once on a new call to avoid jank.
- // TODO: consider moving TelecomUtils.getTypeFromNumber into a CursorLoader
- mCallInfoLabel = TelecomUtils.getTypeFromNumber(context, primaryCall.getNumber());
- case Call.STATE_CONNECTING:
- case Call.STATE_DIALING:
- case Call.STATE_SELECT_PHONE_ACCOUNT:
- case Call.STATE_HOLDING:
- case Call.STATE_DISCONNECTED:
- mHandler.removeCallbacks(mUpdateDurationRunnable);
- String callInfoText = TelecomUtils.getCallInfoText(context,
- primaryCall, mCallInfoLabel);
- setStateText(callInfoText);
- break;
- case Call.STATE_ACTIVE:
- if (mUiBluetoothMonitor.isHfpConnected()) {
- mHandler.post(mUpdateDurationRunnable);
- }
- break;
- case Call.STATE_RINGING:
- Log.w(TAG, "There should not be a ringing call in the ongoing call fragment.");
- break;
- default:
- Log.w(TAG, "Unhandled call state: " + primaryCall.getState());
- }
-
- // If it is a voicemail call, open the dialpad (with no animation).
- if (Objects.equals(primaryCall.getNumber(), TelecomUtils.getVoicemailNumber(context))) {
- openDialpad(false /*animate*/);
- mToggleDialpadButton.setVisibility(View.GONE);
- } else {
- mToggleDialpadButton.setVisibility(View.VISIBLE);
- }
-
- // Handle the holding case.
- if (primaryCall.getState() == Call.STATE_HOLDING) {
- mEndCallButton.setVisibility(View.GONE);
- mUnholdCallButton.setVisibility(View.VISIBLE);
- mMuteButton.setVisibility(View.INVISIBLE);
- mToggleDialpadButton.setVisibility(View.INVISIBLE);
- } else {
- mEndCallButton.setVisibility(View.VISIBLE);
- mUnholdCallButton.setVisibility(View.GONE);
- mMuteButton.setVisibility(View.VISIBLE);
- mToggleDialpadButton.setVisibility(View.VISIBLE);
- }
-
- updateSecondaryCall(primaryCall, mUiCallManager.getSecondaryCall());
- }
-
- private void updateSecondaryCall(UiCall primaryCall, UiCall secondaryCall) {
- if (primaryCall == null || secondaryCall == null) {
- mSecondaryCallContainer.setVisibility(View.GONE);
- mSecondaryCallControls.setVisibility(View.GONE);
- return;
- }
-
- mSecondaryCallContainer.setVisibility(View.VISIBLE);
-
- if (primaryCall.getState() == Call.STATE_ACTIVE
- && secondaryCall.getState() == Call.STATE_HOLDING) {
- mSecondaryCallControls.setVisibility(View.VISIBLE);
- } else {
- mSecondaryCallControls.setVisibility(View.GONE);
- }
-
- Context context = getContext();
- mSecondaryNameTextView.setText(TelecomUtils.getDisplayName(context, secondaryCall));
- mSecondaryStateTextView.setText(
- TelecomUtils.callStateToUiString(context, secondaryCall.getState()));
-
- loadContactPhotoForSecondaryNumber(secondaryCall.getNumber());
- }
-
- /**
- * Loads the contact photo associated with the given number and sets it in the views that
- * correspond with a primary number.
- */
- private void loadContactPhotoForPrimaryNumber(String primaryNumber) {
- // Don't reload the image if the number is the same.
- if (Objects.equals(primaryNumber, mLoadedNumber)) {
- return;
- }
-
- final ContentResolver cr = getContext().getContentResolver();
- BitmapWorkerTask.BitmapRunnable runnable = new BitmapWorkerTask.BitmapRunnable() {
- @Override
- public void run() {
- if (mBitmap != null) {
- Resources r = getResources();
- mSmallContactPhotoView.setImageDrawable(new CircleBitmapDrawable(r, mBitmap));
- mLargeContactPhotoView.setImageBitmap(mBitmap);
- mLargeContactPhotoView.clearColorFilter();
- } else {
- mSmallContactPhotoView.setImageResource(R.drawable.logo_avatar);
- mLargeContactPhotoView.setImageResource(R.drawable.ic_avatar_bg);
- }
- }
- };
- mLoadedNumber = primaryNumber;
- BitmapWorkerTask.loadBitmap(cr, mLargeContactPhotoView, primaryNumber, runnable);
- }
-
- /**
- * Loads the contact photo associated with the given number and sets it in the views that
- * correspond to a secondary number.
- */
- private void loadContactPhotoForSecondaryNumber(String secondaryNumber) {
- BitmapWorkerTask.BitmapRunnable runnable = new BitmapWorkerTask.BitmapRunnable() {
- @Override
- public void run() {
- if (mBitmap != null) {
- mLargeContactPhotoView.setImageBitmap(mBitmap);
- } else {
- mLargeContactPhotoView.setImageResource(R.drawable.logo_avatar);
- }
- }
- };
-
- Context context = getContext();
- BitmapWorkerTask.loadBitmap(context.getContentResolver(), mLargeContactPhotoView,
- secondaryNumber, runnable);
-
- int scrimColor = context.getColor(R.color.phone_secondary_call_scrim);
- mLargeContactPhotoView.setColorFilter(scrimColor);
- }
-
- private void setStateText(CharSequence stateText) {
- mStateTextView.setText(stateText);
- mStateTextView.setVisibility(TextUtils.isEmpty(stateText) ? View.GONE : View.VISIBLE);
- }
-
- /**
- * If the phone is using bluetooth, then do nothing. If the phone is not using bluetooth:
- * <p>
- * <ol>
- * <li>If the phone supports bluetooth, use it.
- * <li>If the phone doesn't support bluetooth and support speaker, use speaker
- * <li>Otherwise, do nothing. Hopefully no phones won't have bt or speaker.
- * </ol>
- */
- private void trySpeakerAudioRouteIfNecessary() {
- if (mUiCallManager == null) {
- return;
- }
-
- int supportedAudioRouteMask = mUiCallManager.getSupportedAudioRouteMask();
- boolean supportsBluetooth = (supportedAudioRouteMask & CallAudioState.ROUTE_BLUETOOTH) != 0;
- boolean supportsSpeaker = (supportedAudioRouteMask & CallAudioState.ROUTE_SPEAKER) != 0;
- boolean isUsingBluetooth =
- mUiCallManager.getAudioRoute() == CallAudioState.ROUTE_BLUETOOTH;
-
- if (supportsBluetooth && !isUsingBluetooth) {
- mUiCallManager.setAudioRoute(CallAudioState.ROUTE_BLUETOOTH);
- } else if (!supportsBluetooth && supportsSpeaker) {
- mUiCallManager.setAudioRoute(CallAudioState.ROUTE_SPEAKER);
- }
- }
-
- private void openDialpad(boolean animate) {
- if (mToggleDialpadButton.isActivated()) {
- return;
- }
- mToggleDialpadButton.setActivated(true);
- // This array of of size 2 because getLocationOnScreen returns (x,y) coordinates.
- int[] location = new int[2];
- mToggleDialpadButton.getLocationOnScreen(location);
-
- // The dialpad should be aligned with the right edge of mToggleDialpadButton.
- int startingMargin = location[1] + mToggleDialpadButton.getWidth();
-
- ViewGroup.MarginLayoutParams layoutParams =
- (ViewGroup.MarginLayoutParams) mDialpadContainer.getLayoutParams();
-
- if (layoutParams.getMarginStart() != startingMargin) {
- layoutParams.setMarginStart(startingMargin);
- mDialpadContainer.setLayoutParams(layoutParams);
- }
-
- Animation anim = new DialpadAnimation(getContext(), false /* reverse */, animate);
- mDialpadContainer.startAnimation(anim);
- }
-
- private void closeDialpad() {
- if (!mToggleDialpadButton.isActivated()) {
- return;
- }
- mToggleDialpadButton.setActivated(false);
- Animation anim = new DialpadAnimation(getContext(), true /* reverse */);
- mDialpadContainer.startAnimation(anim);
- }
-
- private final View.OnTouchListener mDialpadTouchListener = new View.OnTouchListener() {
-
- @Override
- public boolean onTouch(View v, MotionEvent event) {
- Character digit = mDialpadButtonMap.get(v.getId());
- if (digit == null) {
- Log.w(TAG, "Unknown dialpad button pressed.");
- return false;
- }
- if (event.getAction() == MotionEvent.ACTION_DOWN) {
- v.setPressed(true);
- mUiCallManager.playDtmfTone(mUiCallManager.getPrimaryCall(), digit);
- return true;
- } else if (event.getAction() == MotionEvent.ACTION_UP) {
- v.setPressed(false);
- v.performClick();
- mUiCallManager.stopDtmfTone(mUiCallManager.getPrimaryCall());
- return true;
- }
-
- return false;
- }
- };
-
- private final View.OnKeyListener mDialpadKeyListener = new View.OnKeyListener() {
- @Override
- public boolean onKey(View v, int keyCode, KeyEvent event) {
- Character digit = mDialpadButtonMap.get(v.getId());
- if (digit == null) {
- Log.w(TAG, "Unknown dialpad button pressed.");
- return false;
- }
-
- if (event.getKeyCode() != KeyEvent.KEYCODE_DPAD_CENTER) {
- return false;
- }
-
- if (event.getAction() == KeyEvent.ACTION_DOWN) {
- v.setPressed(true);
- mUiCallManager.playDtmfTone(mUiCallManager.getPrimaryCall(), digit);
- return true;
- } else if (event.getAction() == KeyEvent.ACTION_UP) {
- v.setPressed(false);
- mUiCallManager.stopDtmfTone(mUiCallManager.getPrimaryCall());
- return true;
- }
-
- return false;
- }
- };
-
- private final Runnable mUpdateDurationRunnable = new Runnable() {
- @Override
- public void run() {
- UiCall primaryCall = mUiCallManager.getPrimaryCall();
- if (primaryCall.getState() != Call.STATE_ACTIVE) {
- return;
- }
- String callInfoText = TelecomUtils.getCallInfoText(getContext(),
- primaryCall, mCallInfoLabel);
- setStateText(callInfoText);
- mHandler.postDelayed(this /* runnable */, DateUtils.SECOND_IN_MILLIS);
-
- }
- };
-
- private final Runnable mStopDtmfToneRunnable =
- () -> mUiCallManager.stopDtmfTone(mUiCallManager.getPrimaryCall());
-
- @Override
- public void onAudioStateChanged(boolean isMuted, int route, int supportedRouteMask) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, String.format(
- "onAudioStateChanged(); isMuted: %b, audioRoute: %d, supportedAudioRouteMask: %d",
- isMuted, route, supportedRouteMask));
- }
- mMuteButton.setActivated(isMuted);
- trySpeakerAudioRouteIfNecessary();
- }
-
- @Override
- public void onCallStateChanged(UiCall call, int state) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, String.format("onCallStateChanged(); call: %s, state: %s", call, state));
- }
- updateCalls();
- }
-
- @Override
- public void onCallUpdated(UiCall call) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "onCallUpdated(); call: " + call);
- }
- updateCalls();
- }
-
- @Override
- public void onCallAdded(UiCall call) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "onCallAdded(); call: " + call);
- }
- updateCalls();
- trySpeakerAudioRouteIfNecessary();
- }
-
- @Override
- public void onCallRemoved(UiCall call) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "onCallRemoved(); call: " + call);
- }
- mLastRemovedCall = call;
- updateCalls();
- }
-
- private final class DialpadAnimation extends Animation {
- private static final int DURATION = 300;
- private static final float MAX_SCRIM_ALPHA = 0.6f;
-
- private final int mStartingTranslation;
- private final int mScrimColor;
- private final boolean mReverse;
-
- DialpadAnimation(Context context, boolean reverse) {
- this(context, reverse, true);
- }
-
- DialpadAnimation(Context context, boolean reverse, boolean animate) {
- setDuration(animate ? DURATION : 0);
- setInterpolator(new AccelerateDecelerateInterpolator());
- mStartingTranslation = context.getResources().getDimensionPixelOffset(
- R.dimen.in_call_card_dialpad_translation_x);
- mScrimColor = context.getColor(R.color.phone_theme);
- mReverse = reverse;
- }
-
- @Override
- protected void applyTransformation(float interpolatedTime, Transformation t) {
- if (mReverse) {
- interpolatedTime = 1f - interpolatedTime;
- }
- int translationX = (int) (mStartingTranslation * (1f - interpolatedTime));
- mDialpadContainer.setTranslationX(translationX);
- mDialpadContainer.setAlpha(interpolatedTime);
- if (interpolatedTime == 0f) {
- mDialpadContainer.setVisibility(View.GONE);
- } else {
- mDialpadContainer.setVisibility(View.VISIBLE);
- }
- float alpha = 255f * interpolatedTime * MAX_SCRIM_ALPHA;
- mLargeContactPhotoView.setColorFilter(Color.argb((int) alpha, Color.red(mScrimColor),
- Color.green(mScrimColor), Color.blue(mScrimColor)));
-
- mSecondaryNameTextView.setAlpha(1f - interpolatedTime);
- mSecondaryStateTextView.setAlpha(1f - interpolatedTime);
- }
- }
-}
diff --git a/src/com/android/car/dialer/StrequentsAdapter.java b/src/com/android/car/dialer/StrequentsAdapter.java
deleted file mode 100644
index 647be2ff..00000000
--- a/src/com/android/car/dialer/StrequentsAdapter.java
+++ /dev/null
@@ -1,421 +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.car.dialer;
-
-import android.content.ContentResolver;
-import android.content.Context;
-import android.database.Cursor;
-import android.graphics.PorterDuff;
-import android.os.Handler;
-import android.provider.CallLog;
-import android.text.TextUtils;
-import android.text.format.DateUtils;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import com.android.car.dialer.telecom.PhoneLoader;
-import com.android.car.dialer.telecom.TelecomUtils;
-import com.android.car.dialer.telecom.UiCallManager;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-
-import androidx.annotation.Nullable;
-import androidx.car.widget.PagedListView;
-import androidx.recyclerview.widget.RecyclerView;
-
-/**
- * Adapter class for populating Contact data as loaded from the DB to an AA GroupingRecyclerView.
- * It handles two types of contacts:
- * <p>
- * <ul>
- * <li>Strequent contacts (starred and/or frequent)
- * <li>Last call contact
- * </ul>
- */
-public class StrequentsAdapter extends RecyclerView.Adapter<CallLogViewHolder>
- implements PagedListView.ItemCap {
- // The possible view types in this adapter.
- private static final int VIEW_TYPE_EMPTY = 0;
- private static final int VIEW_TYPE_LASTCALL = 1;
- private static final int VIEW_TYPE_STREQUENT = 2;
- private static final long LAST_CALL_REFRESH_INTERVAL_MILLIS = 60 * 1000L;
-
- private final Context mContext;
- private final UiCallManager mUiCallManager;
- private List<ContactEntry> mData;
- private Handler mMainThreadHandler = new Handler();
-
- private LastCallData mLastCallData;
- private Cursor mLastCallCursor;
- private LastCallPeriodicalUpdater mLastCallPeriodicalUpdater = new LastCallPeriodicalUpdater();
-
- private final ContentResolver mContentResolver;
-
- public interface StrequentsListener<T> {
- /** Notified when a row corresponding an individual Contact (not group) was clicked. */
- void onContactClicked(T viewHolder);
- }
-
- private View.OnFocusChangeListener mFocusChangeListener;
- private StrequentsListener<CallLogViewHolder> mStrequentsListener;
-
- private int mMaxItems = -1;
- private boolean mIsEmpty;
-
- public StrequentsAdapter(Context context, UiCallManager callManager) {
- mContext = context;
- mUiCallManager = callManager;
- mContentResolver = context.getContentResolver();
- }
-
- public void setStrequentsListener(@Nullable StrequentsListener<CallLogViewHolder> listener) {
- mStrequentsListener = listener;
- }
-
- public void setLastCallCursor(@Nullable Cursor cursor) {
- mLastCallCursor = cursor;
- mLastCallData = convertLastCallCursor(cursor);
- if (cursor != null) {
- mMainThreadHandler.postDelayed(mLastCallPeriodicalUpdater,
- LAST_CALL_REFRESH_INTERVAL_MILLIS);
- } else {
- mMainThreadHandler.removeCallbacks(mLastCallPeriodicalUpdater);
- }
-
- notifyDataSetChanged();
- }
-
- public void setStrequentCursor(@Nullable Cursor cursor) {
- if (cursor != null) {
- setData(convertStrequentCursorToArray(cursor));
- } else {
- setData(null);
- }
- notifyDataSetChanged();
- }
-
- private void setData(List<ContactEntry> data) {
- mData = data;
- notifyDataSetChanged();
- }
-
- @Override
- public void setMaxItems(int maxItems) {
- mMaxItems = maxItems;
- }
-
- @Override
- public int getItemViewType(int position) {
- if (mIsEmpty) {
- return VIEW_TYPE_EMPTY;
- } else if (position == 0 && mLastCallData != null) {
- return VIEW_TYPE_LASTCALL;
- } else {
- return VIEW_TYPE_STREQUENT;
- }
- }
-
- @Override
- public int getItemCount() {
- int itemCount = mData == null ? 0 : mData.size();
- itemCount += mLastCallData == null ? 0 : 1;
-
- mIsEmpty = itemCount == 0;
-
- // If there is no data to display, add one to the item count to display the card in the
- // empty state.
- if (mIsEmpty) {
- itemCount++;
- }
-
- return mMaxItems >= 0 ? Math.min(mMaxItems, itemCount) : itemCount;
- }
-
- @Override
- public CallLogViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
- View view;
- switch (viewType) {
- case VIEW_TYPE_EMPTY:
- view = LayoutInflater.from(parent.getContext())
- .inflate(R.layout.call_log_list_item_empty, parent, false);
- return new CallLogViewHolder(view);
-
- case VIEW_TYPE_LASTCALL:
- case VIEW_TYPE_STREQUENT:
- default:
- view = LayoutInflater.from(parent.getContext())
- .inflate(R.layout.call_log_list_item_card, parent, false);
- return new CallLogViewHolder(view);
- }
- }
-
- @Override
- public void onBindViewHolder(final CallLogViewHolder viewHolder, int position) {
- switch (viewHolder.getItemViewType()) {
- case VIEW_TYPE_LASTCALL:
- onBindLastCallRow(viewHolder);
- break;
-
- case VIEW_TYPE_EMPTY:
- viewHolder.icon.setImageResource(R.drawable.ic_empty_speed_dial);
- viewHolder.title.setText(R.string.speed_dial_empty);
- viewHolder.title.setTextColor(mContext.getColor(R.color.car_body1_light));
- break;
-
- case VIEW_TYPE_STREQUENT:
- default:
- int positionIntoData = position;
-
- // If there is last call data, then decrement the position so there is not an out of
- // bounds error on the mData.
- if (mLastCallData != null) {
- positionIntoData--;
- }
-
- onBindView(viewHolder, mData.get(positionIntoData));
- viewHolder.callType.setVisibility(View.VISIBLE);
- }
- }
-
- private void onViewClicked(CallLogViewHolder viewHolder) {
- if (mStrequentsListener != null) {
- mStrequentsListener.onContactClicked(viewHolder);
- }
- }
-
- @Override
- public void onViewAttachedToWindow(CallLogViewHolder holder) {
- if (mFocusChangeListener != null) {
- holder.itemView.setOnFocusChangeListener(mFocusChangeListener);
- }
- }
-
- @Override
- public void onViewDetachedFromWindow(CallLogViewHolder holder) {
- holder.itemView.setOnFocusChangeListener(null);
- }
-
- /**
- * Converts the strequents data in the given cursor into a list of {@link ContactEntry}s.
- */
- private List<ContactEntry> convertStrequentCursorToArray(Cursor cursor) {
- List<ContactEntry> strequentContactEntries = new ArrayList<>();
- HashMap<Integer, ContactEntry> entryMap = new HashMap<>();
- cursor.moveToPosition(-1);
-
- while (cursor.moveToNext()) {
- final ContactEntry entry = ContactEntry.fromCursor(cursor, mContext);
- entryMap.put(entry.hashCode(), entry);
- }
-
- strequentContactEntries.addAll(entryMap.values());
- Collections.sort(strequentContactEntries);
- return strequentContactEntries;
- }
-
- /**
- * Binds the views in the entry to the data of last call.
- *
- * @param viewHolder the view holder corresponding to this entry
- */
- private void onBindLastCallRow(final CallLogViewHolder viewHolder) {
- if (mLastCallData == null) {
- return;
- }
-
- viewHolder.itemView.setOnClickListener(v -> onViewClicked(viewHolder));
-
- String primaryText = mLastCallData.getPrimaryText();
- String number = mLastCallData.getNumber();
-
- if (!number.equals(viewHolder.itemView.getTag())) {
- viewHolder.title.setText(mLastCallData.getPrimaryText());
- viewHolder.itemView.setTag(number);
- viewHolder.callTypeIconsView.clear();
- viewHolder.callTypeIconsView.setVisibility(View.VISIBLE);
-
- // mHasFirstItem is true only in main screen, or else it is in drawer, then we need
- // to add
- // call type icons for call history items.
- viewHolder.smallIcon.setVisibility(View.GONE);
- int[] callTypes = mLastCallData.getCallTypes();
- int icons = Math.min(callTypes.length, CallTypeIconsView.MAX_CALL_TYPE_ICONS);
- for (int i = 0; i < icons; i++) {
- viewHolder.callTypeIconsView.add(callTypes[i]);
- }
-
- TelecomUtils.setContactBitmapAsync(mContext, viewHolder.icon, primaryText, number);
- }
-
- viewHolder.text.setText(mLastCallData.getSecondaryText());
- }
-
- /**
- * Converts the last call information in the given cursor into a {@link LastCallData} object
- * so that the cursor can be closed.
- *
- * @return A valid {@link LastCallData} or {@code null} if the cursor is {@code null} or has no
- * data in it.
- */
- @Nullable
- public LastCallData convertLastCallCursor(@Nullable Cursor cursor) {
- if (cursor == null || cursor.getCount() == 0) {
- return null;
- }
-
- cursor.moveToFirst();
-
- final StringBuilder nameSb = new StringBuilder();
- int column = PhoneLoader.getNameColumnIndex(cursor);
- String cachedName = cursor.getString(column);
- final String number = PhoneLoader.getPhoneNumber(cursor, mContentResolver);
- if (cachedName == null) {
- cachedName = TelecomUtils.getDisplayName(mContext, number);
- }
-
- boolean isVoicemail = false;
- if (cachedName == null) {
- if (number.equals(TelecomUtils.getVoicemailNumber(mContext))) {
- isVoicemail = true;
- nameSb.append(mContext.getString(R.string.voicemail));
- } else {
- String displayName = TelecomUtils.getFormattedNumber(mContext, number);
- if (TextUtils.isEmpty(displayName)) {
- displayName = mContext.getString(R.string.unknown);
- }
- nameSb.append(displayName);
- }
- } else {
- nameSb.append(cachedName);
- }
- column = cursor.getColumnIndex(CallLog.Calls.DATE);
- // If we set this to 0, getRelativeTime will return null and no relative time
- // will be displayed.
- long millis = column == -1 ? 0 : cursor.getLong(column);
- StringBuilder secondaryText = new StringBuilder();
- CharSequence relativeDate = getRelativeTime(millis);
- if (!isVoicemail) {
- CharSequence type = TelecomUtils.getTypeFromNumber(mContext, number);
- secondaryText.append(type);
- if (!TextUtils.isEmpty(type) && !TextUtils.isEmpty(relativeDate)) {
- secondaryText.append(", ");
- }
- }
- if (relativeDate != null) {
- secondaryText.append(relativeDate);
- }
-
- int[] callTypes = mUiCallManager.getCallTypes(cursor, 1);
-
- return new LastCallData(number, nameSb.toString(), secondaryText.toString(), callTypes);
- }
-
- /**
- * Bind view function for frequent call row.
- */
- private void onBindView(final CallLogViewHolder viewHolder, final ContactEntry entry) {
- viewHolder.itemView.setOnClickListener(v -> onViewClicked(viewHolder));
-
- final String number = entry.getNumber();
- // TODO: Why is being a voicemail related to not having a name?
- boolean isVoicemail = entry.isVoicemail();
- String secondaryText = "";
- if (!isVoicemail) {
- secondaryText = String.valueOf(TelecomUtils.getTypeFromNumber(mContext, number));
- }
-
- viewHolder.text.setText(secondaryText);
- viewHolder.itemView.setTag(number);
- viewHolder.callTypeIconsView.clear();
-
- String displayName = entry.getDisplayName();
- viewHolder.title.setText(displayName);
-
- TelecomUtils.setContactBitmapAsync(mContext, viewHolder.icon, displayName, number);
-
- if (entry.isStarred()) {
- viewHolder.smallIcon.setVisibility(View.VISIBLE);
- final int iconColor = mContext.getColor(android.R.color.white);
- viewHolder.smallIcon.setColorFilter(iconColor, PorterDuff.Mode.SRC_IN);
- viewHolder.smallIcon.setImageResource(R.drawable.ic_favorite);
- } else {
- viewHolder.smallIcon.setVisibility(View.GONE);
- }
-
- }
-
- /**
- * Build any timestamp and label into a single string. If the given timestamp is invalid, then
- * {@code null} is returned.
- */
- @Nullable
- private static CharSequence getRelativeTime(long millis) {
- if (millis <= 0) {
- return null;
- }
-
- return DateUtils.getRelativeTimeSpanString(millis, System.currentTimeMillis(),
- DateUtils.MINUTE_IN_MILLIS, DateUtils.FORMAT_ABBREV_RELATIVE);
- }
-
- /**
- * A container for data relating to a last call entry.
- */
- private class LastCallData {
- private final String mNumber;
- private final String mPrimaryText;
- private final String mSecondaryText;
- private final int[] mCallTypes;
-
- LastCallData(String number, String primaryText, String secondaryText,
- int[] callTypes) {
- mNumber = number;
- mPrimaryText = primaryText;
- mSecondaryText = secondaryText;
- mCallTypes = callTypes;
- }
-
- public String getNumber() {
- return mNumber;
- }
-
- public String getPrimaryText() {
- return mPrimaryText;
- }
-
- public String getSecondaryText() {
- return mSecondaryText;
- }
-
- public int[] getCallTypes() {
- return mCallTypes;
- }
- }
-
- private class LastCallPeriodicalUpdater implements Runnable {
-
- @Override
- public void run() {
- mLastCallData = convertLastCallCursor(mLastCallCursor);
- notifyItemChanged(0);
- mMainThreadHandler.postDelayed(this, LAST_CALL_REFRESH_INTERVAL_MILLIS);
- }
- }
-}
diff --git a/src/com/android/car/dialer/StrequentsFragment.java b/src/com/android/car/dialer/StrequentsFragment.java
deleted file mode 100644
index 06141eda..00000000
--- a/src/com/android/car/dialer/StrequentsFragment.java
+++ /dev/null
@@ -1,276 +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.car.dialer;
-
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.CursorLoader;
-import android.database.ContentObserver;
-import android.database.Cursor;
-import android.graphics.Rect;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.Handler;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import com.android.car.dialer.telecom.PhoneLoader;
-import com.android.car.dialer.telecom.UiCallManager;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.car.widget.PagedListView;
-import androidx.fragment.app.Fragment;
-import androidx.recyclerview.widget.GridLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-
-/**
- * Contains a list of contacts. The call types can be any of the CALL_TYPE_* fields from
- * {@link PhoneLoader}.
- */
-public class StrequentsFragment extends Fragment {
- private static final String TAG = "Em.StrequentsFrag";
-
- private static final String KEY_MAX_CLICKS = "max_clicks";
- private static final int DEFAULT_MAX_CLICKS = 6;
-
- private UiCallManager mUiCallManager;
- private StrequentsAdapter mAdapter;
- private CursorLoader mSpeedialCursorLoader;
- private CursorLoader mCallLogCursorLoader;
- private Context mContext;
- private PagedListView mListView;
- private Cursor mStrequentCursor;
- private Cursor mCallLogCursor;
- private boolean mHasLoadedData;
-
- public static StrequentsFragment newInstance() {
- return new StrequentsFragment();
- }
-
- @Override
- public void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "onCreate");
- }
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "onCreateView");
- }
-
- mContext = getContext();
- mUiCallManager = UiCallManager.get();
-
- View view = inflater.inflate(R.layout.strequents_fragment, container, false);
- mListView = view.findViewById(R.id.list_view);
- int numOfColumn = getContext().getResources().getInteger(
- R.integer.favorite_fragment_grid_column);
- mListView.getRecyclerView().setLayoutManager(
- new GridLayoutManager(getContext(), numOfColumn));
- mListView.getRecyclerView().addItemDecoration(new ItemSpacingDecoration());
-
- mSpeedialCursorLoader = PhoneLoader.registerCallObserver(PhoneLoader.CALL_TYPE_SPEED_DIAL,
- mContext, (loader, cursor) -> {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "PhoneLoader: onLoadComplete (CALL_TYPE_SPEED_DIAL)");
- }
-
- onLoadStrequentCursor(cursor);
- });
-
- // Get the latest call log from the call logs history.
- mCallLogCursorLoader = PhoneLoader.registerCallObserver(PhoneLoader.CALL_TYPE_ALL, mContext,
- (loader, cursor) -> {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "PhoneLoader: onLoadComplete (CALL_TYPE_ALL)");
- }
- onLoadCallLogCursor(cursor);
- });
-
- ContentResolver contentResolver = mContext.getContentResolver();
- contentResolver.registerContentObserver(mSpeedialCursorLoader.getUri(),
- false, new SpeedDialContentObserver(new Handler()));
- contentResolver.registerContentObserver(mCallLogCursorLoader.getUri(),
- false, new CallLogContentObserver(new Handler()));
-
- // Maximum number of forward acting clicks the user can perform
- Bundle args = getArguments();
- int maxClicks = args == null
- ? DEFAULT_MAX_CLICKS
- : args.getInt(KEY_MAX_CLICKS, DEFAULT_MAX_CLICKS /* G.maxForwardClicks.get() */);
- // We want to show one fewer page than max clicks to allow clicking on an item,
- // but, the first page is "free" since it doesn't take any clicks to show
- final int maxPages = maxClicks < 0 ? -1 : maxClicks;
- if (Log.isLoggable(TAG, Log.VERBOSE)) {
- Log.v(TAG, "Max clicks: " + maxClicks + ", Max pages: " + maxPages);
- }
-
- mAdapter = new StrequentsAdapter(mContext, mUiCallManager);
- mAdapter.setStrequentsListener(viewHolder -> {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "onContactedClicked");
- }
-
- mUiCallManager.safePlaceCall((String) viewHolder.itemView.getTag(), false);
- });
- mListView.setMaxPages(maxPages);
- mListView.setAdapter(mAdapter);
-
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "setItemAnimator");
- }
-
- mListView.getRecyclerView().setItemAnimator(null);
- return view;
- }
-
- @Override
- public void onDestroyView() {
- super.onDestroyView();
-
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "onDestroyView");
- }
-
- mAdapter.setStrequentCursor(null);
- mAdapter.setLastCallCursor(null);
- mCallLogCursorLoader.reset();
- mSpeedialCursorLoader.reset();
- mCallLogCursor = null;
- mStrequentCursor = null;
- mHasLoadedData = false;
- mContext = null;
- }
-
- private void loadDataIntoAdapter() {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "loadDataIntoAdapter");
- }
-
- mHasLoadedData = true;
- mAdapter.setLastCallCursor(mCallLogCursor);
- mAdapter.setStrequentCursor(mStrequentCursor);
- }
-
- private void onLoadStrequentCursor(Cursor cursor) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "onLoadStrequentCursor");
- }
-
- if (cursor == null) {
- throw new IllegalArgumentException(
- "cursor was null in on speed dial fetched");
- }
-
- mStrequentCursor = cursor;
- if (mCallLogCursor != null) {
- if (mHasLoadedData) {
- mAdapter.setStrequentCursor(cursor);
- } else {
- loadDataIntoAdapter();
- }
- }
- }
-
- private void onLoadCallLogCursor(Cursor cursor) {
- if (cursor == null) {
- throw new IllegalArgumentException(
- "cursor was null in on calls fetched");
- }
-
- mCallLogCursor = cursor;
- if (mStrequentCursor != null) {
- if (mHasLoadedData) {
- mAdapter.setLastCallCursor(cursor);
- } else {
- loadDataIntoAdapter();
- }
- }
- }
-
- /**
- * A {@link ContentResolver} that is responsible for reloading the user's starred and frequent
- * contacts.
- */
- private class SpeedDialContentObserver extends ContentObserver {
- public SpeedDialContentObserver(Handler handler) {
- super(handler);
- }
-
- @Override
- public void onChange(boolean selfChange) {
- onChange(selfChange, null);
- }
-
- @Override
- public void onChange(boolean selfChange, Uri uri) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "SpeedDialContentObserver onChange() called. Reloading strequents.");
- }
- mSpeedialCursorLoader.startLoading();
- }
- }
-
- /**
- * A {@link ContentResolver} that is responsible for reloading the user's recent calls.
- */
- private class CallLogContentObserver extends ContentObserver {
- public CallLogContentObserver(Handler handler) {
- super(handler);
- }
-
- @Override
- public void onChange(boolean selfChange) {
- onChange(selfChange, null);
- }
-
- @Override
- public void onChange(boolean selfChange, Uri uri) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "CallLogContentObserver onChange() called. Reloading call log.");
- }
- mCallLogCursorLoader.startLoading();
- }
- }
-
- private class ItemSpacingDecoration extends RecyclerView.ItemDecoration {
-
- @Override
- public void getItemOffsets(@NonNull Rect outRect, @NonNull View view,
- @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
- super.getItemOffsets(outRect, view, parent, state);
- int carPadding1 = mContext.getResources().getDimensionPixelOffset(
- R.dimen.car_padding_1);
-
- int leftPadding = 0;
- int rightPadding = 0;
- if (parent.getChildAdapterPosition(view) % 2 == 0) {
- rightPadding = carPadding1;
- } else {
- leftPadding = carPadding1;
- }
-
- outRect.set(leftPadding, carPadding1, rightPadding, carPadding1);
- }
- }
-}
diff --git a/src/com/android/car/dialer/TelecomActivity.java b/src/com/android/car/dialer/TelecomActivity.java
deleted file mode 100644
index 3d4da113..00000000
--- a/src/com/android/car/dialer/TelecomActivity.java
+++ /dev/null
@@ -1,576 +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.car.dialer;
-
-import static com.android.car.dialer.ui.CallHistoryFragment.CALL_TYPE_KEY;
-
-import android.app.KeyguardManager;
-import android.content.Intent;
-import android.graphics.PorterDuff;
-import android.graphics.drawable.Drawable;
-import android.os.Bundle;
-import android.telecom.Call;
-import android.telephony.PhoneNumberUtils;
-import android.util.Log;
-
-import com.android.car.dialer.telecom.InMemoryPhoneBook;
-import com.android.car.dialer.telecom.PhoneLoader;
-import com.android.car.dialer.telecom.UiCall;
-import com.android.car.dialer.telecom.UiCallManager;
-import com.android.car.dialer.ui.CallHistoryFragment;
-import com.android.car.dialer.ui.ContactListFragment;
-import com.android.car.dialer.ui.InCallFragment;
-
-import java.util.stream.Stream;
-
-import androidx.annotation.Nullable;
-import androidx.annotation.StringRes;
-import androidx.car.drawer.CarDrawerActivity;
-import androidx.car.drawer.CarDrawerAdapter;
-import androidx.car.drawer.DrawerItemViewHolder;
-import androidx.fragment.app.Fragment;
-
-/**
- * Main activity for the Dialer app. Displays different fragments depending on call and
- * connectivity status:
- * <ul>
- * <li>OngoingCallFragment
- * <li>NoHfpFragment
- * <li>DialerFragment
- * <li>StrequentFragment
- * </ul>
- */
-public class TelecomActivity extends CarDrawerActivity implements CallListener {
- private static final String TAG = "TelecomActivity";
-
- private static final String ACTION_ANSWER_CALL = "com.android.car.dialer.ANSWER_CALL";
- private static final String ACTION_END_CALL = "com.android.car.dialer.END_CALL";
-
- private static final String DIALER_BACKSTACK = "DialerBackstack";
- private static final String CONTENT_FRAGMENT_TAG = "CONTENT_FRAGMENT_TAG";
- private static final String DIALER_FRAGMENT_TAG = "DIALER_FRAGMENT_TAG";
-
- private final UiBluetoothMonitor.Listener mBluetoothListener = this::updateCurrentFragment;
-
- private UiCallManager mUiCallManager;
- private UiBluetoothMonitor mUiBluetoothMonitor;
- private boolean mShowInFrontOfKeyguard;
-
- /**
- * Whether or not it is safe to make transactions on the
- * {@link androidx.fragment.app.FragmentManager}. This variable prevents a possible exception
- * when calling commit() on the FragmentManager.
- *
- * <p>The default value is {@code true} because it is only after
- * {@link #onSaveInstanceState(Bundle)} that fragment commits are not allowed.
- */
- private boolean mAllowFragmentCommits = true;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setToolbarElevation(0f);
-
- if (vdebug()) {
- Log.d(TAG, "onCreate");
- }
-
- KeyguardManager keyguardManager = (KeyguardManager) getSystemService(KEYGUARD_SERVICE);
- showInFrontOfKeyguard(keyguardManager.isKeyguardLocked());
-
- setMainContent(R.layout.telecom_activity);
- getWindow().getDecorView().setBackgroundColor(getColor(R.color.phone_theme));
- updateTitle();
-
- mUiCallManager = UiCallManager.init(getApplicationContext());
- mUiBluetoothMonitor = new UiBluetoothMonitor(this);
-
- InMemoryPhoneBook.init(getApplicationContext());
-
- findViewById(R.id.search).setOnClickListener(
- v -> startActivity(new Intent(this, ContactSearchActivity.class)));
-
- getDrawerController().setRootAdapter(new DialerRootAdapter());
- }
-
- @Override
- protected void onDestroy() {
- super.onDestroy();
- if (vdebug()) {
- Log.d(TAG, "onDestroy");
- }
- mUiBluetoothMonitor.tearDown();
- InMemoryPhoneBook.tearDown();
- mUiCallManager.tearDown();
- mUiCallManager = null;
- }
-
- @Override
- protected void onStop() {
- super.onStop();
- mUiCallManager.removeListener(this);
- mUiBluetoothMonitor.removeListener(mBluetoothListener);
- }
-
- @Override
- public void onSaveInstanceState(Bundle outState) {
- // A transaction can only be committed with this method prior to its containing activity
- // saving its state.
- mAllowFragmentCommits = false;
- super.onSaveInstanceState(outState);
- }
-
- @Override
- protected void onNewIntent(Intent i) {
- super.onNewIntent(i);
- setIntent(i);
- }
-
- @Override
- protected void onStart() {
- if (vdebug()) {
- Log.d(TAG, "onStart");
- }
- super.onStart();
-
- // Fragment commits are not allowed once the Activity's state has been saved. Once
- // onStart() has been called, the FragmentManager should now allow commits.
- mAllowFragmentCommits = true;
-
- // Update the current fragment before handling the intent so that any UI updates in
- // handleIntent() is not overridden by updateCurrentFragment().
- updateCurrentFragment();
- handleIntent();
-
- mUiCallManager.addListener(this);
- mUiBluetoothMonitor.addListener(mBluetoothListener);
- }
-
- private void handleIntent() {
- Intent intent = getIntent();
- String action = intent != null ? intent.getAction() : null;
-
- if (vdebug()) {
- Log.d(TAG, "handleIntent, intent: " + intent + ", action: " + action);
- }
-
- if (action == null || action.length() == 0) {
- return;
- }
-
- String number;
- UiCall ringingCall;
- switch (action) {
- case ACTION_ANSWER_CALL:
- ringingCall = mUiCallManager.getCallWithState(Call.STATE_RINGING);
- if (ringingCall == null) {
- Log.e(TAG, "Unable to answer ringing call. There is none.");
- } else {
- mUiCallManager.answerCall(ringingCall);
- }
- break;
-
- case ACTION_END_CALL:
- ringingCall = mUiCallManager.getCallWithState(Call.STATE_RINGING);
- if (ringingCall == null) {
- Log.e(TAG, "Unable to end ringing call. There is none.");
- } else {
- mUiCallManager.disconnectCall(ringingCall);
- }
- break;
-
- case Intent.ACTION_DIAL:
- number = PhoneNumberUtils.getNumberFromIntent(intent, this);
- if (!(getCurrentFragment() instanceof NoHfpFragment)) {
- showDialer(number);
- }
- break;
-
- case Intent.ACTION_CALL:
- number = PhoneNumberUtils.getNumberFromIntent(intent, this);
- mUiCallManager.safePlaceCall(number, false /* bluetoothRequired */);
- break;
-
- default:
- // Do nothing.
- }
-
- setIntent(null);
- }
-
- private void showInFrontOfKeyguard(boolean show) {
- if (mShowInFrontOfKeyguard == show) {
- return;
- }
- mShowInFrontOfKeyguard = show;
- setShowWhenLocked(show);
- setTurnScreenOn(show);
- }
-
- /**
- * Updates the content fragment of this Activity based on the state of the application.
- */
- private void updateCurrentFragment() {
- if (vdebug()) {
- Log.d(TAG, "updateCurrentFragment()");
- }
-
- boolean callEmpty = mUiCallManager.getCalls().isEmpty();
- if (!mUiBluetoothMonitor.isBluetoothEnabled() && callEmpty) {
- showNoHfpFragment(R.string.bluetooth_disabled);
- } else if (!mUiBluetoothMonitor.isBluetoothPaired() && callEmpty) {
- showNoHfpFragment(R.string.bluetooth_unpaired);
- } else if (!mUiBluetoothMonitor.isHfpConnected() && callEmpty) {
- showNoHfpFragment(R.string.no_hfp);
- } else {
- UiCall ongoingCall = mUiCallManager.getPrimaryCall();
-
- if (vdebug()) {
- Log.d(TAG, "ongoingCall: " + ongoingCall + ", mCurrentFragment: "
- + getCurrentFragment());
- }
-
- if (ongoingCall == null && getCurrentFragment() instanceof InCallFragment) {
- showSpeedDialFragment();
- } else if (ongoingCall != null) {
- showOngoingCallFragment();
- } else {
- showSpeedDialFragment();
- }
- }
-
- if (vdebug()) {
- Log.d(TAG, "updateCurrentFragment: done");
- }
- }
-
- private void showSpeedDialFragment() {
- if (vdebug()) {
- Log.d(TAG, "showSpeedDialFragment");
- }
-
- if (!mAllowFragmentCommits || getCurrentFragment() instanceof StrequentsFragment) {
- return;
- }
-
- Fragment fragment = StrequentsFragment.newInstance();
- setContentFragment(fragment);
- }
-
- private void showOngoingCallFragment() {
- if (vdebug()) {
- Log.d(TAG, "showOngoingCallFragment");
- }
- if (!mAllowFragmentCommits || getCurrentFragment() instanceof InCallFragment) {
- // in case the dialer is still open, (e.g. when dialing the second phone during
- // a phone call), close it
- maybeHideDialer();
- getDrawerController().closeDrawer();
- return;
- }
-
- showInFrontOfKeyguard(true);
- Fragment fragment = InCallFragment.newInstance();
- setContentFragmentWithFadeAnimation(fragment);
- getDrawerController().closeDrawer();
- }
-
- private void showDialer() {
- if (vdebug()) {
- Log.d(TAG, "showDialer");
- }
-
- showDialer(null /* dialNumber */);
- }
-
- /**
- * Displays the {@link DialerFragment} and initialize it with the given phone number.
- */
- private void showDialer(@Nullable String dialNumber) {
- if (vdebug()) {
- Log.d(TAG, "showDialer with number: " + dialNumber);
- }
-
- if (!mAllowFragmentCommits ||
- getSupportFragmentManager().findFragmentByTag(DIALER_FRAGMENT_TAG) != null) {
- return;
- }
-
- Fragment fragment = DialerFragment.newInstance(dialNumber);
- // Add the dialer fragment to the backstack so that it can be popped off to dismiss it.
- setContentFragment(fragment);
- }
-
- /**
- * Checks if the dialpad fragment is opened and hides it if it is.
- */
- private void maybeHideDialer() {
- // The dialer is the only fragment to be added to the back stack. Dismiss the dialer by
- // removing it from the back stack.
- if (getSupportFragmentManager().getBackStackEntryCount() > 0) {
- getSupportFragmentManager().popBackStack();
- }
- }
-
- private void showNoHfpFragment(@StringRes int stringResId) {
- if (!mAllowFragmentCommits) {
- return;
- }
-
- String errorMessage = getString(stringResId);
- Fragment currentFragment = getCurrentFragment();
-
- if (currentFragment instanceof NoHfpFragment) {
- ((NoHfpFragment) currentFragment).setErrorMessage(errorMessage);
- } else {
- setContentFragment(NoHfpFragment.newInstance(errorMessage));
- }
- }
-
- private void setContentFragmentWithSlideAndDelayAnimation(Fragment fragment) {
- if (vdebug()) {
- Log.d(TAG, "setContentFragmentWithSlideAndDelayAnimation, fragment: " + fragment);
- }
- setContentFragmentWithAnimations(fragment,
- R.anim.telecom_slide_in_with_delay, R.anim.telecom_slide_out);
- }
-
- private void setContentFragmentWithFadeAnimation(Fragment fragment) {
- if (vdebug()) {
- Log.d(TAG, "setContentFragmentWithFadeAnimation, fragment: " + fragment);
- }
- setContentFragmentWithAnimations(fragment,
- R.anim.telecom_fade_in, R.anim.telecom_fade_out);
- }
-
- private void setContentFragmentWithAnimations(Fragment fragment, int enter, int exit) {
- if (vdebug()) {
- Log.d(TAG, "setContentFragmentWithAnimations: " + fragment);
- }
-
- maybeHideDialer();
-
- getSupportFragmentManager().beginTransaction()
- .setCustomAnimations(enter, exit)
- .replace(R.id.content_fragment_container, fragment, CONTENT_FRAGMENT_TAG)
- .commitNow();
- }
-
- /**
- * Sets the fragment that will be shown as the main content of this Activity. Note that this
- * fragment is not always visible. In particular, the dialer fragment can show up on top of this
- * fragment.
- */
- private void setContentFragment(Fragment fragment) {
- maybeHideDialer();
- getSupportFragmentManager().beginTransaction()
- .replace(R.id.content_fragment_container, fragment, CONTENT_FRAGMENT_TAG)
- .commitNow();
- updateTitle();
- }
-
- /**
- * Returns the fragment that is currently being displayed as the content view. Note that this
- * is not necessarily the fragment that is visible. For example, the returned fragment
- * could be the content, but the dial fragment is being displayed on top of it. Check for
- * the existence of the dial fragment with the TAG {@link #DIALER_FRAGMENT_TAG}.
- */
- @Nullable
- private Fragment getCurrentFragment() {
- return getSupportFragmentManager().findFragmentByTag(CONTENT_FRAGMENT_TAG);
- }
-
- private static boolean vdebug() {
- return Log.isLoggable(TAG, Log.DEBUG);
- }
-
- @Override
- public void onAudioStateChanged(boolean isMuted, int route, int supportedRouteMask) {
- fragmentsToPropagateCallback().forEach(fragment -> ((CallListener) fragment)
- .onAudioStateChanged(isMuted, route, supportedRouteMask));
- }
-
- @Override
- public void onCallStateChanged(UiCall call, int state) {
- if (vdebug()) {
- Log.d(TAG, "onCallStateChanged");
- }
- updateCurrentFragment();
-
- fragmentsToPropagateCallback().forEach(fragment -> ((CallListener) fragment)
- .onCallStateChanged(call, state));
- }
-
- @Override
- public void onCallUpdated(UiCall call) {
- if (vdebug()) {
- Log.d(TAG, "onCallUpdated");
- }
- updateCurrentFragment();
-
- fragmentsToPropagateCallback().forEach(fragment -> ((CallListener) fragment)
- .onCallUpdated(call));
- }
-
- @Override
- public void onCallAdded(UiCall call) {
- if (vdebug()) {
- Log.d(TAG, "onCallAdded");
- }
- updateCurrentFragment();
-
- fragmentsToPropagateCallback().forEach(fragment -> ((CallListener) fragment)
- .onCallAdded(call));
- }
-
- @Override
- public void onCallRemoved(UiCall call) {
- if (vdebug()) {
- Log.d(TAG, "onCallRemoved");
- }
- updateCurrentFragment();
- showInFrontOfKeyguard(false);
-
- fragmentsToPropagateCallback().forEach(fragment -> ((CallListener) fragment)
- .onCallRemoved(call));
- }
-
- private static boolean shouldPropagateCallback(Fragment fragment) {
- return fragment instanceof CallListener && fragment.isAdded();
- }
-
- private Stream<Fragment> fragmentsToPropagateCallback() {
- return getSupportFragmentManager().getFragments().stream()
- .filter(fragment -> shouldPropagateCallback(fragment));
- }
-
- private class DialerRootAdapter extends CarDrawerAdapter {
- private static final int ITEM_FAVORITES = 0;
- private static final int ITEM_CALLLOG_ALL = 1;
- private static final int ITEM_CALLLOG_MISSED = 2;
- private static final int ITEM_CONTACT = 3;
- private static final int ITEM_DIAL = 4;
-
- private static final int ITEM_COUNT = 5;
-
- DialerRootAdapter() {
- super(TelecomActivity.this, false /* showDisabledListOnEmpty */);
- }
-
- @Override
- protected int getActualItemCount() {
- return ITEM_COUNT;
- }
-
- @Override
- public void populateViewHolder(DrawerItemViewHolder holder, int position) {
- final int iconColor = getResources().getColor(R.color.car_tint);
- int textResId, iconResId;
- switch (position) {
- case ITEM_DIAL:
- textResId = R.string.calllog_dial_number;
- iconResId = R.drawable.ic_drawer_dialpad;
- break;
- case ITEM_CALLLOG_ALL:
- textResId = R.string.calllog_all;
- iconResId = R.drawable.ic_drawer_history;
- break;
- case ITEM_CALLLOG_MISSED:
- textResId = R.string.calllog_missed;
- iconResId = R.drawable.ic_call_missed;
- break;
- case ITEM_FAVORITES:
- textResId = R.string.calllog_favorites;
- iconResId = R.drawable.ic_favorite;
- break;
- case ITEM_CONTACT:
- textResId = R.string.contact_menu_label;
- iconResId = R.drawable.ic_contact;
- break;
- default:
- Log.wtf(TAG, "Unexpected position: " + position);
- return;
- }
- holder.getTitle().setText(textResId);
- Drawable drawable = getDrawable(iconResId);
- drawable.setColorFilter(iconColor, PorterDuff.Mode.SRC_IN);
- holder.getIcon().setImageDrawable(drawable);
- }
-
- @Override
- public void onItemClick(int position) {
- getDrawerController().closeDrawer();
- switch (position) {
- case ITEM_DIAL:
- showDialer();
- break;
- case ITEM_CALLLOG_ALL:
- showCallHistory(PhoneLoader.CallType.CALL_TYPE_ALL);
- break;
- case ITEM_CALLLOG_MISSED:
- showCallHistory(PhoneLoader.CallType.MISSED_TYPE);
- break;
- case ITEM_FAVORITES:
- showSpeedDialFragment();
- break;
- case ITEM_CONTACT:
- showContact();
- break;
- default:
- Log.w(TAG, "Invalid position in ROOT menu! " + position);
- }
- setTitle(getTitleString());
- }
- }
-
- private void showCallHistory(@PhoneLoader.CallType int callType) {
- setContentFragment(CallHistoryFragment.newInstance(callType));
- }
-
- private void showContact() {
- setContentFragment(ContactListFragment.newInstance());
- }
-
- private void updateTitle() {
- setTitle(getTitleString());
- }
-
- private String getTitleString() {
- Fragment currentFragment = getCurrentFragment();
-
- int titleResId = R.string.phone_app_name;
-
- if (currentFragment instanceof StrequentsFragment) {
- titleResId = R.string.contacts_title;
- } else if (currentFragment instanceof CallHistoryFragment) {
- int callType = currentFragment.getArguments().getInt(CALL_TYPE_KEY);
- if (callType == PhoneLoader.CallType.MISSED_TYPE) {
- titleResId = R.string.missed_call_title;
- } else {
- titleResId = R.string.call_history_title;
- }
- } else if (currentFragment instanceof ContactListFragment) {
- titleResId = R.string.contacts_title;
- } else if (currentFragment instanceof DialerFragment) {
- titleResId = R.string.dialpad_title;
- } else if (currentFragment instanceof InCallFragment
- || currentFragment instanceof OngoingCallFragment) {
- titleResId = R.string.in_call_title;
- }
-
- return getString(titleResId);
- }
-}
diff --git a/src/com/android/car/dialer/TelecomIntents.java b/src/com/android/car/dialer/TelecomIntents.java
deleted file mode 100644
index a63ce1eb..00000000
--- a/src/com/android/car/dialer/TelecomIntents.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * 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.car.dialer;
-
-/**
- * Constants related to starting Dialer-specific Intents.
- */
-public class TelecomIntents {
- private TelecomIntents() {}
-
- /**
- * Activity action to display the details of a contact. The use of this action should be
- * accompanied by an URI string extra to the contact to display. Use the key
- * {@link #CONTACT_LOOKUP_URI_EXTRA}.
- *
- * @see android.provider.ContactsContract.Contacts#getLookupUri(long, String)
- */
- public static final String ACTION_SHOW_CONTACT_DETAILS =
- "com.android.car.dialer.SHOW_CONTACT_DETAILS";
-
- /**
- * A key to the Intent extra that holds the contact lookup URI.
- */
- public static final String CONTACT_LOOKUP_URI_EXTRA =
- "com.android.car.dialer.CONTACT_LOOKUP_URI_EXTRA";
-}
diff --git a/src/com/android/car/dialer/UiBluetoothMonitor.java b/src/com/android/car/dialer/UiBluetoothMonitor.java
deleted file mode 100644
index fdc3560c..00000000
--- a/src/com/android/car/dialer/UiBluetoothMonitor.java
+++ /dev/null
@@ -1,116 +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.car.dialer;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothHeadsetClient;
-import android.bluetooth.BluetoothProfile;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.util.Log;
-
-import java.util.List;
-import java.util.concurrent.CopyOnWriteArrayList;
-
-/**
- * Class that responsible for getting status of bluetooth connections.
- */
-public class UiBluetoothMonitor {
- private static String TAG = "Em.BtMonitor";
-
- private List<Listener> mListeners = new CopyOnWriteArrayList<>();
- private final Context mContext;
- private final BluetoothAdapter mBluetoothAdapter;
- private final BluetoothBroadcastReceiver mBluetoothBroadcastReceiver;
-
- UiBluetoothMonitor(Context context) {
- mContext = context;
- mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
- mBluetoothBroadcastReceiver = new BluetoothBroadcastReceiver();
- }
-
- public void tearDown() {
- mBluetoothBroadcastReceiver.tearDown();
- }
-
- public boolean isBluetoothEnabled() {
- return mBluetoothAdapter != null && mBluetoothAdapter.isEnabled();
- }
-
- public boolean isHfpConnected() {
- if (mBluetoothAdapter == null) {
- return false;
- }
- return mBluetoothAdapter.getProfileConnectionState(BluetoothProfile.HEADSET_CLIENT)
- == BluetoothProfile.STATE_CONNECTED;
- }
- public boolean isBluetoothPaired() {
- return mBluetoothAdapter != null && mBluetoothAdapter.getBondedDevices().size() > 0;
- }
-
- public void addListener(Listener listener) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "addListener: " + listener);
- }
- mListeners.add(listener);
- }
-
- protected void notifyListeners() {
- for (Listener listener : mListeners) {
- listener.onStateChanged();
- }
- }
-
- public void removeListener(Listener listener) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "removeListener: " + listener);
- }
- mListeners.remove(listener);
- }
-
- public interface Listener {
- /**
- * Calls when state of Bluetooth was changed, for example when Bluetooth was turned off or
- * on, connection state was changed.
- */
- void onStateChanged();
- }
-
- private final class BluetoothBroadcastReceiver extends BroadcastReceiver {
- BluetoothBroadcastReceiver() {
- IntentFilter filter = new IntentFilter();
- filter.addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED);
- filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
- filter.addAction(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED);
- mContext.registerReceiver(this, filter);
- }
-
- @Override
- public void onReceive(Context context, Intent intent) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Bluetooth broadcast intent action received: " + intent.getAction());
- }
- notifyListeners();
- }
-
- void tearDown() {
- mContext.unregisterReceiver(this);
- }
- }
-}
diff --git a/src/com/android/car/dialer/livedata/AudioRouteLiveData.java b/src/com/android/car/dialer/livedata/AudioRouteLiveData.java
new file mode 100644
index 00000000..d2aaa18d
--- /dev/null
+++ b/src/com/android/car/dialer/livedata/AudioRouteLiveData.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.livedata;
+
+import android.bluetooth.BluetoothHeadsetClient;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+
+import androidx.lifecycle.LiveData;
+
+import com.android.car.dialer.log.L;
+import com.android.car.dialer.telecom.UiCallManager;
+
+/**
+ * Provides the current connecting audio route.
+ */
+public class AudioRouteLiveData extends LiveData<Integer> {
+ private static final String TAG = "CD.AudioRouteLiveData";
+
+ private final Context mContext;
+ private final IntentFilter mAudioRouteChangeFilter;
+
+ private final BroadcastReceiver mAudioRouteChangeReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ updateAudioRoute();
+ }
+ };
+
+ public AudioRouteLiveData(Context context) {
+ mContext = context;
+ mAudioRouteChangeFilter =
+ new IntentFilter(BluetoothHeadsetClient.ACTION_AUDIO_STATE_CHANGED);
+ }
+
+ @Override
+ protected void onActive() {
+ updateAudioRoute();
+ mContext.registerReceiver(mAudioRouteChangeReceiver, mAudioRouteChangeFilter);
+ }
+
+ @Override
+ protected void onInactive() {
+ mContext.unregisterReceiver(mAudioRouteChangeReceiver);
+ }
+
+ private void updateAudioRoute() {
+ int audioRoute = UiCallManager.get().getAudioRoute();
+ if (getValue() == null || audioRoute != getValue()) {
+ L.d(TAG, "updateAudioRoute to %s", audioRoute);
+ setValue(audioRoute);
+ }
+ }
+}
diff --git a/src/com/android/car/dialer/livedata/BluetoothHfpStateLiveData.java b/src/com/android/car/dialer/livedata/BluetoothHfpStateLiveData.java
new file mode 100644
index 00000000..4257d59f
--- /dev/null
+++ b/src/com/android/car/dialer/livedata/BluetoothHfpStateLiveData.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.livedata;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothHeadsetClient;
+import android.bluetooth.BluetoothProfile;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+
+import com.android.car.dialer.log.L;
+
+import androidx.lifecycle.LiveData;
+
+/**
+ * Provides the connectivity state of HFP Bluetooth profile. States can be one of:
+ * <ul>
+ * <li>{@link BluetoothProfile#STATE_DISCONNECTED},
+ * <li>{@link BluetoothProfile#STATE_CONNECTING},
+ * <li>{@link BluetoothProfile#STATE_CONNECTED},
+ * <li>{@link BluetoothProfile#STATE_DISCONNECTING}
+ * </ul>
+ */
+public class BluetoothHfpStateLiveData extends LiveData<Integer> {
+ private static final String TAG = "CD.BluetoothHfpStateLiveData";
+
+ private final BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+ private final Context mContext;
+ private final IntentFilter mIntentFilter = new IntentFilter();
+
+ private BroadcastReceiver mBluetoothStateReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ updateState();
+ }
+ };
+
+ /** Creates a new {@link BluetoothHfpStateLiveData}. Call on main thread. */
+ public BluetoothHfpStateLiveData(Context context) {
+ mContext = context;
+ mIntentFilter.addAction(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED);
+ }
+
+ @Override
+ protected void onActive() {
+ if (mBluetoothAdapter != null) {
+ updateState();
+ mContext.registerReceiver(mBluetoothStateReceiver, mIntentFilter);
+ }
+ }
+
+ @Override
+ protected void onInactive() {
+ if (mBluetoothAdapter != null) {
+ mContext.unregisterReceiver(mBluetoothStateReceiver);
+ }
+ }
+
+ private void updateState() {
+ int state = mBluetoothAdapter.getProfileConnectionState(
+ BluetoothProfile.HEADSET_CLIENT);
+ if (getValue() == null || state != getValue()) {
+ L.d(TAG, "updateState to %s", state);
+ setValue(state);
+ }
+ }
+}
diff --git a/src/com/android/car/dialer/livedata/BluetoothPairListLiveData.java b/src/com/android/car/dialer/livedata/BluetoothPairListLiveData.java
new file mode 100644
index 00000000..bc6d3a28
--- /dev/null
+++ b/src/com/android/car/dialer/livedata/BluetoothPairListLiveData.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.livedata;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+
+import androidx.annotation.MainThread;
+import androidx.lifecycle.LiveData;
+
+import com.android.car.dialer.log.L;
+
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * Provides a list of paired Bluetooth devices.
+ */
+public class BluetoothPairListLiveData extends LiveData<Set<BluetoothDevice>> {
+ private static final String TAG = "CD.BluetoothPairListLiveData";
+
+ private final BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+ private final Context mContext;
+ private final IntentFilter mIntentFilter = new IntentFilter();
+
+ private BroadcastReceiver mBluetoothPairListReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ updateList();
+ }
+ };
+
+ /** Creates a new {@link BluetoothPairListLiveData}. Call on main thread. */
+ @MainThread
+ public BluetoothPairListLiveData(Context context) {
+ mContext = context;
+ mIntentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
+ }
+
+ @Override
+ protected void onActive() {
+ if (mBluetoothAdapter != null) {
+ updateList();
+ mContext.registerReceiver(mBluetoothPairListReceiver, mIntentFilter);
+ }
+ }
+
+ @Override
+ protected void onInactive() {
+ if (mBluetoothAdapter != null) {
+ mContext.unregisterReceiver(mBluetoothPairListReceiver);
+ }
+ }
+
+ private void updateList() {
+ Set<BluetoothDevice> devices = mBluetoothAdapter.getBondedDevices();
+ L.d(TAG, "updateList to %s", devices);
+ setValue(devices);
+ }
+}
diff --git a/src/com/android/car/dialer/livedata/BluetoothStateLiveData.java b/src/com/android/car/dialer/livedata/BluetoothStateLiveData.java
new file mode 100644
index 00000000..b8de0a10
--- /dev/null
+++ b/src/com/android/car/dialer/livedata/BluetoothStateLiveData.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.livedata;
+
+import android.bluetooth.BluetoothAdapter;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+
+import androidx.annotation.IntDef;
+import androidx.lifecycle.LiveData;
+
+import com.android.car.dialer.log.L;
+
+/**
+ * Provides the device Bluetooth availability. Updates client with {@link BluetoothState}.
+ */
+public class BluetoothStateLiveData extends LiveData<Integer> {
+ private static final String TAG = "CD.BluetoothStateLiveData";
+
+ @IntDef({
+ BluetoothState.UNKNOWN,
+ BluetoothState.DISABLED,
+ BluetoothState.ENABLED,
+ })
+ public @interface BluetoothState {
+ /** Bluetooth is not supported on the current device */
+ int UNKNOWN = 0;
+ /** Bluetooth is disabled */
+ int DISABLED = 1;
+ /** Bluetooth is enabled */
+ int ENABLED = 2;
+ }
+
+ private final BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+ private final Context mContext;
+ private final IntentFilter mIntentFilter = new IntentFilter();
+
+ private BroadcastReceiver mBluetoothStateReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ updateState();
+ }
+ };
+
+ /** Creates a new {@link BluetoothStateLiveData}. Call on main thread. */
+ public BluetoothStateLiveData(Context context) {
+ mContext = context;
+ mIntentFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
+ }
+
+ @Override
+ protected void onActive() {
+ if (mBluetoothAdapter != null) {
+ updateState();
+ mContext.registerReceiver(mBluetoothStateReceiver, mIntentFilter);
+ }
+
+ }
+
+ @Override
+ protected void onInactive() {
+ if (mBluetoothAdapter != null) {
+ mContext.unregisterReceiver(mBluetoothStateReceiver);
+ }
+ }
+
+ private void updateState() {
+ @BluetoothState int state = BluetoothState.UNKNOWN;
+ if (mBluetoothAdapter != null) {
+ state = mBluetoothAdapter.isEnabled() ? BluetoothState.ENABLED
+ : BluetoothState.DISABLED;
+ }
+
+ if (getValue() == null || state != getValue()) {
+ L.d(TAG, "updateState to %s", state);
+ setValue(state);
+ }
+ }
+}
diff --git a/src/com/android/car/dialer/livedata/CallDetailLiveData.java b/src/com/android/car/dialer/livedata/CallDetailLiveData.java
new file mode 100644
index 00000000..4d818016
--- /dev/null
+++ b/src/com/android/car/dialer/livedata/CallDetailLiveData.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.livedata;
+
+import android.telecom.Call;
+import android.telecom.InCallService;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LiveData;
+
+import com.android.car.telephony.common.CallDetail;
+
+import java.util.List;
+
+/**
+ * Represents the details of an active phone call.
+ */
+public class CallDetailLiveData extends LiveData<CallDetail> {
+
+ private final Call mTelecomCall;
+
+ public CallDetailLiveData(@NonNull Call telecomCall) {
+ mTelecomCall = telecomCall;
+ }
+
+ @Override
+ protected void onActive() {
+ super.onActive();
+ setTelecomCallDetail(mTelecomCall);
+ mTelecomCall.registerCallback(mCallback);
+ }
+
+ @Override
+ protected void onInactive() {
+ super.onInactive();
+ mTelecomCall.unregisterCallback(mCallback);
+ }
+
+ private Call.Callback mCallback = new Call.Callback() {
+ @Override
+ public void onStateChanged(Call telecomCall, int state) {
+ // no ops
+ }
+
+ @Override
+ public void onParentChanged(Call telecomCall, Call parent) {
+ // no ops
+ }
+
+ @Override
+ public void onCallDestroyed(Call telecomCall) {
+ // no ops
+ }
+
+ @Override
+ public void onDetailsChanged(Call telecomCall, Call.Details details) {
+ setTelecomCallDetail(mTelecomCall);
+ }
+
+ @Override
+ public void onVideoCallChanged(Call telecomCall, InCallService.VideoCall videoCall) {
+ // no ops
+ }
+
+ @Override
+ public void onCannedTextResponsesLoaded(Call telecomCall,
+ List<String> cannedTextResponses) {
+ // no ops
+ }
+
+ @Override
+ public void onChildrenChanged(Call telecomCall, List<Call> children) {
+ // no ops
+ }
+ };
+
+ private void setTelecomCallDetail(Call telecomCall) {
+ setValue(CallDetail.fromTelecomCallDetail(telecomCall.getDetails()));
+ }
+}
diff --git a/src/com/android/car/dialer/livedata/CallHistoryLiveData.java b/src/com/android/car/dialer/livedata/CallHistoryLiveData.java
index fd34c03b..857a53ee 100644
--- a/src/com/android/car/dialer/livedata/CallHistoryLiveData.java
+++ b/src/com/android/car/dialer/livedata/CallHistoryLiveData.java
@@ -13,79 +13,107 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package com.android.car.dialer.livedata;
-import android.content.ContentResolver;
+import static com.android.car.dialer.livedata.CallHistoryLiveData.CallType.CALL_TYPE_ALL;
+
import android.content.Context;
-import android.content.CursorLoader;
-import android.database.ContentObserver;
+import android.database.Cursor;
import android.net.Uri;
-import android.os.Handler;
+import android.provider.CallLog;
-import com.android.car.dialer.telecom.PhoneLoader;
-import com.android.car.dialer.ui.CallLogListingTask;
+import androidx.annotation.IntDef;
-import java.util.List;
+import com.android.car.telephony.common.AsyncQueryLiveData;
+import com.android.car.telephony.common.PhoneCallLog;
+import com.android.car.telephony.common.QueryParam;
-import androidx.lifecycle.LiveData;
+import java.util.ArrayList;
+import java.util.List;
/**
* Live data which loads call history.
*/
-public class CallHistoryLiveData extends LiveData<List<CallLogListingTask.CallLogItem>> {
+//TODO: Rename to PhoneCallLogLiveData
+public class CallHistoryLiveData extends AsyncQueryLiveData<List<PhoneCallLog>> {
+ /** The default limit of loading call logs */
+ private final static int DEFAULT_CALL_LOG_LIMIT = 100;
+ private static final String[] EMPTY_STRING_ARRAY = new String[0];
- private final Context mContext;
- private final ContentResolver mContentResolver;
- private CursorLoader mCursorLoader;
- private CallLogContentObserver mCallLogContentObserver = new CallLogContentObserver(
- new Handler());
-
- public CallHistoryLiveData(Context context) {
- this.mContext = context;
- mContentResolver = context.getContentResolver();
+ @IntDef({
+ CALL_TYPE_ALL,
+ CallType.INCOMING_TYPE,
+ CallType.OUTGOING_TYPE,
+ CallType.MISSED_TYPE,
+ })
+ public @interface CallType {
+ int CALL_TYPE_ALL = -1;
+ int INCOMING_TYPE = CallLog.Calls.INCOMING_TYPE;
+ int OUTGOING_TYPE = CallLog.Calls.OUTGOING_TYPE;
+ int MISSED_TYPE = CallLog.Calls.MISSED_TYPE;
+ int VOICEMAIL_TYPE = CallLog.Calls.VOICEMAIL_TYPE;
}
- @Override
- protected void onActive() {
- super.onActive();
- mCursorLoader = PhoneLoader.registerCallObserver(getCallHistoryFilterType(),
- mContext,
- (loader, cursor) -> {
- CallLogListingTask task = new CallLogListingTask(mContext, cursor,
- this::setValue);
- task.execute();
- });
-
- mContentResolver.registerContentObserver(mCursorLoader.getUri(),
- false, mCallLogContentObserver);
+ /**
+ * Creates a new instance of call history live data which loads all types of call history
+ * with a limit of 100 logs.
+ */
+ public static CallHistoryLiveData newInstance(Context context) {
+ return newInstance(context, CALL_TYPE_ALL, DEFAULT_CALL_LOG_LIMIT);
}
- @Override
- protected void onInactive() {
- super.onInactive();
- mContentResolver.unregisterContentObserver(mCallLogContentObserver);
+ /**
+ * Returns a new instance of last call live data.
+ */
+ public static CallHistoryLiveData newLastCallLiveData(Context context) {
+ return newInstance(context, CALL_TYPE_ALL, 1);
}
- protected int getCallHistoryFilterType() {
- return PhoneLoader.CALL_TYPE_ALL;
- }
+ private static CallHistoryLiveData newInstance(Context context, int callType, int limit) {
+ StringBuilder where = new StringBuilder();
+ List<String> selectionArgs = new ArrayList<>();
+ limit = limit < 0 ? 0 : limit;
- /**
- * A {@link ContentObserver} that is responsible for reloading the user's recent calls.
- */
- private class CallLogContentObserver extends ContentObserver {
- public CallLogContentObserver(Handler handler) {
- super(handler);
+ if (callType != CALL_TYPE_ALL) {
+ // add a filter for call type
+ where.append(String.format("(%s = ?)", CallLog.Calls.TYPE));
+ selectionArgs.add(Integer.toString(callType));
}
+ String selection = where.length() > 0 ? where.toString() : null;
- @Override
- public void onChange(boolean selfChange) {
- onChange(selfChange, null);
- }
+ Uri uri = CallLog.Calls.CONTENT_URI.buildUpon()
+ .appendQueryParameter(CallLog.Calls.LIMIT_PARAM_KEY,
+ Integer.toString(limit))
+ .build();
+ QueryParam queryParam = new QueryParam(
+ uri,
+ null,
+ selection,
+ selectionArgs.toArray(EMPTY_STRING_ARRAY),
+ CallLog.Calls.DEFAULT_SORT_ORDER);
+ return new CallHistoryLiveData(context, queryParam);
+ }
+
+ private final Context mContext;
+ private CallHistoryLiveData(Context context, QueryParam queryParam) {
+ super(context, QueryParam.of(queryParam));
+ mContext = context;
+ }
+
+ @Override
+ protected List<PhoneCallLog> convertToEntity(Cursor cursor) {
+ List<PhoneCallLog> resultList = new ArrayList<>();
+
+ while (cursor.moveToNext()) {
+ PhoneCallLog phoneCallLog = PhoneCallLog.fromCursor(mContext, cursor);
+ PhoneCallLog previousCallLog = resultList.isEmpty() ? null : resultList.get(
+ resultList.size() - 1);
- @Override
- public void onChange(boolean selfChange, Uri uri) {
- mCursorLoader.startLoading();
+ if (previousCallLog == null || !previousCallLog.merge(phoneCallLog)) {
+ resultList.add(phoneCallLog);
+ }
}
+ return resultList;
}
}
diff --git a/src/com/android/car/dialer/livedata/CallStateLiveData.java b/src/com/android/car/dialer/livedata/CallStateLiveData.java
new file mode 100644
index 00000000..af2faf9d
--- /dev/null
+++ b/src/com/android/car/dialer/livedata/CallStateLiveData.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.livedata;
+
+import android.telecom.Call;
+import android.telecom.InCallService;
+
+import androidx.lifecycle.LiveData;
+
+import java.util.List;
+
+/**
+ * Represents an active phone call state.
+ */
+public class CallStateLiveData extends LiveData<Integer> {
+
+ private final Call mTelecomCall;
+
+ public CallStateLiveData(Call telecomCall) {
+ mTelecomCall = telecomCall;
+ }
+
+ @Override
+ protected void onActive() {
+ super.onActive();
+
+ setValue(mTelecomCall.getState());
+ mTelecomCall.registerCallback(mCallback);
+ }
+
+ @Override
+ protected void onInactive() {
+ super.onInactive();
+ mTelecomCall.unregisterCallback(mCallback);
+ }
+
+ private Call.Callback mCallback = new Call.Callback() {
+ @Override
+ public void onStateChanged(Call telecomCall, int state) {
+ setValue(state);
+ }
+
+ @Override
+ public void onParentChanged(Call telecomCall, Call parent) {
+ // no ops
+ }
+
+ @Override
+ public void onCallDestroyed(Call telecomCall) {
+ // no ops
+ }
+
+ @Override
+ public void onDetailsChanged(Call telecomCall, Call.Details details) {
+ // no ops
+ }
+
+ @Override
+ public void onVideoCallChanged(Call telecomCall, InCallService.VideoCall videoCall) {
+ // no ops
+ }
+
+ @Override
+ public void onCannedTextResponsesLoaded(Call telecomCall,
+ List<String> cannedTextResponses) {
+ // no ops
+ }
+
+ @Override
+ public void onChildrenChanged(Call telecomCall, List<Call> children) {
+ // no ops
+ }
+ };
+}
diff --git a/src/com/android/car/dialer/livedata/ContactDetailsLiveData.java b/src/com/android/car/dialer/livedata/ContactDetailsLiveData.java
new file mode 100644
index 00000000..d708025e
--- /dev/null
+++ b/src/com/android/car/dialer/livedata/ContactDetailsLiveData.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.livedata;
+
+import android.content.ContentUris;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.car.telephony.common.AsyncQueryLiveData;
+import com.android.car.telephony.common.Contact;
+import com.android.car.telephony.common.QueryParam;
+
+/** {@link androidx.lifecycle.LiveData} for contact details that observes the contact change. */
+public class ContactDetailsLiveData extends AsyncQueryLiveData<Contact> {
+ private final Context mContext;
+
+ public ContactDetailsLiveData(Context context, @NonNull Uri contactLookupUri) {
+ super(context, new ContactDetailsQueryParamProvider(contactLookupUri, context));
+ mContext = context;
+ }
+
+ @Override
+ protected Contact convertToEntity(Cursor cursor) {
+ // Contact is not deleted.
+ if (cursor.moveToFirst()) {
+ Contact contact = Contact.fromCursor(mContext, cursor);
+ while (cursor.moveToNext()) {
+ contact.merge(Contact.fromCursor(mContext, cursor));
+ }
+ return contact;
+ }
+ return null;
+ }
+
+ /**
+ * Contact id varies on contact change. When we start a new query, this {@link
+ * QueryParam.Provider} refreshes the contact lookup uri to get the most up to date contact id
+ * and creates a new {@link QueryParam}.
+ */
+ private static class ContactDetailsQueryParamProvider implements QueryParam.Provider {
+
+ private final Context mContext;
+ private final Uri mContactLookupUri;
+
+ public ContactDetailsQueryParamProvider(Uri contactLookupUri, Context context) {
+ mContactLookupUri = contactLookupUri;
+ mContext = context;
+ }
+
+ @Nullable
+ @Override
+ public QueryParam getQueryParam() {
+ Uri refreshedContactLookupUri = ContactsContract.Contacts.getLookupUri(
+ mContext.getContentResolver(), mContactLookupUri);
+ return convertToQueryParam(refreshedContactLookupUri);
+ }
+
+ /**
+ * Build the query param from the given contact lookup uri. Caller is responsible for
+ * passing in the most up to date uri.
+ *
+ * @param contactLookupUri Up to date uri describing the requested {@link Contact} entry.
+ * When contact is deleted, the uri will be null.
+ */
+ @Nullable
+ private static QueryParam convertToQueryParam(@Nullable Uri contactLookupUri) {
+ if (contactLookupUri == null) {
+ return null;
+ }
+ long contactId = ContentUris.parseId(contactLookupUri);
+ return new QueryParam(
+ ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
+ /* projection= */null,
+ /* selection= */ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = ?",
+ new String[]{String.valueOf(contactId)},
+ /* orderBy= */null);
+ }
+ }
+}
diff --git a/src/com/android/car/dialer/livedata/FavoriteContactLiveData.java b/src/com/android/car/dialer/livedata/FavoriteContactLiveData.java
new file mode 100644
index 00000000..800ee81b
--- /dev/null
+++ b/src/com/android/car/dialer/livedata/FavoriteContactLiveData.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.livedata;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.provider.ContactsContract;
+
+import com.android.car.telephony.common.AsyncQueryLiveData;
+import com.android.car.telephony.common.Contact;
+import com.android.car.telephony.common.QueryParam;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Live data which loads starred contact list.
+ */
+public class FavoriteContactLiveData extends AsyncQueryLiveData<List<Contact>> {
+ private static final int IS_STARRED = 1;
+ private final Context mContext;
+
+ /**
+ * Creates a new instance of {@link FavoriteContactLiveData}.
+ */
+ public static FavoriteContactLiveData newInstance(Context context) {
+ String selection = ContactsContract.Data.MIMETYPE + " = ?"
+ + " and "
+ + ContactsContract.Data.STARRED + " = ?";
+ String[] selectionArgs = new String[2];
+ selectionArgs[0] = ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE;
+ selectionArgs[1] = String.valueOf(IS_STARRED);
+
+ QueryParam starredContactsQueryParam =
+ new QueryParam(
+ ContactsContract.Data.CONTENT_URI,
+ null,
+ selection,
+ selectionArgs,
+ ContactsContract.Contacts.DISPLAY_NAME + " ASC ");
+ return new FavoriteContactLiveData(context, starredContactsQueryParam);
+ }
+
+ private FavoriteContactLiveData(Context context, QueryParam queryParam) {
+ super(context, QueryParam.of(queryParam));
+ mContext = context;
+ }
+
+ @Override
+ protected List<Contact> convertToEntity(Cursor cursor) {
+ Map<String, Contact> result = new LinkedHashMap<>();
+ while (cursor.moveToNext()) {
+ Contact contact = Contact.fromCursor(mContext, cursor);
+ String lookupKey = contact.getLookupKey();
+ if (result.containsKey(lookupKey)) {
+ Contact existingContact = result.get(lookupKey);
+ existingContact.merge(contact);
+ } else {
+ result.put(lookupKey, contact);
+ }
+ }
+ return new ArrayList<>(result.values());
+ }
+}
diff --git a/src/com/android/car/dialer/livedata/HeartBeatLiveData.java b/src/com/android/car/dialer/livedata/HeartBeatLiveData.java
new file mode 100644
index 00000000..d29ed396
--- /dev/null
+++ b/src/com/android/car/dialer/livedata/HeartBeatLiveData.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.livedata;
+
+import android.os.Handler;
+import android.os.Looper;
+
+import androidx.lifecycle.LiveData;
+
+/**
+ * Emits a true value in a fixed periodical pace. The first beat begins when this live data becomes
+ * active.
+ *
+ * <p> Note that if this heart beat is shared, the time can be less than the given interval between
+ * observation and first beat for the second observer.
+ */
+public class HeartBeatLiveData extends LiveData<Boolean> {
+ private long mPulseRate;
+ private Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
+
+ public HeartBeatLiveData(long rateInMillis) {
+ mPulseRate = rateInMillis;
+ }
+
+ @Override
+ protected void onActive() {
+ super.onActive();
+ mMainThreadHandler.post(mUpdateDurationRunnable);
+ }
+
+ @Override
+ protected void onInactive() {
+ super.onInactive();
+ mMainThreadHandler.removeCallbacks(mUpdateDurationRunnable);
+ }
+
+ private final Runnable mUpdateDurationRunnable = new Runnable() {
+ @Override
+ public void run() {
+ setValue(true);
+ mMainThreadHandler.postDelayed(this, mPulseRate);
+ }
+ };
+}
diff --git a/src/com/android/car/dialer/livedata/SharedPreferencesLiveData.java b/src/com/android/car/dialer/livedata/SharedPreferencesLiveData.java
new file mode 100644
index 00000000..13ae0493
--- /dev/null
+++ b/src/com/android/car/dialer/livedata/SharedPreferencesLiveData.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.livedata;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.text.TextUtils;
+
+import androidx.lifecycle.LiveData;
+import androidx.preference.PreferenceManager;
+
+import com.android.car.dialer.log.L;
+
+/**
+ * Provides SharedPreferences.
+ */
+public class SharedPreferencesLiveData extends LiveData<SharedPreferences> {
+ private static final String TAG = "CD.PreferenceLiveData";
+
+ private final SharedPreferences mSharedPreferences;
+ private final String mKey;
+
+ private final SharedPreferences.OnSharedPreferenceChangeListener
+ mOnSharedPreferenceChangeListener;
+
+ public SharedPreferencesLiveData(Context context, String key) {
+ mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+ mKey = key;
+
+ mOnSharedPreferenceChangeListener = (sharedPreferences, k) -> {
+ if (TextUtils.equals(k, mKey)) {
+ updateSharedPreferences();
+ }
+ };
+ }
+
+ @Override
+ protected void onActive() {
+ updateSharedPreferences();
+ mSharedPreferences.registerOnSharedPreferenceChangeListener(
+ mOnSharedPreferenceChangeListener);
+ }
+
+ @Override
+ protected void onInactive() {
+ mSharedPreferences.unregisterOnSharedPreferenceChangeListener(
+ mOnSharedPreferenceChangeListener);
+ }
+
+ private void updateSharedPreferences() {
+ L.i(TAG, "Update SharedPreferences");
+ setValue(mSharedPreferences);
+ }
+
+ /**
+ * Returns the monitored shared preference key.
+ */
+ public String getKey() {
+ return mKey;
+ }
+}
diff --git a/src/com/android/car/dialer/livedata/UnreadMissedCallLiveData.java b/src/com/android/car/dialer/livedata/UnreadMissedCallLiveData.java
new file mode 100644
index 00000000..08f3910a
--- /dev/null
+++ b/src/com/android/car/dialer/livedata/UnreadMissedCallLiveData.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.livedata;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.provider.CallLog;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LiveData;
+
+import com.android.car.telephony.common.AsyncQueryLiveData;
+import com.android.car.telephony.common.PhoneCallLog;
+import com.android.car.telephony.common.QueryParam;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/** {@link LiveData} for missed calls that haven't been read by user. */
+public class UnreadMissedCallLiveData extends AsyncQueryLiveData<List<PhoneCallLog>> {
+ private static final String[] EMPTY_STRING_ARRAY = new String[0];
+
+ /** Get the {@link UnreadMissedCallLiveData} instance. */
+ public static UnreadMissedCallLiveData newInstance(Context context) {
+ StringBuilder where = new StringBuilder();
+ List<String> selectionArgs = new ArrayList<>();
+ where.append(String.format("(%s = ?)", CallLog.Calls.TYPE));
+ where.append(String.format("AND (%s = 1)", CallLog.Calls.NEW));
+ where.append(String.format("AND (%s IS NOT 1)", CallLog.Calls.IS_READ));
+ selectionArgs.add(Integer.toString(CallLog.Calls.MISSED_TYPE));
+
+ String selection = where.length() > 0 ? where.toString() : null;
+
+ QueryParam queryParam = new QueryParam(
+ CallLog.Calls.CONTENT_URI,
+ null,
+ selection,
+ selectionArgs.toArray(EMPTY_STRING_ARRAY),
+ CallLog.Calls.DEFAULT_SORT_ORDER);
+ return new UnreadMissedCallLiveData(context, queryParam);
+ }
+
+ private final Context mContext;
+
+ private UnreadMissedCallLiveData(Context context, QueryParam queryParam) {
+ super(context, QueryParam.of(queryParam));
+ setValue(Collections.EMPTY_LIST);
+ mContext = context;
+ }
+
+ @NonNull
+ @Override
+ protected List<PhoneCallLog> convertToEntity(@NonNull Cursor cursor) {
+ List<PhoneCallLog> missedCalls = new ArrayList<>();
+
+ while (cursor.moveToNext()) {
+ PhoneCallLog phoneCallLog = PhoneCallLog.fromCursor(mContext, cursor);
+ int index = missedCalls.indexOf(phoneCallLog);
+ PhoneCallLog existingCallLog = null;
+ if (index != -1) {
+ existingCallLog = missedCalls.get(index);
+ }
+
+ if (existingCallLog == null || !existingCallLog.merge(phoneCallLog)) {
+ missedCalls.add(phoneCallLog);
+ }
+ }
+ return missedCalls;
+ }
+}
diff --git a/src/com/android/car/dialer/log/L.java b/src/com/android/car/dialer/log/L.java
index f85a51aa..55860dab 100644
--- a/src/com/android/car/dialer/log/L.java
+++ b/src/com/android/car/dialer/log/L.java
@@ -13,58 +13,78 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package com.android.car.dialer.log;
+import android.os.Build;
import android.util.Log;
+import androidx.annotation.NonNull;
+
/**
* Util class for logging.
*/
public class L {
- private String mTag;
-
- public L(String tag) {
- mTag = tag;
- }
-
- public void v(String msg) {
- if (Log.isLoggable(mTag, Log.VERBOSE)) {
- Log.v(mTag, msg);
+ /**
+ * Logs verbose level logs if loggable.
+ *
+ * <p>@see String#format(String, Object...) for formatting log string.
+ */
+ public static void v(String tag, @NonNull String msg, Object... args) {
+ if (Log.isLoggable(tag, Log.VERBOSE) || Build.IS_DEBUGGABLE) {
+ Log.v(tag, String.format(msg, args));
}
}
- public void d(String msg) {
- if (Log.isLoggable(mTag, Log.DEBUG)) {
- Log.d(mTag, msg);
+ /**
+ * Logs debug level logs if loggable.
+ *
+ * <p>@see String#format(String, Object...) for formatting log string.
+ */
+ public static void d(String tag, @NonNull String msg, Object... args) {
+ if (Log.isLoggable(tag, Log.DEBUG) || Build.IS_DEBUGGABLE) {
+ Log.d(tag, String.format(msg, args));
}
}
- public void w(String msg) {
- Log.w(mTag, msg);
- }
-
- public static L logger(String tag) {
- return new L(tag);
- }
-
- public static void v(String tag, String msg) {
- if (Log.isLoggable(tag, Log.VERBOSE)) {
- Log.v(tag, msg);
+ /**
+ * Logs info level logs if loggable.
+ *
+ * <p>@see String#format(String, Object...) for formatting log string.
+ */
+ public static void i(String tag, @NonNull String msg, Object... args) {
+ if (Log.isLoggable(tag, Log.INFO) || Build.IS_DEBUGGABLE) {
+ Log.i(tag, String.format(msg, args));
}
}
- public static void d(String tag, String msg) {
- if (Log.isLoggable(tag, Log.DEBUG)) {
- Log.d(tag, msg);
+ /**
+ * Logs warning level logs if loggable.
+ *
+ * <p>@see String#format(String, Object...) for formatting log string.
+ */
+ public static void w(String tag, @NonNull String msg, Object... args) {
+ if (Log.isLoggable(tag, Log.WARN) || Build.IS_DEBUGGABLE) {
+ Log.w(tag, String.format(msg, args));
}
}
- public static void w(String tag, String msg) {
- Log.w(tag, msg);
+ /**
+ * Logs error level logs.
+ *
+ * <p>@see String#format(String, Object...) for formatting log string.
+ */
+ public static void e(String tag, @NonNull String msg, Object... args) {
+ Log.e(tag, String.format(msg, args));
}
- public static void i(String tag, String msg) {
- Log.i(tag, msg);
+ /**
+ * Logs warning level logs.
+ *
+ * <p>@see String#format(String, Object...) for formatting log string.
+ */
+ public static void e(String tag, Exception e, @NonNull String msg, Object... args) {
+ Log.e(tag, String.format(msg, args), e);
}
}
diff --git a/src/com/android/car/dialer/notification/InCallNotificationController.java b/src/com/android/car/dialer/notification/InCallNotificationController.java
new file mode 100644
index 00000000..2d778c76
--- /dev/null
+++ b/src/com/android/car/dialer/notification/InCallNotificationController.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.notification;
+
+import android.annotation.TargetApi;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.drawable.Icon;
+import android.telecom.Call;
+
+import androidx.annotation.StringRes;
+import androidx.core.util.Pair;
+
+import com.android.car.dialer.Constants;
+import com.android.car.dialer.R;
+import com.android.car.dialer.log.L;
+import com.android.car.dialer.ui.activecall.InCallActivity;
+import com.android.car.telephony.common.CallDetail;
+
+/** Controller that manages the heads up notification for incoming calls. */
+public final class InCallNotificationController {
+ private static final String TAG = "CD.InCallNotificationController";
+ private static final String CHANNEL_ID = "com.android.car.dialer.incoming";
+ // A random number that is used for notification id.
+ private static final int NOTIFICATION_ID = 20181105;
+
+ private static InCallNotificationController sInCallNotificationController;
+
+ /**
+ * Initialized a globally accessible {@link InCallNotificationController} which can be retrieved
+ * by {@link #get}. If this function is called a second time before calling {@link #tearDown()},
+ * an {@link IllegalStateException} will be thrown.
+ *
+ * @param applicationContext Application context.
+ */
+ public static void init(Context applicationContext) {
+ if (sInCallNotificationController == null) {
+ sInCallNotificationController = new InCallNotificationController(applicationContext);
+ } else {
+ throw new IllegalStateException("InCallNotificationController has been initialized.");
+ }
+ }
+
+ /**
+ * Gets the global {@link InCallNotificationController} instance. Make sure
+ * {@link #init(Context)} is called before calling this method.
+ */
+ public static InCallNotificationController get() {
+ if (sInCallNotificationController == null) {
+ throw new IllegalStateException(
+ "Call InCallNotificationController.init(Context) before calling this function");
+ }
+ return sInCallNotificationController;
+ }
+
+ public static void tearDown() {
+ sInCallNotificationController = null;
+ }
+
+ private final Context mContext;
+ private final NotificationManager mNotificationManager;
+
+ @TargetApi(26)
+ private InCallNotificationController(Context context) {
+ mContext = context;
+ mNotificationManager =
+ (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+
+ CharSequence name = mContext.getString(R.string.in_call_notification_channel_name);
+ NotificationChannel notificationChannel = new NotificationChannel(CHANNEL_ID, name,
+ NotificationManager.IMPORTANCE_HIGH);
+ mNotificationManager.createNotificationChannel(notificationChannel);
+ }
+
+
+ /** Show a new incoming call notification or update the existing incoming call notification. */
+ @TargetApi(26)
+ public void showInCallNotification(Call call) {
+ L.d(TAG, "showInCallNotification");
+ CallDetail callDetail = CallDetail.fromTelecomCallDetail(call.getDetails());
+ String number = callDetail.getNumber();
+ Pair<String, Icon> displayNameAndRoundedAvatar =
+ NotificationUtils.getDisplayNameAndRoundedAvatar(mContext, number);
+
+ Intent intent = new Intent(mContext, InCallActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.putExtra(Constants.Intents.EXTRA_SHOW_INCOMING_CALL, true);
+ PendingIntent fullscreenIntent = PendingIntent.getActivity(mContext, 0, intent,
+ PendingIntent.FLAG_UPDATE_CURRENT);
+
+ Notification.Builder builder = new Notification.Builder(mContext, CHANNEL_ID)
+ .setSmallIcon(R.drawable.ic_phone)
+ .setLargeIcon(displayNameAndRoundedAvatar.second)
+ .setContentTitle(displayNameAndRoundedAvatar.first)
+ .setContentText(mContext.getString(R.string.notification_incoming_call))
+ .setFullScreenIntent(fullscreenIntent, /* highPriority= */true)
+ .setCategory(Notification.CATEGORY_CALL)
+ .addAction(getAction(call, R.string.answer_call,
+ NotificationService.ACTION_ANSWER_CALL))
+ .addAction(getAction(call, R.string.decline_call,
+ NotificationService.ACTION_DECLINE_CALL))
+ .setOngoing(true)
+ .setAutoCancel(false);
+
+ mNotificationManager.notify(
+ call.getDetails().getTelecomCallId(),
+ NOTIFICATION_ID,
+ builder.build());
+ }
+
+ /** Cancel the incoming call notification for the given call. */
+ public void cancelInCallNotification(Call call) {
+ L.d(TAG, "cancelInCallNotification");
+ if (call.getDetails() != null) {
+ mNotificationManager.cancel(call.getDetails().getTelecomCallId(), NOTIFICATION_ID);
+ }
+ }
+
+ private Notification.Action getAction(Call call, @StringRes int actionText,
+ String intentAction) {
+ CharSequence text = mContext.getString(actionText);
+ PendingIntent intent = PendingIntent.getBroadcast(
+ mContext,
+ 0,
+ getIntent(intentAction, call),
+ PendingIntent.FLAG_UPDATE_CURRENT);
+ return new Notification.Action.Builder(null, text, intent).build();
+ }
+
+ private Intent getIntent(String action, Call call) {
+ Intent intent = new Intent(action, null, mContext, NotificationReceiver.class);
+ intent.putExtra(NotificationService.EXTRA_CALL_ID, call.getDetails().getTelecomCallId());
+ return intent;
+ }
+}
diff --git a/src/com/android/car/dialer/notification/MissedCallNotificationController.java b/src/com/android/car/dialer/notification/MissedCallNotificationController.java
new file mode 100644
index 00000000..a1d47f76
--- /dev/null
+++ b/src/com/android/car/dialer/notification/MissedCallNotificationController.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.notification;
+
+import android.annotation.TargetApi;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.drawable.Icon;
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+import androidx.core.util.Pair;
+import androidx.lifecycle.Observer;
+
+import com.android.car.dialer.Constants;
+import com.android.car.dialer.R;
+import com.android.car.dialer.livedata.UnreadMissedCallLiveData;
+import com.android.car.dialer.log.L;
+import com.android.car.dialer.ui.TelecomActivity;
+import com.android.car.dialer.ui.TelecomPageTab;
+import com.android.car.telephony.common.PhoneCallLog;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/** Controller that manages the missed call notifications. */
+public final class MissedCallNotificationController {
+ private static final String TAG = "CD.MissedCallNotification";
+ private static final String CHANNEL_ID = "com.android.car.dialer.missedcall";
+ // A random number that is used for notification id.
+ private static final int NOTIFICATION_ID = 20190520;
+
+ private static MissedCallNotificationController sMissedCallNotificationController;
+
+ /**
+ * Initialized a globally accessible {@link MissedCallNotificationController} which can be
+ * retrieved by {@link #get}. If this function is called a second time before calling {@link
+ * #tearDown()}, an {@link IllegalStateException} will be thrown.
+ *
+ * @param applicationContext Application context.
+ */
+ public static void init(Context applicationContext) {
+ if (sMissedCallNotificationController == null) {
+ sMissedCallNotificationController = new MissedCallNotificationController(
+ applicationContext);
+ } else {
+ throw new IllegalStateException(
+ "MissedCallNotificationController has been initialized.");
+ }
+ }
+
+ /**
+ * Gets the global {@link MissedCallNotificationController} instance. Make sure {@link
+ * #init(Context)} is called before calling this method.
+ */
+ public static MissedCallNotificationController get() {
+ if (sMissedCallNotificationController == null) {
+ throw new IllegalStateException(
+ "Call MissedCallNotificationController.init(Context) before calling this "
+ + "function");
+ }
+ return sMissedCallNotificationController;
+ }
+
+ /** Tear down the global missed call notification controller. */
+ public void tearDown() {
+ mUnreadMissedCallLiveData.removeObserver(mUnreadMissedCallObserver);
+ sMissedCallNotificationController = null;
+ }
+
+ private final Context mContext;
+ private final NotificationManager mNotificationManager;
+ private final UnreadMissedCallLiveData mUnreadMissedCallLiveData;
+ private final Observer<List<PhoneCallLog>> mUnreadMissedCallObserver;
+ private final List<PhoneCallLog> mCurrentPhoneCallLogList;
+
+ @TargetApi(26)
+ private MissedCallNotificationController(Context context) {
+ mContext = context;
+ mNotificationManager =
+ (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+ CharSequence name = mContext.getString(R.string.missed_call_notification_channel_name);
+ NotificationChannel notificationChannel = new NotificationChannel(CHANNEL_ID, name,
+ NotificationManager.IMPORTANCE_DEFAULT);
+ mNotificationManager.createNotificationChannel(notificationChannel);
+
+ mCurrentPhoneCallLogList = new ArrayList<>();
+ mUnreadMissedCallLiveData = UnreadMissedCallLiveData.newInstance(context);
+ mUnreadMissedCallObserver = this::updateNotifications;
+ mUnreadMissedCallLiveData.observeForever(mUnreadMissedCallObserver);
+ }
+
+ /**
+ * The phone call log list might be null when switching users if permission gets denied and
+ * throws exception.
+ */
+ private void updateNotifications(@Nullable List<PhoneCallLog> phoneCallLogs) {
+ List<PhoneCallLog> updatedPhoneCallLogs =
+ phoneCallLogs == null ? Collections.emptyList() : phoneCallLogs;
+ for (PhoneCallLog phoneCallLog : updatedPhoneCallLogs) {
+ showMissedCallNotification(phoneCallLog);
+ if (mCurrentPhoneCallLogList.contains(phoneCallLog)) {
+ mCurrentPhoneCallLogList.remove(phoneCallLog);
+ }
+ }
+
+ for (PhoneCallLog phoneCallLog : mCurrentPhoneCallLogList) {
+ cancelMissedCallNotification(phoneCallLog);
+ }
+ mCurrentPhoneCallLogList.clear();
+ mCurrentPhoneCallLogList.addAll(updatedPhoneCallLogs);
+ }
+
+ private void showMissedCallNotification(PhoneCallLog phoneCallLog) {
+ L.d(TAG, "show missed call notification %s", phoneCallLog);
+ String phoneNumberString = phoneCallLog.getPhoneNumberString();
+ Pair<String, Icon> displayNameAndRoundedAvatar =
+ NotificationUtils.getDisplayNameAndRoundedAvatar(mContext, phoneNumberString);
+ Notification.Builder builder = new Notification.Builder(mContext, CHANNEL_ID)
+ .setSmallIcon(R.drawable.ic_phone)
+ .setLargeIcon(displayNameAndRoundedAvatar.second)
+ .setContentTitle(
+ mContext.getString(R.string.notification_missed_call) + String.format(
+ " (%d)", phoneCallLog.getAllCallRecords().size()))
+ .setContentText(displayNameAndRoundedAvatar.first)
+ .setContentIntent(getContentPendingIntent())
+ .setDeleteIntent(getDeleteIntent())
+ .setOnlyAlertOnce(true)
+ .setShowWhen(true)
+ .setWhen(phoneCallLog.getLastCallEndTimestamp())
+ .setAutoCancel(false);
+
+ if (!TextUtils.isEmpty(phoneNumberString)) {
+ builder.addAction(getAction(phoneNumberString, R.string.call_back,
+ NotificationService.ACTION_CALL_BACK_MISSED));
+ // TODO: add action button to send message
+ }
+
+ mNotificationManager.notify(
+ getTag(phoneCallLog),
+ NOTIFICATION_ID,
+ builder.build());
+ }
+
+ private void cancelMissedCallNotification(PhoneCallLog phoneCallLog) {
+ L.d(TAG, "cancel missed call notification %s", phoneCallLog);
+ mNotificationManager.cancel(getTag(phoneCallLog), NOTIFICATION_ID);
+ }
+
+ private PendingIntent getContentPendingIntent() {
+ Intent intent = new Intent(mContext, TelecomActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.setAction(Constants.Intents.ACTION_SHOW_PAGE);
+ intent.putExtra(Constants.Intents.EXTRA_SHOW_PAGE, TelecomPageTab.Page.CALL_HISTORY);
+ intent.putExtra(Constants.Intents.EXTRA_ACTION_READ_MISSED, true);
+ PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent,
+ PendingIntent.FLAG_UPDATE_CURRENT);
+ return pendingIntent;
+ }
+
+ private PendingIntent getDeleteIntent() {
+ Intent intent = new Intent(NotificationService.ACTION_READ_ALL_MISSED, null, mContext,
+ NotificationReceiver.class);
+ PendingIntent pendingIntent = PendingIntent.getBroadcast(
+ mContext,
+ 0,
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT);
+ return pendingIntent;
+ }
+
+ private Notification.Action getAction(String phoneNumberString, @StringRes int actionText,
+ String intentAction) {
+ CharSequence text = mContext.getString(actionText);
+ PendingIntent intent = PendingIntent.getBroadcast(
+ mContext,
+ 0,
+ getIntent(intentAction, phoneNumberString),
+ PendingIntent.FLAG_UPDATE_CURRENT);
+ return new Notification.Action.Builder(null, text, intent).build();
+ }
+
+ private Intent getIntent(String action, String phoneNumberString) {
+ Intent intent = new Intent(action, null, mContext, NotificationReceiver.class);
+ intent.putExtra(NotificationService.EXTRA_CALL_ID, phoneNumberString);
+ return intent;
+ }
+
+ private String getTag(@NonNull PhoneCallLog phoneCallLog) {
+ return String.valueOf(phoneCallLog.hashCode());
+ }
+}
diff --git a/src/com/android/car/dialer/notification/MissedCallReceiver.java b/src/com/android/car/dialer/notification/MissedCallReceiver.java
new file mode 100644
index 00000000..25b7e15f
--- /dev/null
+++ b/src/com/android/car/dialer/notification/MissedCallReceiver.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.notification;
+
+import static android.telecom.TelecomManager.ACTION_SHOW_MISSED_CALLS_NOTIFICATION;
+import static android.telecom.TelecomManager.EXTRA_NOTIFICATION_COUNT;
+import static android.telecom.TelecomManager.EXTRA_NOTIFICATION_PHONE_NUMBER;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.telecom.Log;
+
+import com.android.car.dialer.log.L;
+
+/**
+ * A {@link BroadcastReceiver} that is used to inform telecom manager that we are showing the
+ * missed call notification that it does not have to show missed call notification on its behalf.
+ *
+ * <p>We only log the intent. The missed call notification is monitored and handled in the {@link
+ * MissedCallNotificationController}.
+ */
+public class MissedCallReceiver extends BroadcastReceiver {
+ private static final String TAG = "CD.MissedCallReceiver";
+
+ @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, 0);
+ String phoneNumber = intent.getStringExtra(EXTRA_NOTIFICATION_PHONE_NUMBER);
+
+ L.d(TAG, "Count: %d PhoneNumber: %s", count, Log.pii(phoneNumber));
+ }
+}
diff --git a/src/com/android/car/dialer/notification/NotificationReceiver.java b/src/com/android/car/dialer/notification/NotificationReceiver.java
new file mode 100644
index 00000000..8c7d8d73
--- /dev/null
+++ b/src/com/android/car/dialer/notification/NotificationReceiver.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.notification;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+/** This class is responsible for offloading notification broadcasts to the notification service. */
+public class NotificationReceiver extends BroadcastReceiver {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ NotificationService.enqueueWork(context, intent);
+ }
+}
diff --git a/src/com/android/car/dialer/notification/NotificationService.java b/src/com/android/car/dialer/notification/NotificationService.java
new file mode 100644
index 00000000..2f282907
--- /dev/null
+++ b/src/com/android/car/dialer/notification/NotificationService.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.notification;
+
+import android.content.Context;
+import android.content.Intent;
+import android.telecom.Call;
+import android.text.TextUtils;
+
+import androidx.core.app.JobIntentService;
+
+import com.android.car.dialer.Constants;
+import com.android.car.dialer.telecom.UiCallManager;
+import com.android.car.telephony.common.TelecomUtils;
+
+import java.util.List;
+
+/**
+ * A {@link JobIntentService} that is used to handle actions from notifications to:
+ * <ul><li>answer or inject an incoming call.
+ * <li>call back or message to a missed call.
+ */
+public class NotificationService extends JobIntentService {
+ static final String ACTION_ANSWER_CALL = "CD.ACTION_ANSWER_CALL";
+ static final String ACTION_DECLINE_CALL = "CD.ACTION_DECLINE_CALL";
+ static final String ACTION_CALL_BACK_MISSED = "CD.ACTION_CALL_BACK_MISSED";
+ static final String ACTION_MESSAGE_MISSED = "CD.ACTION_MESSAGE_MISSED";
+ static final String ACTION_READ_ALL_MISSED = "CD.ACTION_READ_ALL_MISSED";
+ static final String EXTRA_CALL_ID = "CD.EXTRA_CALL_ID";
+
+ /** Create an intent to handle reading all missed call action and schedule for executing. */
+ public static void readAllMissedCall(Context context) {
+ Intent readAllMissedCallIntent = new Intent(context, NotificationReceiver.class);
+ readAllMissedCallIntent.setAction(ACTION_READ_ALL_MISSED);
+ enqueueWork(context, readAllMissedCallIntent);
+ }
+
+ /** Enqueue the intent. */
+ static void enqueueWork(Context context, Intent intent) {
+ enqueueWork(
+ context, NotificationService.class, Constants.JobIds.NOTIFICATION_SERVICE, intent);
+ }
+
+ @Override
+ protected void onHandleWork(Intent intent) {
+ String action = intent.getAction();
+ String callId = intent.getStringExtra(EXTRA_CALL_ID);
+ switch (action) {
+ case ACTION_ANSWER_CALL:
+ answerCall(callId);
+ break;
+ case ACTION_DECLINE_CALL:
+ declineCall(callId);
+ break;
+ case ACTION_CALL_BACK_MISSED:
+ UiCallManager.get().placeCall(callId);
+ TelecomUtils.markCallLogAsRead(getApplicationContext(), callId);
+ break;
+ case ACTION_MESSAGE_MISSED:
+ // TODO: call assistant to send message
+ TelecomUtils.markCallLogAsRead(getApplicationContext(), callId);
+ break;
+ case ACTION_READ_ALL_MISSED:
+ TelecomUtils.markCallLogAsRead(getApplicationContext(), callId);
+ break;
+ default:
+ break;
+ }
+ }
+
+ private void answerCall(String callId) {
+ List<Call> callList = UiCallManager.get().getCallList();
+ for (Call call : callList) {
+ if (call.getDetails() != null
+ && TextUtils.equals(call.getDetails().getTelecomCallId(), callId)) {
+ call.answer(/* videoState= */0);
+ return;
+ }
+ }
+ }
+
+ private void declineCall(String callId) {
+ List<Call> callList = UiCallManager.get().getCallList();
+ for (Call call : callList) {
+ if (call.getDetails() != null
+ && TextUtils.equals(call.getDetails().getTelecomCallId(), callId)) {
+ call.reject(false, /* textMessage= */"");
+ return;
+ }
+ }
+ }
+}
diff --git a/src/com/android/car/dialer/notification/NotificationUtils.java b/src/com/android/car/dialer/notification/NotificationUtils.java
new file mode 100644
index 00000000..9b6f9322
--- /dev/null
+++ b/src/com/android/car/dialer/notification/NotificationUtils.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.notification;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+
+import androidx.annotation.Nullable;
+import androidx.core.graphics.drawable.RoundedBitmapDrawable;
+import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
+import androidx.core.util.Pair;
+
+import com.android.car.apps.common.LetterTileDrawable;
+import com.android.car.dialer.R;
+import com.android.car.telephony.common.TelecomUtils;
+
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+
+/** Util class that shares common functionality for notifications. */
+final class NotificationUtils {
+ private NotificationUtils() {
+ }
+
+ static Pair<String, Icon> getDisplayNameAndRoundedAvatar(Context context,
+ String phoneNumberString) {
+ Pair<String, Uri> displayNameAndAvatarUri = TelecomUtils.getDisplayNameAndAvatarUri(
+ context, phoneNumberString);
+
+ int avatarSize = context.getResources().getDimensionPixelSize(R.dimen.avatar_icon_size);
+ Icon largeIcon = loadRoundedContactAvatar(context, displayNameAndAvatarUri.second,
+ avatarSize);
+ if (largeIcon == null) {
+ largeIcon = createLetterTile(context, displayNameAndAvatarUri.first, avatarSize);
+ }
+ return new Pair<>(displayNameAndAvatarUri.first, largeIcon);
+ }
+
+ static Icon loadRoundedContactAvatar(Context context, @Nullable Uri avatarUri, int avatarSize) {
+ if (avatarUri == null) {
+ return null;
+ }
+
+ try {
+ InputStream input = context.getContentResolver().openInputStream(avatarUri);
+ if (input == null) {
+ return null;
+ }
+ RoundedBitmapDrawable roundedBitmapDrawable = RoundedBitmapDrawableFactory.create(
+ context.getResources(), input);
+ roundedBitmapDrawable.setCircular(true);
+
+ final Bitmap result = Bitmap.createBitmap(avatarSize, avatarSize,
+ Bitmap.Config.ARGB_8888);
+ final Canvas canvas = new Canvas(result);
+ roundedBitmapDrawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+ roundedBitmapDrawable.draw(canvas);
+ roundedBitmapDrawable.getBitmap().recycle();
+ return Icon.createWithBitmap(result);
+ } catch (FileNotFoundException e) {
+ // No-op
+ }
+ return null;
+ }
+
+ static Icon createLetterTile(Context context, String displayName, int avatarSize) {
+ LetterTileDrawable letterTileDrawable = TelecomUtils.createLetterTile(context, displayName);
+ letterTileDrawable.setIsCircular(true);
+ return Icon.createWithBitmap(letterTileDrawable.toBitmap(avatarSize));
+ }
+}
diff --git a/src/com/android/car/dialer/telecom/ContactBitmapWorker.java b/src/com/android/car/dialer/telecom/ContactBitmapWorker.java
deleted file mode 100644
index b57179e2..00000000
--- a/src/com/android/car/dialer/telecom/ContactBitmapWorker.java
+++ /dev/null
@@ -1,119 +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.car.dialer.telecom;
-
-import android.content.ContentResolver;
-import android.graphics.Bitmap;
-import android.os.AsyncTask;
-import android.widget.ImageView;
-
-import java.lang.ref.WeakReference;
-
-import androidx.annotation.MainThread;
-import androidx.annotation.Nullable;
-
-/**
- * Helper task that retrieves a Contact photo from the local Contacts store. The loading task
- * is tied to an ImageView that allows a lightweight management of the task upon update of the view.
- *
- * TODO(mcrico): Using a View's TAG to store and manage Async task loading is pretty brittle.
- * Vanagon is not depending on this logic and projected shouldn't either.
- */
-public class ContactBitmapWorker extends AsyncTask<Void, Void, Bitmap> {
- private final WeakReference<ImageView> mImageViewReference;
- private final WeakReference<ContentResolver> mContentResolverReference;
- private final String mNumber;
- private final BitmapWorkerListener mListener;
-
- /** Interface to receive updates from this worker */
- public interface BitmapWorkerListener {
- /** Called in the main thread upon bitmap load finish */
- @MainThread
- void onBitmapLoaded(@Nullable Bitmap bitmap);
- }
-
- /**
- * @return A worker task if a new one was needed to load the bitmap.
- */
- @Nullable public static ContactBitmapWorker loadBitmap(
- ContentResolver contentResolver,
- ImageView imageView,
- String number,
- BitmapWorkerListener listener) {
-
- // This work may be underway already.
- if (!cancelPotentialWork(number, imageView)) {
- return null;
- }
-
- ContactBitmapWorker task =
- new ContactBitmapWorker(contentResolver, imageView, number, listener);
- imageView.setTag(task);
- imageView.setImageResource(0);
- task.execute();
- return task;
- }
-
- /** Use {@link #loadBitmap} instead, as it guarantees de-duplication of work */
- private ContactBitmapWorker(
- ContentResolver contentResolver,
- ImageView imageView,
- String number,
- BitmapWorkerListener listener) {
- mImageViewReference = new WeakReference<>(imageView);
- mContentResolverReference = new WeakReference<>(contentResolver);
- mNumber = number;
- mListener = listener;
- }
-
- @Override
- protected Bitmap doInBackground(Void... voids) {
- final ContentResolver contentResolver = mContentResolverReference.get();
- if (contentResolver != null) {
- return TelecomUtils.getContactPhotoFromNumber(contentResolver, mNumber);
- }
- return null;
- }
-
- @Override
- protected void onPostExecute(Bitmap bitmap) {
- if (isCancelled()) {
- return;
- }
-
- if (mImageViewReference.get() != null) {
- mListener.onBitmapLoaded(bitmap);
- }
- }
-
- /**
- * @return Whether a new Bitmap loading should continue for this imageView.
- */
- private static boolean cancelPotentialWork(String number, ImageView imageView) {
- final ContactBitmapWorker bitmapWorkerTask = (ContactBitmapWorker) imageView.getTag();
- if (bitmapWorkerTask != null) {
- if (bitmapWorkerTask.mNumber != number) {
- bitmapWorkerTask.cancel(true);
- imageView.setTag(null);
- } else {
- // The same work is already in progress
- return false;
- }
- }
-
- return true;
- }
-}
diff --git a/src/com/android/car/dialer/telecom/InCallRouter.java b/src/com/android/car/dialer/telecom/InCallRouter.java
new file mode 100644
index 00000000..07044706
--- /dev/null
+++ b/src/com/android/car/dialer/telecom/InCallRouter.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.telecom;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Handler;
+import android.telecom.Call;
+
+import com.android.car.dialer.log.L;
+import com.android.car.dialer.notification.InCallNotificationController;
+import com.android.car.dialer.ui.activecall.InCallActivity;
+import com.android.car.dialer.ui.activecall.InCallViewModel;
+
+import java.util.ArrayList;
+
+/**
+ * Routes a call to different path depending on its state. If there is any {@link
+ * InCallServiceImpl.ActiveCallListChangedCallback} that already handles the call, i.e. the {@link
+ * InCallViewModel} that actively updates the in call page, then we don't show HUN for the ringing
+ * call or attempt to start the in call page again.
+ */
+class InCallRouter {
+
+ private static final String TAG = "CD.InCallRouter";
+
+ private final Context mContext;
+ private final Handler mMainHandler;
+ private final InCallNotificationController mInCallNotificationController;
+ private final ArrayList<InCallServiceImpl.ActiveCallListChangedCallback>
+ mActiveCallListChangedCallbacks = new ArrayList<>();
+ private final ProjectionCallHandler mProjectionCallHandler;
+
+ InCallRouter(Context context) {
+ mContext = context;
+ mMainHandler = Handler.getMain();
+ mInCallNotificationController = InCallNotificationController.get();
+ mProjectionCallHandler = new ProjectionCallHandler(context);
+ }
+
+ void start() {
+ mProjectionCallHandler.start();
+ mActiveCallListChangedCallbacks.add(mProjectionCallHandler);
+ }
+
+ void stop() {
+ mActiveCallListChangedCallbacks.remove(mProjectionCallHandler);
+ mProjectionCallHandler.stop();
+ }
+
+ /**
+ * Routes the added call to the correct path:
+ * <ul>
+ * <li> First dispatches it to the {@link InCallServiceImpl.ActiveCallListChangedCallback}s.
+ * <li> If the ringing call is not handled by callbacks, it will show a HUN.
+ * <li> If the call is in other state and not handled by callbacks, it will try to launch the in
+ * call page.
+ */
+ void onCallAdded(Call call) {
+ boolean isHandled = routeToActiveCallListChangedCallback(call);
+ if (isHandled) {
+ return;
+ }
+
+ int state = call.getState();
+ if (state == Call.STATE_RINGING) {
+ routeToNotification(call);
+ } else {
+ routeToInCallPage(call);
+ }
+ }
+
+ /**
+ * Called by {@link InCallServiceImpl#onCallRemoved(Call)}. It notifies the {@link
+ * InCallServiceImpl.ActiveCallListChangedCallback}s to update the active call list.
+ */
+ void onCallRemoved(Call call) {
+ for (InCallServiceImpl.ActiveCallListChangedCallback callback :
+ mActiveCallListChangedCallbacks) {
+ callback.onTelecomCallRemoved(call);
+ }
+ }
+
+ void registerActiveCallListChangedCallback(
+ InCallServiceImpl.ActiveCallListChangedCallback callback) {
+ mMainHandler.post(() -> mActiveCallListChangedCallbacks.add(callback));
+ }
+
+ void unregisterActiveCallHandler(InCallServiceImpl.ActiveCallListChangedCallback callback) {
+ mMainHandler.post(() -> mActiveCallListChangedCallbacks.remove(callback));
+ }
+
+ /** Dispatches the call to {@link InCallServiceImpl.ActiveCallListChangedCallback}. */
+ private boolean routeToActiveCallListChangedCallback(Call call) {
+ boolean isHandled = false;
+ for (InCallServiceImpl.ActiveCallListChangedCallback callback :
+ mActiveCallListChangedCallbacks) {
+ if (callback.onTelecomCallAdded(call)) {
+ isHandled = true;
+ }
+ }
+
+ return isHandled;
+ }
+
+ /** Presents the ringing call in HUN. */
+ private void routeToNotification(Call call) {
+ mInCallNotificationController.showInCallNotification(call);
+ call.registerCallback(new Call.Callback() {
+ @Override
+ public void onStateChanged(Call call, int state) {
+ L.d(TAG, "Ringing call state changed to %d", state);
+ routeToInCallPage(call);
+ mInCallNotificationController.cancelInCallNotification(call);
+ call.unregisterCallback(this);
+ }
+ });
+ }
+
+ /** Launches {@link InCallActivity} and presents the on going call in the in call page. */
+ private void routeToInCallPage(Call call) {
+ // Don't launch the in call page if state is disconnected. Otherwise, the InCallActivity
+ // finishes right after onCreate() and flashes.
+ if (call.getState() != Call.STATE_DISCONNECTED) {
+ Intent launchIntent = new Intent(mContext, InCallActivity.class);
+ mContext.startActivity(launchIntent);
+ }
+ }
+}
diff --git a/src/com/android/car/dialer/telecom/InCallServiceImpl.java b/src/com/android/car/dialer/telecom/InCallServiceImpl.java
index 48688963..a5faaf6a 100644
--- a/src/com/android/car/dialer/telecom/InCallServiceImpl.java
+++ b/src/com/android/car/dialer/telecom/InCallServiceImpl.java
@@ -18,11 +18,12 @@ package com.android.car.dialer.telecom;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
+import android.os.Process;
import android.telecom.Call;
import android.telecom.CallAudioState;
import android.telecom.InCallService;
-import android.telecom.TelecomManager;
-import android.util.Log;
+
+import com.android.car.dialer.log.L;
import java.util.concurrent.CopyOnWriteArrayList;
@@ -31,84 +32,81 @@ import java.util.concurrent.CopyOnWriteArrayList;
* {@link UiCallManager}. For incoming calls it will launch Dialer app.
*/
public class InCallServiceImpl extends InCallService {
- private static final String TAG = "Em.InCallService";
+ private static final String TAG = "CD.InCallService";
- static final String ACTION_LOCAL_BIND = "local_bind";
+ /** An action which indicates a bind is from local component. */
+ public static final String ACTION_LOCAL_BIND = "local_bind";
private CopyOnWriteArrayList<Callback> mCallbacks = new CopyOnWriteArrayList<>();
- private TelecomManager mTelecomManager;
+ private InCallRouter mInCallRouter;
+
+ /** Listens to active call list changes. Callbacks will be called on main thread. */
+ public interface ActiveCallListChangedCallback {
+
+ /**
+ * Called when a new call is added.
+ *
+ * @return if the given call has been handled by this callback.
+ */
+ boolean onTelecomCallAdded(Call telecomCall);
+
+ /**
+ * Called when an existing call is removed.
+ *
+ * @return if the given call has been handled by this callback.
+ */
+ boolean onTelecomCallRemoved(Call telecomCall);
+ }
@Override
public void onCreate() {
super.onCreate();
- mTelecomManager = getApplicationContext().getSystemService(TelecomManager.class);
+ mInCallRouter = new InCallRouter(getApplicationContext());
+ mInCallRouter.start();
}
@Override
- public void onCallAdded(Call telecomCall) {
- super.onCallAdded(telecomCall);
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "onCallAdded: " + telecomCall + ", state: " + telecomCall);
- }
+ public void onDestroy() {
+ super.onDestroy();
+ mInCallRouter.stop();
+ mInCallRouter = null;
+ }
- telecomCall.registerCallback(mCallListener);
- mCallListener.onStateChanged(telecomCall, telecomCall.getState());
+ @Override
+ public void onCallAdded(Call telecomCall) {
+ L.d(TAG, "onCallAdded: %s", telecomCall);
for (Callback callback : mCallbacks) {
callback.onTelecomCallAdded(telecomCall);
}
+
+ mInCallRouter.onCallAdded(telecomCall);
}
@Override
public void onCallRemoved(Call telecomCall) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "onCallRemoved: " + telecomCall);
- }
+ L.d(TAG, "onCallRemoved: %s", telecomCall);
for (Callback callback : mCallbacks) {
callback.onTelecomCallRemoved(telecomCall);
}
- telecomCall.unregisterCallback(mCallListener);
- super.onCallRemoved(telecomCall);
+
+ mInCallRouter.onCallRemoved(telecomCall);
}
@Override
public IBinder onBind(Intent intent) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "onBind: " + intent);
- }
-
+ L.d(TAG, "onBind: %s", intent);
return ACTION_LOCAL_BIND.equals(intent.getAction())
? new LocalBinder()
: super.onBind(intent);
}
- private final Call.Callback mCallListener = new Call.Callback() {
- @Override
- public void onStateChanged(Call call, int state) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "onStateChanged call: " + call + ", state: " + state );
- }
-
- if (state == Call.STATE_RINGING || state == Call.STATE_DIALING) {
- if (Log.isLoggable(TAG, Log.INFO)) {
- Log.i(TAG, "Incoming/outgoing call: " + call);
- }
-
- // TODO(b/25190782): here we should show heads-up notification for incoming call,
- // however system notifications are disabled by System UI and we haven't implemented
- // a way to show heads-up notifications in embedded mode.
- Intent launchIntent = getPackageManager()
- .getLaunchIntentForPackage(mTelecomManager.getDefaultDialerPackage());
- startActivity(launchIntent);
- }
- }
- };
-
@Override
public boolean onUnbind(Intent intent) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "onUnbind, intent: " + intent);
+ L.d(TAG, "onUnbind, intent: %s", intent);
+ if (ACTION_LOCAL_BIND.equals(intent.getAction())) {
+ return false;
}
return super.onUnbind(intent);
}
@@ -128,15 +126,37 @@ public class InCallServiceImpl extends InCallService {
mCallbacks.remove(callback);
}
+ public void addActiveCallListChangedCallback(ActiveCallListChangedCallback callback) {
+ mInCallRouter.registerActiveCallListChangedCallback(callback);
+ }
+
+ public void removeActiveCallListChangedCallback(ActiveCallListChangedCallback callback) {
+ mInCallRouter.unregisterActiveCallHandler(callback);
+ }
+
+ @Deprecated
interface Callback {
void onTelecomCallAdded(Call telecomCall);
+
void onTelecomCallRemoved(Call telecomCall);
+
void onCallAudioStateChanged(CallAudioState audioState);
}
- class LocalBinder extends Binder {
- InCallServiceImpl getService() {
- return InCallServiceImpl.this;
+ /**
+ * Local binder only available for Car Dialer package.
+ */
+ public class LocalBinder extends Binder {
+
+ /**
+ * Returns a reference to {@link InCallServiceImpl}. Any process other than Dialer
+ * process won't be able to get a reference.
+ */
+ public InCallServiceImpl getService() {
+ if (getCallingPid() == Process.myPid()) {
+ return InCallServiceImpl.this;
+ }
+ return null;
}
}
}
diff --git a/src/com/android/car/dialer/telecom/InMemoryPhoneBook.java b/src/com/android/car/dialer/telecom/InMemoryPhoneBook.java
deleted file mode 100644
index a5fd34f8..00000000
--- a/src/com/android/car/dialer/telecom/InMemoryPhoneBook.java
+++ /dev/null
@@ -1,122 +0,0 @@
-package com.android.car.dialer.telecom;
-
-import android.content.Context;
-import android.database.Cursor;
-import android.provider.ContactsContract;
-import android.telephony.PhoneNumberUtils;
-
-import com.android.car.dialer.ContactEntry;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.loader.content.CursorLoader;
-import androidx.loader.content.Loader;
-
-/**
- * A singleton statically accessible helper class which pre-loads contacts list into memory so
- * that they can be accessed more easily and quickly.
- */
-public class InMemoryPhoneBook implements Loader.OnLoadCompleteListener<Cursor> {
- private static InMemoryPhoneBook sInMemoryPhoneBook;
-
- private final Context mContext;
-
- private boolean mIsLoaded = false;
- private List<ContactEntry> mContactEntries = new ArrayList<>();
- Map<Integer, List<ContactEntry>> mIdToContactEntryMap;
-
- private InMemoryPhoneBook(Context context) {
- mContext = context;
- }
-
- public static InMemoryPhoneBook init(Context context) {
- if (sInMemoryPhoneBook == null) {
- sInMemoryPhoneBook = new InMemoryPhoneBook(context);
- sInMemoryPhoneBook.onInit();
- } else {
- throw new IllegalStateException("Call teardown before reinitialized PhoneBook");
- }
- return get();
- }
-
- public static InMemoryPhoneBook get() {
- if (sInMemoryPhoneBook != null) {
- return sInMemoryPhoneBook;
- } else {
- throw new IllegalStateException("Call init before get InMemoryPhoneBook");
- }
- }
-
- public static void tearDown() {
- sInMemoryPhoneBook = null;
- }
-
- private void onInit() {
- CursorLoader cursorLoader = createPhoneBookCursorLoader();
- cursorLoader.registerListener(0, this);
- cursorLoader.startLoading();
- }
-
- public boolean isLoaded() {
- return mIsLoaded;
- }
-
- /**
- * Returns a alphabetically sorted contact list.
- */
- public List<ContactEntry> getOrderedContactEntries() {
- return mContactEntries;
- }
-
- @Nullable
- public ContactEntry lookupContactEntry(String phoneNumber) {
- for (ContactEntry contactEntry : mContactEntries) {
- if (PhoneNumberUtils.compare(mContext, phoneNumber, contactEntry.getNumber())) {
- return contactEntry;
- }
- }
- return null;
- }
-
- public Map<Integer, List<ContactEntry>> getIdToContactEntryMap() {
- if (mIdToContactEntryMap == null) {
- mIdToContactEntryMap = new HashMap<>();
- for (ContactEntry contactEntry : mContactEntries) {
- List<ContactEntry> list;
- if (mIdToContactEntryMap.containsKey(contactEntry.getId())) {
- list = mIdToContactEntryMap.get(contactEntry.getId());
- } else {
- list = new ArrayList<>();
- }
- list.add(contactEntry);
- }
- }
- return mIdToContactEntryMap;
- }
-
- @Override
- public void onLoadComplete(@NonNull Loader<Cursor> loader, @Nullable Cursor cursor) {
- if (cursor != null) {
- while (cursor.moveToNext()) {
- mContactEntries.add(ContactEntry.fromCursor(cursor, mContext));
- }
- }
- mIsLoaded = true;
- }
-
- private CursorLoader createPhoneBookCursorLoader() {
- return new CursorLoader(mContext,
- ContactsContract.Data.CONTENT_URI,
- null,
- ContactsContract.Data.MIMETYPE + " = '"
- + ContactsContract.CommonDataKinds.Phone
- .CONTENT_ITEM_TYPE + "'",
- null,
- ContactsContract.Contacts.DISPLAY_NAME + " ASC ");
- }
-}
diff --git a/src/com/android/car/dialer/telecom/PhoneLoader.java b/src/com/android/car/dialer/telecom/PhoneLoader.java
deleted file mode 100644
index 8d55d9b5..00000000
--- a/src/com/android/car/dialer/telecom/PhoneLoader.java
+++ /dev/null
@@ -1,257 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.car.dialer.telecom;
-
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.CursorLoader;
-import android.content.Loader;
-import android.database.Cursor;
-import android.net.Uri;
-import android.provider.BaseColumns;
-import android.provider.CallLog;
-import android.provider.ContactsContract;
-import android.text.TextUtils;
-import android.util.Log;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-
-import androidx.annotation.IntDef;
-
-/**
- * Manages loading different types of call logs.
- * Currently supports:
- * All calls
- * Missed calls
- * speed dial calls
- */
-public class PhoneLoader {
- private static final String TAG = "Em.PhoneLoader";
-
- /** CALL_TYPE_ALL and _MISSED's values are assigned to be consistent with the Dialer **/
- public final static int CALL_TYPE_ALL = -1;
- public final static int CALL_TYPE_MISSED = CallLog.Calls.MISSED_TYPE;
- /** Starred and frequent **/
- public final static int CALL_TYPE_SPEED_DIAL = 2;
-
- @IntDef({
- CallType.CALL_TYPE_ALL,
- CallType.INCOMING_TYPE,
- CallType.OUTGOING_TYPE,
- CallType.MISSED_TYPE,
- })
- public @interface CallType {
- int CALL_TYPE_ALL = -1;
- int INCOMING_TYPE = CallLog.Calls.INCOMING_TYPE;
- int OUTGOING_TYPE = CallLog.Calls.OUTGOING_TYPE;
- int MISSED_TYPE = CallLog.Calls.MISSED_TYPE;
- }
-
- private static final int NUM_LOGS_TO_DISPLAY = 100;
- private static final String[] EMPTY_STRING_ARRAY = new String[0];
-
- public static final int INCOMING_TYPE = 1;
- public static final int OUTGOING_TYPE = 2;
- public static final int MISSED_TYPE = 3;
- public static final int VOICEMAIL_TYPE = 4;
-
- private static HashMap<String, String> sNumberCache;
-
- /**
- * Hybrid Factory for creating a Contact Loader that also immediately starts its execution.
- * Note: NOT to be used wit LoaderManagers.
- */
- public static CursorLoader registerCallObserver(int type,
- Context context, Loader.OnLoadCompleteListener<Cursor> listener) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "registerCallObserver: type: " + type + ", listener: " + listener);
- }
-
- switch (type) {
- case CALL_TYPE_ALL:
- case CALL_TYPE_MISSED:
- return fetchCallLog(type, context, listener);
- case CALL_TYPE_SPEED_DIAL:
- CursorLoader loader = newStrequentContactLoader(context);
- loader.registerListener(0, listener);
- loader.startLoading();
- return loader;
- default:
- throw new UnsupportedOperationException("Unknown CALL_TYPE " + type + ".");
- }
- }
-
- /**
- * Factory method for creating a Loader that will fetch strequent contacts from the phone.
- */
- public static CursorLoader newStrequentContactLoader(Context context) {
- Uri uri = ContactsContract.Contacts.CONTENT_STREQUENT_URI.buildUpon()
- .appendQueryParameter(ContactsContract.STREQUENT_PHONE_ONLY, "true")
- .appendQueryParameter(ContactsContract.REMOVE_DUPLICATE_ENTRIES, "true").build();
-
- return new CursorLoader(context, uri, null, null, null, null);
- }
-
- // TODO(mcrico): Separate into a factory method and move configuration to registerCallObserver
- private static CursorLoader fetchCallLog(int callType,
- Context context, Loader.OnLoadCompleteListener<Cursor> listener) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "fetchCallLog");
- }
-
- // We need to check for NULL explicitly otherwise entries with where READ is NULL
- // may not match either the query or its negation.
- // We consider the calls that are not yet consumed (i.e. IS_READ = 0) as "new".
- StringBuilder where = new StringBuilder();
- List<String> selectionArgs = new ArrayList<String>();
-
- if (callType > CALL_TYPE_ALL) {
- // add a filter for call type
- where.append(String.format("(%s = ?)", CallLog.Calls.TYPE));
- selectionArgs.add(Integer.toString(callType));
- }
- String selection = where.length() > 0 ? where.toString() : null;
-
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "accessingCallLog");
- }
-
- Uri uri = CallLog.Calls.CONTENT_URI.buildUpon()
- .appendQueryParameter(CallLog.Calls.LIMIT_PARAM_KEY,
- Integer.toString(NUM_LOGS_TO_DISPLAY))
- .build();
- CursorLoader loader = new CursorLoader(context, uri, null, selection,
- selectionArgs.toArray(EMPTY_STRING_ARRAY), CallLog.Calls.DEFAULT_SORT_ORDER);
- loader.registerListener(0, listener);
- loader.startLoading();
- return loader;
- }
-
- /**
- * @return The column index of the contact id. It should be {@link BaseColumns#_ID}. However,
- * if that fails use {@link android.provider.ContactsContract.RawContacts#CONTACT_ID}.
- * If that also fails, we use the first column in the table.
- */
- public static int getIdColumnIndex(Cursor cursor) {
- int ret = cursor.getColumnIndex(BaseColumns._ID);
- if (ret == -1) {
- if (Log.isLoggable(TAG, Log.INFO)) {
- Log.i(TAG, "Falling back to contact_id instead of _id");
- }
-
- // Some versions of the ContactsProvider on LG don't have an _id column but instead
- // use contact_id. If the lookup for _id fails, we fallback to contact_id.
- ret = cursor.getColumnIndexOrThrow(ContactsContract.RawContacts.CONTACT_ID);
- }
- if (ret == -1) {
- Log.e(TAG, "Neither _id or contact_id exist! Falling back to column 0. " +
- "There is no guarantee that this will work!");
- ret = 0;
- }
- return ret;
- }
-
- /**
- * @return The column index of the number.
- * Will return a valid column for call log or contacts queries.
- */
- public static int getNumberColumnIndex(Cursor cursor) {
- int numberColumn = cursor.getColumnIndex(CallLog.Calls.NUMBER);
- if (numberColumn == -1) {
- numberColumn = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER);
- }
- return numberColumn;
- }
-
-
- /**
- * @return The column index of the number type.
- * Will return a valid column for call log or contacts queries.
- */
- public static int getTypeColumnIndex(Cursor cursor) {
- int typeColumn = cursor.getColumnIndex(CallLog.Calls.TYPE);
- if (typeColumn == -1) {
- typeColumn = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.TYPE);
- }
- return typeColumn;
- }
-
- /**
- * @return The column index of the name.
- * Will return a valid column for call log or contacts queries.
- */
- public static int getNameColumnIndex(Cursor cursor) {
- int typeColumn = cursor.getColumnIndex(CallLog.Calls.CACHED_NAME);
- if (typeColumn == -1) {
- typeColumn = cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME);
- }
- return typeColumn;
- }
-
- /**
- * @return The phone number for the contact. Most phones will simply get the value in the
- * column returned by {@link #getNumberColumnIndex(Cursor)}. However, some devices
- * such as the Galaxy S6 return null for those columns. In those cases, we use the
- * contact id (which we hopefully do have) to look up just the phone number for that
- * specific contact.
- */
- public static String getPhoneNumber(Cursor cursor, ContentResolver cr) {
- int columnIndex = getNumberColumnIndex(cursor);
- String number = cursor.getString(columnIndex);
- if (number == null) {
- Log.w(TAG, "Phone number is null. Using fallback method.");
- int idColumnIndex = getIdColumnIndex(cursor);
- String idColumnName = cursor.getColumnName(idColumnIndex);
- String contactId = cursor.getString(idColumnIndex);
- getNumberFromContactId(cr, idColumnName, contactId);
- }
- return number;
- }
-
- /**
- * Return the phone number for the given contact id.
- *
- * @param columnName On some phones, we have to use non-standard columns for the primary key.
- * @param id The value in the columnName for the desired contact.
- * @return The phone number for the given contact or empty string if there was an error.
- */
- public static String getNumberFromContactId(ContentResolver cr, String columnName, String id) {
- if (TextUtils.isEmpty(id)) {
- Log.e(TAG, "You must specify a valid id to get a contact's phone number.");
- return "";
- }
- if (sNumberCache == null) {
- sNumberCache = new HashMap<>();
- } else if (sNumberCache.containsKey(id)) {
- return sNumberCache.get(id);
- }
-
- Uri uri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI;
- Cursor phoneNumberCursor = cr.query(uri,
- new String[]{ContactsContract.CommonDataKinds.Phone.NUMBER},
- columnName + " = ?", new String[]{id}, null);
-
- if (!phoneNumberCursor.moveToFirst()) {
- Log.e(TAG, "Unable to move phone number cursor to the first item.");
- return "";
- }
- String number = phoneNumberCursor.getString(0);
- phoneNumberCursor.close();
- return number;
- }
-}
diff --git a/src/com/android/car/dialer/telecom/ProjectionCallHandler.java b/src/com/android/car/dialer/telecom/ProjectionCallHandler.java
new file mode 100644
index 00000000..c29b1fc2
--- /dev/null
+++ b/src/com/android/car/dialer/telecom/ProjectionCallHandler.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.dialer.telecom;
+
+import android.annotation.Nullable;
+import android.bluetooth.BluetoothDevice;
+import android.car.Car;
+import android.car.CarProjectionManager;
+import android.car.projection.ProjectionStatus;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.telecom.Call;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+
+import com.android.car.dialer.log.L;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Collections;
+import java.util.List;
+
+class ProjectionCallHandler implements InCallServiceImpl.ActiveCallListChangedCallback,
+ CarProjectionManager.ProjectionStatusListener {
+ private static final String TAG = "CD.ProjectionCallHandler";
+
+ @VisibleForTesting static final String HFP_CLIENT_SCHEME = "hfpc";
+ @VisibleForTesting static final String PROJECTION_STATUS_EXTRA_HANDLES_PHONE_UI =
+ "android.car.projection.HANDLES_PHONE_UI";
+ @VisibleForTesting static final String PROJECTION_STATUS_EXTRA_DEVICE_STATE =
+ "android.car.projection.DEVICE_STATE";
+
+ private final CarProjectionManager mCarProjectionManager;
+ private final TelecomManager mTelecomManager;
+
+ private int mProjectionState = ProjectionStatus.PROJECTION_STATE_INACTIVE;
+ private List<ProjectionStatus> mProjectionDetails = Collections.emptyList();
+
+ ProjectionCallHandler(Context context) {
+ this(context.getSystemService(TelecomManager.class),
+ (CarProjectionManager)
+ Car.createCar(context).getCarManager(Car.PROJECTION_SERVICE));
+ }
+
+ @VisibleForTesting
+ ProjectionCallHandler(TelecomManager telecomManager, CarProjectionManager projectionManager) {
+ mTelecomManager = telecomManager;
+ mCarProjectionManager = projectionManager;
+ }
+
+ void start() {
+ mCarProjectionManager.registerProjectionStatusListener(this);
+ }
+
+ void stop() {
+ mCarProjectionManager.unregisterProjectionStatusListener(this);
+ }
+
+ @Override
+ public void onProjectionStatusChanged(
+ int state, String packageName, List<ProjectionStatus> details) {
+ mProjectionState = state;
+ mProjectionDetails = details;
+ }
+
+ @Override
+ public boolean onTelecomCallAdded(Call telecomCall) {
+ L.d(TAG, "onTelecomCallAdded(%s)", telecomCall);
+ if (mProjectionState != ProjectionStatus.PROJECTION_STATE_ACTIVE_BACKGROUND
+ && mProjectionState != ProjectionStatus.PROJECTION_STATE_ACTIVE_FOREGROUND) {
+ // Nothing's actively projecting, so no need to even check anything else.
+ return false;
+ }
+
+ if (mTelecomManager.isInEmergencyCall()) {
+ L.i(TAG, "Not suppressing UI for projection - in emergency call");
+ return false;
+ }
+
+ String bluetoothAddress = getHfpBluetoothAddressForCall(telecomCall);
+ if (bluetoothAddress == null) {
+ // Not an HFP call, so don't suppress UI.
+ return false;
+ }
+
+ return shouldSuppressCallUiForBluetoothDevice(bluetoothAddress);
+ }
+
+ @Override
+ public boolean onTelecomCallRemoved(Call telecomCall) {
+ return false;
+ }
+
+ @Nullable
+ private String getHfpBluetoothAddressForCall(Call call) {
+ Call.Details details = call.getDetails();
+ if (details == null) {
+ return null;
+ }
+
+ PhoneAccountHandle accountHandle = details.getAccountHandle();
+ PhoneAccount account = mTelecomManager.getPhoneAccount(accountHandle);
+ if (account == null) {
+ return null;
+ }
+
+ Uri address = account.getAddress();
+ if (address == null || !HFP_CLIENT_SCHEME.equals(address.getScheme())) {
+ return null;
+ }
+
+ return address.getSchemeSpecificPart();
+ }
+
+ private boolean shouldSuppressCallUiForBluetoothDevice(String bluetoothAddress) {
+ L.d(TAG, "shouldSuppressCallUiFor(%s)", bluetoothAddress);
+ for (ProjectionStatus status : mProjectionDetails) {
+ if (!status.isActive()) {
+ // Don't suppress UI for packages that aren't actively projecting.
+ L.d(TAG, "skip non-projecting package %s", status.getPackageName());
+ continue;
+ }
+
+ Bundle appExtras = status.getExtras();
+ if (!appExtras.getBoolean(PROJECTION_STATUS_EXTRA_HANDLES_PHONE_UI, true)) {
+ // Don't suppress UI for apps that say they don't handle phone UI.
+ continue;
+ }
+
+ for (ProjectionStatus.MobileDevice device : status.getConnectedMobileDevices()) {
+ if (!device.isProjecting()) {
+ // Don't suppress UI for devices that aren't foreground.
+ L.d(TAG, "skip non-projecting device %s", device.getName());
+ continue;
+ }
+
+ Bundle extras = device.getExtras();
+ if (extras.getInt(PROJECTION_STATUS_EXTRA_DEVICE_STATE,
+ ProjectionStatus.PROJECTION_STATE_ACTIVE_FOREGROUND)
+ != ProjectionStatus.PROJECTION_STATE_ACTIVE_FOREGROUND) {
+ L.d(TAG, "skip device %s - not foreground", device.getName());
+ continue;
+ }
+
+ Parcelable projectingBluetoothDevice =
+ extras.getParcelable(BluetoothDevice.EXTRA_DEVICE);
+
+ L.d(TAG, "Device %s has BT device %s", device.getName(), projectingBluetoothDevice);
+
+ if (projectingBluetoothDevice == null) {
+ L.i(TAG, "Suppressing in-call UI - device %s is projecting, and does not "
+ + "specify a Bluetooth address", device);
+ return true;
+ } else if (!(projectingBluetoothDevice instanceof BluetoothDevice)) {
+ L.e(TAG, "Device %s has bad EXTRA_DEVICE value %s - treating as unspecified",
+ device, projectingBluetoothDevice);
+ return true;
+ } else if (bluetoothAddress.equals(
+ ((BluetoothDevice) projectingBluetoothDevice).getAddress())) {
+ L.i(TAG, "Suppressing in-call UI - device %s is projecting, and call is coming "
+ + "from device's Bluetooth address %s", device, bluetoothAddress);
+ return true;
+ }
+ }
+ }
+
+ // No projecting apps want to suppress this device, so let it through.
+ return false;
+ }
+}
diff --git a/src/com/android/car/dialer/telecom/TelecomUtils.java b/src/com/android/car/dialer/telecom/TelecomUtils.java
deleted file mode 100644
index b5f058fa..00000000
--- a/src/com/android/car/dialer/telecom/TelecomUtils.java
+++ /dev/null
@@ -1,374 +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.car.dialer.telecom;
-
-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.BitmapFactory;
-import android.graphics.BitmapFactory.Options;
-import android.graphics.Rect;
-import android.net.Uri;
-import android.provider.ContactsContract;
-import android.provider.ContactsContract.CommonDataKinds.Phone;
-import android.provider.ContactsContract.PhoneLookup;
-import android.provider.Settings;
-import android.telecom.Call;
-import android.telephony.PhoneNumberUtils;
-import android.telephony.TelephonyManager;
-import android.text.TextUtils;
-import android.text.format.DateUtils;
-import android.util.Log;
-import android.widget.ImageView;
-
-import com.android.car.apps.common.LetterTileDrawable;
-import com.android.car.dialer.R;
-import com.android.car.dialer.ui.CircleBitmapDrawable;
-
-import java.io.InputStream;
-import java.util.Locale;
-
-import androidx.annotation.Nullable;
-import androidx.annotation.WorkerThread;
-
-public class TelecomUtils {
- private final static String TAG = "Em.TelecomUtils";
-
- private static final String[] CONTACT_ID_PROJECTION = new String[]{
- ContactsContract.PhoneLookup.DISPLAY_NAME,
- ContactsContract.PhoneLookup.TYPE,
- ContactsContract.PhoneLookup.LABEL,
- ContactsContract.PhoneLookup._ID
- };
-
- private static String sVoicemailNumber;
- private static TelephonyManager sTelephonyManager;
-
- @WorkerThread
- public static Bitmap getContactPhotoFromNumber(ContentResolver contentResolver, String number) {
- if (number == null) {
- return null;
- }
-
- int id = getContactIdFromNumber(contentResolver, number);
- if (id == 0) {
- return null;
- }
- return getContactPhotoFromId(contentResolver, id);
- }
-
- /**
- * Return the contact id for the given contact id
- *
- * @param id the contact id to get the photo for
- * @return the contact photo if it is found, null otherwise.
- */
- public static Bitmap getContactPhotoFromId(ContentResolver contentResolver, long id) {
- Uri photoUri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, id);
- InputStream photoDataStream = ContactsContract.Contacts.openContactPhotoInputStream(
- contentResolver, photoUri, true);
-
- Options options = new Options();
- options.inPreferQualityOverSpeed = true;
- // Scaling will be handled by later. We shouldn't scale multiple times to avoid
- // quality lost due to multiple potential scaling up and down.
- options.inScaled = false;
-
- Rect nullPadding = null;
- Bitmap photo = BitmapFactory.decodeStream(photoDataStream, nullPadding, options);
- if (photo != null) {
- photo.setDensity(Bitmap.DENSITY_NONE);
- }
- return photo;
- }
-
- /**
- * Return the contact id for the given phone number.
- *
- * @param number Caller phone number
- * @return the contact id if it is found, 0 otherwise.
- */
- public static int getContactIdFromNumber(ContentResolver cr, String number) {
- if (number == null || number.isEmpty()) {
- return 0;
- }
-
- Uri uri = Uri.withAppendedPath(
- ContactsContract.PhoneLookup.CONTENT_FILTER_URI,
- Uri.encode(number));
- Cursor cursor = cr.query(uri, CONTACT_ID_PROJECTION, null, null, null);
-
- try {
- if (cursor != null && cursor.moveToFirst()) {
- int id = cursor.getInt(cursor.getColumnIndex(ContactsContract.PhoneLookup._ID));
- return id;
- }
- } finally {
- if (cursor != null) {
- cursor.close();
- }
- }
- return 0;
- }
-
- /**
- * Return the label for the given phone number.
- *
- * @param number Caller phone number
- * @return the label if it is found, 0 otherwise.
- */
- public static CharSequence getTypeFromNumber(Context context, String number) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "getTypeFromNumber, number: " + number);
- }
- String defaultLabel = "";
- if (number == null || number.isEmpty()) {
- return defaultLabel;
- }
-
- ContentResolver cr = context.getContentResolver();
- Resources res = context.getResources();
- Uri uri = Uri.withAppendedPath(
- PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number));
- Cursor cursor = cr.query(uri, CONTACT_ID_PROJECTION, null, null, null);
-
- try {
- if (cursor != null && cursor.moveToFirst()) {
- int typeColumn = cursor.getColumnIndex(PhoneLookup.TYPE);
- int type = cursor.getInt(typeColumn);
- int labelColumn = cursor.getColumnIndex(PhoneLookup.LABEL);
- String label = cursor.getString(labelColumn);
- CharSequence typeLabel =
- Phone.getTypeLabel(res, type, label);
- return typeLabel;
- }
- } finally {
- if (cursor != null) {
- cursor.close();
- }
- }
- return defaultLabel;
- }
-
- public static String getVoicemailNumber(Context context) {
- if (sVoicemailNumber == null) {
- sVoicemailNumber = getTelephonyManager(context).getVoiceMailNumber();
- }
- return sVoicemailNumber;
- }
-
- public static TelephonyManager getTelephonyManager(Context context) {
- if (sTelephonyManager == null) {
- sTelephonyManager =
- (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
- }
- return sTelephonyManager;
- }
-
- public static String getFormattedNumber(Context context, String number) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "getFormattedNumber: " + number);
- }
- if (number == null) {
- return "";
- }
-
- String countryIso = getTelephonyManager(context).getSimCountryIso().toUpperCase(Locale.US);
- if (countryIso.length() != 2) {
- countryIso = Locale.getDefault().getCountry();
- if (countryIso == null || countryIso.length() != 2) {
- countryIso = "US";
- }
- }
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "PhoneNumberUtils.formatNumberToE16, number: "
- + number + ", country: " + countryIso);
- }
- String e164 = PhoneNumberUtils.formatNumberToE164(number, countryIso);
- String formattedNumber = PhoneNumberUtils.formatNumber(number, e164, countryIso);
- formattedNumber = TextUtils.isEmpty(formattedNumber) ? number : formattedNumber;
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "getFormattedNumber, result: " + formattedNumber);
- }
- return formattedNumber;
- }
-
- public static String getDisplayName(Context context, UiCall call) {
- // A call might get created before its children are added. In that case, the display name
- // would go from "Unknown" to "Conference call" therefore we don't want to cache it.
- if (call.hasChildren()) {
- return context.getString(R.string.conference_call);
- }
-
- return getDisplayName(context, call.getNumber(), call.getGatewayInfoOriginalAddress());
- }
-
- public static String getDisplayName(Context context, String number) {
- return getDisplayName(context, number, null);
- }
-
- private static String getDisplayName(Context context, String number,
- Uri gatewayOriginalAddress) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "getDisplayName: " + number
- + ", gatewayOriginalAddress: " + gatewayOriginalAddress);
- }
-
- if (TextUtils.isEmpty(number)) {
- return context.getString(R.string.unknown);
- }
- ContentResolver cr = context.getContentResolver();
- String name;
- if (number.equals(getVoicemailNumber(context))) {
- name = context.getResources().getString(R.string.voicemail);
- } else {
- name = getContactNameFromNumber(cr, number);
- }
-
- if (name == null) {
- name = getFormattedNumber(context, number);
- }
- if (name == null && gatewayOriginalAddress != null) {
- name = gatewayOriginalAddress.getSchemeSpecificPart();
- }
- if (name == null) {
- name = context.getString(R.string.unknown);
- }
- return name;
- }
-
- private static String getContactNameFromNumber(ContentResolver cr, String number) {
- Uri uri = Uri.withAppendedPath(
- ContactsContract.PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number));
-
- Cursor cursor = null;
- String name = null;
- try {
- cursor = cr.query(uri,
- new String[]{ContactsContract.PhoneLookup.DISPLAY_NAME}, null, null, null);
- if (cursor != null && cursor.moveToFirst()) {
- name = cursor.getString(0);
- }
- } finally {
- if (cursor != null) {
- cursor.close();
- }
- }
- return name;
- }
-
- /**
- * @return A formatted string that has information about the phone call
- * Possible strings:
- * "Mobile · Dialing"
- * "Mobile · 1:05"
- * "Bluetooth disconnected"
- */
- public static String getCallInfoText(Context context, UiCall call, CharSequence label) {
- String text;
- if (call.getState() == Call.STATE_ACTIVE) {
- long duration = System.currentTimeMillis() - call.getConnectTimeMillis();
- String durationString = DateUtils.formatElapsedTime(duration / 1000);
- if (!TextUtils.isEmpty(durationString) && !TextUtils.isEmpty(label)) {
- text = context.getString(R.string.phone_label_with_info, label, durationString);
- } else if (!TextUtils.isEmpty(durationString)) {
- text = durationString;
- } else if (!TextUtils.isEmpty(label)) {
- text = (String) label;
- } else {
- text = "";
- }
- } else {
- String state = callStateToUiString(context, call.getState());
- if (!TextUtils.isEmpty(label)) {
- text = context.getString(R.string.phone_label_with_info, label, state);
- } else {
- text = state;
- }
- }
- return text;
- }
-
- /**
- * @return A string representation of the call state that can be presented to a user.
- */
- public static String callStateToUiString(Context context, int state) {
- Resources res = context.getResources();
- switch (state) {
- case Call.STATE_ACTIVE:
- return res.getString(R.string.call_state_call_active);
- case Call.STATE_HOLDING:
- return res.getString(R.string.call_state_hold);
- case Call.STATE_NEW:
- case Call.STATE_CONNECTING:
- return res.getString(R.string.call_state_connecting);
- case Call.STATE_SELECT_PHONE_ACCOUNT:
- case Call.STATE_DIALING:
- return res.getString(R.string.call_state_dialing);
- case Call.STATE_DISCONNECTED:
- return res.getString(R.string.call_state_call_ended);
- case Call.STATE_RINGING:
- return res.getString(R.string.call_state_call_ringing);
- case Call.STATE_DISCONNECTING:
- return res.getString(R.string.call_state_call_ending);
- default:
- throw new IllegalStateException("Unknown Call State: " + state);
- }
- }
-
- public static boolean isNetworkAvailable(Context context) {
- TelephonyManager tm =
- (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
- return tm.getNetworkType() != TelephonyManager.NETWORK_TYPE_UNKNOWN &&
- tm.getSimState() == TelephonyManager.SIM_STATE_READY;
- }
-
- public static boolean isAirplaneModeOn(Context context) {
- return Settings.System.getInt(context.getContentResolver(),
- Settings.Global.AIRPLANE_MODE_ON, 0) != 0;
- }
-
- /**
- * Sets a Contact bitmap on the provided image taking into account fail cases.
- * It will attempt to load a Bitmap from the Contacts store, otherwise it will paint
- * a the first letter of the contact name.
- *
- * @param number A key to have a consisten color per phone number.
- * @return A worker task if a new one was needed to load the bitmap.
- */
- @Nullable
- public static ContactBitmapWorker setContactBitmapAsync(Context context,
- final ImageView icon, final @Nullable String name, final String number) {
- return ContactBitmapWorker.loadBitmap(context.getContentResolver(), icon, number,
- bitmap -> {
- Resources r = icon.getResources();
- if (bitmap != null) {
- icon.setScaleType(ImageView.ScaleType.CENTER_CROP);
- icon.setImageDrawable(new CircleBitmapDrawable(r, bitmap));
- } else {
- icon.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
- LetterTileDrawable letterTileDrawable = new LetterTileDrawable(r);
- letterTileDrawable.setContactDetails(name, number);
- letterTileDrawable.setIsCircular(true);
- icon.setImageDrawable(letterTileDrawable);
- }
- });
- }
-
-}
diff --git a/src/com/android/car/dialer/telecom/UiBluetoothMonitor.java b/src/com/android/car/dialer/telecom/UiBluetoothMonitor.java
new file mode 100644
index 00000000..1b290c7a
--- /dev/null
+++ b/src/com/android/car/dialer/telecom/UiBluetoothMonitor.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.telecom;
+
+import android.content.Context;
+
+import com.android.car.dialer.livedata.BluetoothHfpStateLiveData;
+import com.android.car.dialer.livedata.BluetoothPairListLiveData;
+import com.android.car.dialer.livedata.BluetoothStateLiveData;
+
+/**
+ * Class that responsible for getting status of bluetooth connections.
+ */
+public class UiBluetoothMonitor {
+ private static String TAG = "Em.BtMonitor";
+
+ private static UiBluetoothMonitor sUiBluetoothMonitor;
+
+ private final Context mContext;
+
+ private BluetoothHfpStateLiveData mHfpStateLiveData;
+ private BluetoothPairListLiveData mPairListLiveData;
+ private BluetoothStateLiveData mBluetoothStateLiveData;
+
+ /**
+ * Initialized a globally accessible {@link UiBluetoothMonitor} which can be retrieved by
+ * {@link #get}. If this function is called a second time before calling {@link #tearDown()},
+ * an exception will be thrown.
+ *
+ * @param applicationContext Application context.
+ */
+ // TODO: Create a singleton abstract class for Dialer common service.
+ public static UiBluetoothMonitor init(Context applicationContext) {
+ if (sUiBluetoothMonitor == null) {
+ sUiBluetoothMonitor = new UiBluetoothMonitor(applicationContext);
+ } else {
+ throw new IllegalStateException("UiBluetoothMonitor has been initialized.");
+ }
+
+ return sUiBluetoothMonitor;
+ }
+
+ public static UiBluetoothMonitor get() {
+ return sUiBluetoothMonitor;
+ }
+
+ private UiBluetoothMonitor(Context applicationContext) {
+ mContext = applicationContext;
+ }
+
+ /**
+ * Stops the {@link UiBluetoothMonitor}. Call this function when Dialer goes to background.
+ * {@link #get()} won't return a valid {@link UiBluetoothMonitor} after calling this function.
+ */
+ public void tearDown() {
+ sUiBluetoothMonitor = null;
+ }
+
+ /**
+ * Returns a LiveData which monitors the HFP profile state changes.
+ */
+ public BluetoothHfpStateLiveData getHfpStateLiveData() {
+ if (mHfpStateLiveData == null) {
+ mHfpStateLiveData = new BluetoothHfpStateLiveData(mContext);
+ }
+ return mHfpStateLiveData;
+ }
+
+ /**
+ * Returns a LiveData which monitors the paired device list changes.
+ */
+ public BluetoothPairListLiveData getPairListLiveData() {
+ if (mPairListLiveData == null) {
+ mPairListLiveData = new BluetoothPairListLiveData(mContext);
+ }
+ return mPairListLiveData;
+ }
+
+ /**
+ * Returns a LiveData which monitors the Bluetooth state changes.
+ */
+ public BluetoothStateLiveData getBluetoothStateLiveData() {
+ if (mBluetoothStateLiveData == null) {
+ mBluetoothStateLiveData = new BluetoothStateLiveData(mContext);
+ }
+ return mBluetoothStateLiveData;
+ }
+}
diff --git a/src/com/android/car/dialer/telecom/UiCall.java b/src/com/android/car/dialer/telecom/UiCall.java
deleted file mode 100644
index 7b98c649..00000000
--- a/src/com/android/car/dialer/telecom/UiCall.java
+++ /dev/null
@@ -1,97 +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.car.dialer.telecom;
-
-import android.net.Uri;
-
-/**
- * Represents a single call on UI. It is an abstraction of {@code android.telecom.Call}.
- */
-public class UiCall {
- private final int mId;
-
- private int mState;
- private boolean mHasParent;
- private String mNumber;
- private CharSequence mDisconnectCause;
- private boolean mHasChildren;
- private Uri mGatewayInfoOriginalAddress;
- private long connectTimeMillis;
-
- public UiCall(int id) {
- mId = id;
- }
-
- public int getId() {
- return mId;
- }
-
- public int getState() {
- return mState;
- }
-
- public void setState(int state) {
- mState = state;
- }
-
- public boolean hasParent() {
- return mHasParent;
- }
-
- public void setHasParent(boolean hasParent) {
- mHasParent = hasParent;
- }
-
- public void setHasChildren(boolean hasChildren) {
- mHasChildren = hasChildren;
- }
-
- public boolean hasChildren() {
- return mHasChildren;
- }
-
- public String getNumber() {
- return mNumber;
- }
-
- public void setNumber(String number) {
- mNumber = number;
- }
-
- public CharSequence getDisconnectCause() {
- return mDisconnectCause;
- }
-
- public void setDisconnectCause(CharSequence disconnectCause) {
- mDisconnectCause = disconnectCause;
- }
-
- public Uri getGatewayInfoOriginalAddress() {
- return mGatewayInfoOriginalAddress;
- }
-
- public void setGatewayInfoOriginalAddress(Uri gatewayInfoOriginalAddress) {
- mGatewayInfoOriginalAddress = gatewayInfoOriginalAddress;
- }
-
- public long getConnectTimeMillis() {
- return connectTimeMillis;
- }
-
- public void setConnectTimeMillis(long connectTimeMillis) {
- this.connectTimeMillis = connectTimeMillis;
- }
-}
diff --git a/src/com/android/car/dialer/telecom/UiCallManager.java b/src/com/android/car/dialer/telecom/UiCallManager.java
index be424872..b0dbe818 100644
--- a/src/com/android/car/dialer/telecom/UiCallManager.java
+++ b/src/com/android/car/dialer/telecom/UiCallManager.java
@@ -24,73 +24,47 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
-import android.database.Cursor;
import android.net.Uri;
import android.os.IBinder;
-import android.provider.CallLog;
import android.telecom.Call;
import android.telecom.CallAudioState;
import android.telecom.CallAudioState.CallAudioRoute;
-import android.telecom.DisconnectCause;
-import android.telecom.GatewayInfo;
-import android.telecom.InCallService;
+import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
-import android.telephony.TelephonyManager;
import android.text.TextUtils;
-import android.util.Log;
+import android.widget.Toast;
-import com.android.car.dialer.CallListener;
import com.android.car.dialer.R;
+import com.android.car.dialer.log.L;
+import com.android.car.telephony.common.TelecomUtils;
+import com.android.internal.annotations.VisibleForTesting;
+
+import com.google.i18n.phonenumbers.PhoneNumberUtil;
+import com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberType;
+import com.google.i18n.phonenumbers.PhoneNumberUtil.ValidationResult;
+import com.google.i18n.phonenumbers.Phonenumber;
-import java.lang.ref.WeakReference;
import java.util.ArrayList;
-import java.util.Calendar;
import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashMap;
import java.util.List;
-import java.util.Map;
-import java.util.concurrent.CopyOnWriteArrayList;
/**
* The entry point for all interactions between UI and telecom.
*/
public class UiCallManager {
- private static String TAG = "Em.TelecomMgr";
+ private static String TAG = "CD.TelecomMgr";
- private static final String HFP_CLIENT_CONNECTION_SERVICE_CLASS_NAME
+ @VisibleForTesting
+ static final String HFP_CLIENT_CONNECTION_SERVICE_CLASS_NAME
= "com.android.bluetooth.hfpclient.connserv.HfpClientConnectionService";
- // Rate limit how often you can place outgoing calls.
- private static final long MIN_TIME_BETWEEN_CALLS_MS = 3000;
- private static final List<Integer> sCallStateRank = new ArrayList<>();
private static UiCallManager sUiCallManager;
- // Used to assign id's to UiCall objects as they're created.
- private static int nextCarPhoneCallId = 0;
-
- static {
- // States should be added from lowest rank to highest
- sCallStateRank.add(Call.STATE_DISCONNECTED);
- sCallStateRank.add(Call.STATE_DISCONNECTING);
- sCallStateRank.add(Call.STATE_NEW);
- sCallStateRank.add(Call.STATE_CONNECTING);
- sCallStateRank.add(Call.STATE_SELECT_PHONE_ACCOUNT);
- sCallStateRank.add(Call.STATE_HOLDING);
- sCallStateRank.add(Call.STATE_ACTIVE);
- sCallStateRank.add(Call.STATE_DIALING);
- sCallStateRank.add(Call.STATE_RINGING);
- }
-
private Context mContext;
- private TelephonyManager mTelephonyManager;
- private long mLastPlacedCallTimeMs;
private TelecomManager mTelecomManager;
private InCallServiceImpl mInCallService;
private BluetoothHeadsetClient mBluetoothHeadsetClient;
- private final Map<UiCall, Call> mCallMapping = new HashMap<>();
- private final List<CallListener> mCallListeners = new CopyOnWriteArrayList<>();
/**
* Initialized a globally accessible {@link UiCallManager} which can be retrieved by
@@ -120,13 +94,17 @@ public class UiCallManager {
return sUiCallManager;
}
- private UiCallManager(Context context) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "SetUp");
- }
+ /**
+ * This is used only for testing
+ */
+ @VisibleForTesting
+ public static void set(UiCallManager uiCallManager) {
+ sUiCallManager = uiCallManager;
+ }
+ private UiCallManager(Context context) {
+ L.d(TAG, "SetUp");
mContext = context;
- mTelephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
mTelecomManager = (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE);
Intent intent = new Intent(context, InCallServiceImpl.class);
@@ -154,45 +132,15 @@ public class UiCallManager {
@Override
public void onServiceConnected(ComponentName name, IBinder binder) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "onServiceConnected: " + name + ", service: " + binder);
- }
+ L.d(TAG, "onServiceConnected: %s, service: %s", name, binder);
mInCallService = ((InCallServiceImpl.LocalBinder) binder).getService();
- mInCallService.registerCallback(mInCallServiceCallback);
-
- // The InCallServiceImpl could be bound when we already have some active calls, let's
- // notify UI about these calls.
- for (Call telecomCall : mInCallService.getCalls()) {
- UiCall uiCall = doTelecomCallAdded(telecomCall);
- onStateChanged(uiCall, uiCall.getState());
- }
}
@Override
public void onServiceDisconnected(ComponentName name) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "onServiceDisconnected: " + name);
- }
- mInCallService.unregisterCallback(mInCallServiceCallback);
+ L.d(TAG, "onServiceDisconnected: %s", name);
+ mInCallService = null;
}
-
- private InCallServiceImpl.Callback mInCallServiceCallback =
- new InCallServiceImpl.Callback() {
- @Override
- public void onTelecomCallAdded(Call telecomCall) {
- doTelecomCallAdded(telecomCall);
- }
-
- @Override
- public void onTelecomCallRemoved(Call telecomCall) {
- doTelecomCallRemoved(telecomCall);
- }
-
- @Override
- public void onCallAudioStateChanged(CallAudioState audioState) {
- doCallAudioStateChanged(audioState);
- }
- };
};
/**
@@ -205,77 +153,13 @@ public class UiCallManager {
mContext.unbindService(mInCallServiceConnection);
mInCallService = null;
}
- mCallMapping.clear();
// Clear out the mContext reference to avoid memory leak.
mContext = null;
sUiCallManager = null;
}
- public void addListener(CallListener listener) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "addListener: " + listener);
- }
- mCallListeners.add(listener);
- }
-
- public void removeListener(CallListener listener) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "removeListener: " + listener);
- }
- mCallListeners.remove(listener);
- }
-
- protected void placeCall(String number) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "placeCall: " + number);
- }
- Uri uri = Uri.fromParts("tel", number, null);
- Log.d(TAG, "android.telecom.TelecomManager#placeCall: " + uri);
- mTelecomManager.placeCall(uri, null);
- }
-
- public void answerCall(UiCall uiCall) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "answerCall: " + uiCall);
- }
-
- Call telecomCall = mCallMapping.get(uiCall);
- if (telecomCall != null) {
- telecomCall.answer(0);
- }
- }
-
- public void rejectCall(UiCall uiCall, boolean rejectWithMessage, String textMessage) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "rejectCall: " + uiCall + ", rejectWithMessage: " + rejectWithMessage
- + "textMessage: " + textMessage);
- }
-
- Call telecomCall = mCallMapping.get(uiCall);
- if (telecomCall != null) {
- telecomCall.reject(rejectWithMessage, textMessage);
- }
- }
-
- public void disconnectCall(UiCall uiCall) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "disconnectCall: " + uiCall);
- }
-
- Call telecomCall = mCallMapping.get(uiCall);
- if (telecomCall != null) {
- telecomCall.disconnect();
- }
- }
-
- public List<UiCall> getCalls() {
- return new ArrayList<>(mCallMapping.keySet());
- }
-
public boolean getMuted() {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "getMuted");
- }
+ L.d(TAG, "getMuted");
if (mInCallService == null) {
return false;
}
@@ -284,9 +168,7 @@ public class UiCallManager {
}
public void setMuted(boolean muted) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "setMuted: " + muted);
- }
+ L.d(TAG, "setMuted: " + muted);
if (mInCallService == null) {
return;
}
@@ -294,9 +176,7 @@ public class UiCallManager {
}
public int getSupportedAudioRouteMask() {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "getSupportedAudioRouteMask");
- }
+ L.d(TAG, "getSupportedAudioRouteMask");
CallAudioState audioState = getCallAudioStateOrNull();
return audioState != null ? audioState.getSupportedRouteMask() : 0;
@@ -342,13 +222,29 @@ public class UiCallManager {
}
}
+ /**
+ * Returns the current audio route.
+ * The available routes are defined in {@link CallAudioState}.
+ */
public int getAudioRoute() {
- CallAudioState audioState = getCallAudioStateOrNull();
- int audioRoute = audioState != null ? audioState.getRoute() : 0;
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "getAudioRoute " + audioRoute);
+ if (isBluetoothCall()
+ && mBluetoothHeadsetClient != null
+ && !mBluetoothHeadsetClient.getConnectedDevices().isEmpty()) {
+ // TODO: Make this handle multiple devices
+ BluetoothDevice device = mBluetoothHeadsetClient.getConnectedDevices().get(0);
+ int audioState = mBluetoothHeadsetClient.getAudioState(device);
+
+ if (audioState == BluetoothHeadsetClient.STATE_AUDIO_CONNECTED) {
+ return CallAudioState.ROUTE_BLUETOOTH;
+ } else {
+ return CallAudioState.ROUTE_EARPIECE;
+ }
+ } else {
+ CallAudioState audioState = getCallAudioStateOrNull();
+ int audioRoute = audioState != null ? audioState.getRoute() : 0;
+ L.d(TAG, "getAudioRoute " + audioRoute);
+ return audioRoute;
}
- return audioRoute;
}
/**
@@ -371,410 +267,79 @@ public class UiCallManager {
// TODO: Implement routing audio if current call is not a bluetooth call.
}
- public void holdCall(UiCall uiCall) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "holdCall: " + uiCall);
- }
-
- Call telecomCall = mCallMapping.get(uiCall);
- if (telecomCall != null) {
- telecomCall.hold();
- }
- }
-
- public void unholdCall(UiCall uiCall) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "unholdCall: " + uiCall);
- }
-
- Call telecomCall = mCallMapping.get(uiCall);
- if (telecomCall != null) {
- telecomCall.unhold();
- }
- }
-
- public void playDtmfTone(UiCall uiCall, char digit) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "playDtmfTone: call: " + uiCall + ", digit: " + digit);
- }
-
- Call telecomCall = mCallMapping.get(uiCall);
- if (telecomCall != null) {
- telecomCall.playDtmfTone(digit);
- }
- }
-
- public void stopDtmfTone(UiCall uiCall) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "stopDtmfTone: call: " + uiCall);
- }
-
- Call telecomCall = mCallMapping.get(uiCall);
- if (telecomCall != null) {
- telecomCall.stopDtmfTone();
- }
- }
-
- public void postDialContinue(UiCall uiCall, boolean proceed) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "postDialContinue: call: " + uiCall + ", proceed: " + proceed);
- }
-
- Call telecomCall = mCallMapping.get(uiCall);
- if (telecomCall != null) {
- telecomCall.postDialContinue(proceed);
- }
- }
-
- public void conference(UiCall uiCall, UiCall otherUiCall) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "conference: call: " + uiCall + ", otherCall: " + otherUiCall);
- }
-
- Call telecomCall = mCallMapping.get(uiCall);
- Call otherTelecomCall = mCallMapping.get(otherUiCall);
- if (telecomCall != null) {
- telecomCall.conference(otherTelecomCall);
- }
- }
-
- public void splitFromConference(UiCall uiCall) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "splitFromConference: call: " + uiCall);
- }
-
- Call telecomCall = mCallMapping.get(uiCall);
- if (telecomCall != null) {
- telecomCall.splitFromConference();
- }
- }
-
- private UiCall doTelecomCallAdded(final Call telecomCall) {
- Log.d(TAG, "doTelecomCallAdded: " + telecomCall);
-
- UiCall uiCall = getOrCreateCallContainer(telecomCall);
- telecomCall.registerCallback(new TelecomCallListener(this, uiCall));
- for (CallListener listener : mCallListeners) {
- listener.onCallAdded(uiCall);
- }
- Log.d(TAG, "Call backs registered");
-
- if (telecomCall.getState() == Call.STATE_SELECT_PHONE_ACCOUNT) {
- // TODO(b/26189994): need to show Phone Account picker to let user choose a phone
- // account. It should be an account from TelecomManager#getCallCapablePhoneAccounts
- // list.
- Log.w(TAG, "Need to select phone account for the given call: " + telecomCall + ", "
- + "but this feature is not implemented yet.");
- telecomCall.disconnect();
- }
- return uiCall;
- }
-
- private void doTelecomCallRemoved(Call telecomCall) {
- UiCall uiCall = getOrCreateCallContainer(telecomCall);
-
- mCallMapping.remove(uiCall);
-
- for (CallListener listener : mCallListeners) {
- listener.onCallRemoved(uiCall);
- }
- }
-
- private void doCallAudioStateChanged(CallAudioState audioState) {
- for (CallListener listener : mCallListeners) {
- listener.onAudioStateChanged(audioState.isMuted(), audioState.getRoute(),
- audioState.getSupportedRouteMask());
- }
- }
-
- private void onStateChanged(UiCall uiCall, int state) {
- for (CallListener listener : mCallListeners) {
- listener.onCallStateChanged(uiCall, state);
- }
- }
-
- private void onCallUpdated(UiCall uiCall) {
- for (CallListener listener : mCallListeners) {
- listener.onCallUpdated(uiCall);
- }
- }
-
- private UiCall getOrCreateCallContainer(Call telecomCall) {
- for (Map.Entry<UiCall, Call> entry : mCallMapping.entrySet()) {
- if (entry.getValue() == telecomCall) {
- return entry.getKey();
- }
- }
-
- UiCall uiCall = new UiCall(nextCarPhoneCallId++);
- updateCallContainerFromTelecom(uiCall, telecomCall);
- mCallMapping.put(uiCall, telecomCall);
- return uiCall;
- }
-
- private static void updateCallContainerFromTelecom(UiCall uiCall, Call telecomCall) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "updateCallContainerFromTelecom: call: " + uiCall + ", telecomCall: "
- + telecomCall);
- }
-
- uiCall.setState(telecomCall.getState());
- uiCall.setHasChildren(!telecomCall.getChildren().isEmpty());
- uiCall.setHasParent(telecomCall.getParent() != null);
-
- Call.Details details = telecomCall.getDetails();
- if (details == null) {
- return;
- }
-
- uiCall.setConnectTimeMillis(details.getConnectTimeMillis());
-
- DisconnectCause cause = details.getDisconnectCause();
- uiCall.setDisconnectCause(cause == null ? null : cause.getLabel());
-
- GatewayInfo gatewayInfo = details.getGatewayInfo();
- uiCall.setGatewayInfoOriginalAddress(
- gatewayInfo == null ? null : gatewayInfo.getOriginalAddress());
-
- String number = "";
- if (gatewayInfo != null) {
- number = gatewayInfo.getOriginalAddress().getSchemeSpecificPart();
- } else if (details.getHandle() != null) {
- number = details.getHandle().getSchemeSpecificPart();
- }
- uiCall.setNumber(number);
- }
-
private CallAudioState getCallAudioStateOrNull() {
return mInCallService != null ? mInCallService.getCallAudioState() : null;
}
- /** Returns a first call that matches at least one provided call state */
- public UiCall getCallWithState(int... callStates) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "getCallWithState: " + callStates);
- }
- for (UiCall call : getCalls()) {
- for (int callState : callStates) {
- if (call.getState() == callState) {
- return call;
- }
- }
- }
- return null;
- }
-
- public UiCall getPrimaryCall() {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "getPrimaryCall");
- }
- List<UiCall> calls = getCalls();
- if (calls.isEmpty()) {
- return null;
- }
-
- Collections.sort(calls, getCallComparator());
- UiCall uiCall = calls.get(0);
- if (uiCall.hasParent()) {
- return null;
- }
- return uiCall;
- }
-
- public UiCall getSecondaryCall() {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "getSecondaryCall");
- }
- List<UiCall> calls = getCalls();
- if (calls.size() < 2) {
- return null;
- }
-
- Collections.sort(calls, getCallComparator());
- UiCall uiCall = calls.get(1);
- if (uiCall.hasParent()) {
- return null;
- }
- return uiCall;
- }
-
- public static final int CAN_PLACE_CALL_RESULT_OK = 0;
- public static final int CAN_PLACE_CALL_RESULT_NETWORK_UNAVAILABLE = 1;
- public static final int CAN_PLACE_CALL_RESULT_HFP_UNAVAILABLE = 2;
- public static final int CAN_PLACE_CALL_RESULT_AIRPLANE_MODE = 3;
-
- public int getCanPlaceCallStatus(String number, boolean bluetoothRequired) {
- // TODO(b/26191392): figure out the logic for projected and embedded modes
- return CAN_PLACE_CALL_RESULT_OK;
- }
-
- public String getFailToPlaceCallMessage(int canPlaceCallResult) {
- switch (canPlaceCallResult) {
- case CAN_PLACE_CALL_RESULT_OK:
- return "";
- case CAN_PLACE_CALL_RESULT_HFP_UNAVAILABLE:
- return mContext.getString(R.string.error_no_hfp);
- case CAN_PLACE_CALL_RESULT_AIRPLANE_MODE:
- return mContext.getString(R.string.error_airplane_mode);
- case CAN_PLACE_CALL_RESULT_NETWORK_UNAVAILABLE:
- default:
- return mContext.getString(R.string.error_network_not_available);
+ /**
+ * Places call through TelecomManager
+ *
+ * @return {@code true} if a call is successfully placed, false if number is invalid.
+ */
+ public boolean placeCall(String number) {
+ if (isValidNumber(number)) {
+ Uri uri = Uri.fromParts("tel", number, null);
+ L.d(TAG, "android.telecom.TelecomManager#placeCall: %s", number);
+ mTelecomManager.placeCall(uri, null);
+ return true;
+ } else {
+ L.d(TAG, "invalid number dialed", number);
+ Toast.makeText(mContext, R.string.error_invalid_phone_number,
+ Toast.LENGTH_SHORT).show();
+ return false;
}
}
- /** Places call only if there's no outgoing call right now */
- public void safePlaceCall(String number, boolean bluetoothRequired) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "safePlaceCall: " + number);
- }
-
- int placeCallStatus = getCanPlaceCallStatus(number, bluetoothRequired);
- if (placeCallStatus != CAN_PLACE_CALL_RESULT_OK) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Unable to place a call: " + placeCallStatus);
- }
- return;
- }
-
- UiCall outgoingCall = getCallWithState(
- Call.STATE_CONNECTING, Call.STATE_NEW, Call.STATE_DIALING);
- if (outgoingCall == null) {
- long now = Calendar.getInstance().getTimeInMillis();
- if (now - mLastPlacedCallTimeMs > MIN_TIME_BETWEEN_CALLS_MS) {
- placeCall(number);
- mLastPlacedCallTimeMs = now;
- } else {
- if (Log.isLoggable(TAG, Log.INFO)) {
- Log.i(TAG, "You have to wait " + MIN_TIME_BETWEEN_CALLS_MS
- + "ms between making calls");
+ /**
+ * Runs basic validation check of a phone number, to verify it is the correct length
+ * in an internationalized way. Further validation on whether the number actually exists
+ * is left for the phone carrier.
+ */
+ private boolean isValidNumber(String number) {
+ Phonenumber.PhoneNumber phoneNumber = TelecomUtils.createI18nPhoneNumber(mContext,
+ number);
+ if (phoneNumber != null) {
+ for (PhoneNumberType type : PhoneNumberType.values()) {
+ ValidationResult result =
+ PhoneNumberUtil.getInstance().isPossibleNumberForTypeWithReason(phoneNumber,
+ type);
+ if (result != ValidationResult.TOO_SHORT && result != ValidationResult.TOO_LONG) {
+ return true;
}
}
}
+ return false;
}
public void callVoicemail() {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "callVoicemail");
- }
+ L.d(TAG, "callVoicemail");
String voicemailNumber = TelecomUtils.getVoicemailNumber(mContext);
if (TextUtils.isEmpty(voicemailNumber)) {
- Log.w(TAG, "Unable to get voicemail number.");
+ L.w(TAG, "Unable to get voicemail number.");
return;
}
- safePlaceCall(voicemailNumber, false);
+ placeCall(voicemailNumber);
}
- /**
- * Returns the call types for the given number of items in the cursor.
- * <p/>
- * It uses the next {@code count} rows in the cursor to extract the types.
- * <p/>
- * Its position in the cursor is unchanged by this function.
- */
- public int[] getCallTypes(Cursor cursor, int count) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "getCallTypes: cursor: " + cursor + ", count: " + count);
- }
-
- int position = cursor.getPosition();
- int[] callTypes = new int[count];
- String voicemailNumber = mTelephonyManager.getVoiceMailNumber();
- int column;
- for (int index = 0; index < count; ++index) {
- column = cursor.getColumnIndex(CallLog.Calls.NUMBER);
- String phoneNumber = cursor.getString(column);
- if (phoneNumber != null && phoneNumber.equals(voicemailNumber)) {
- callTypes[index] = PhoneLoader.VOICEMAIL_TYPE;
- } else {
- column = cursor.getColumnIndex(CallLog.Calls.TYPE);
- callTypes[index] = cursor.getInt(column);
+ /** Check if emergency call is supported by any phone account. */
+ public boolean isEmergencyCallSupported() {
+ List<PhoneAccountHandle> phoneAccountHandleList =
+ mTelecomManager.getCallCapablePhoneAccounts();
+ for (PhoneAccountHandle phoneAccountHandle : phoneAccountHandleList) {
+ PhoneAccount phoneAccount = mTelecomManager.getPhoneAccount(phoneAccountHandle);
+ L.d(TAG, "phoneAccount: %s", phoneAccount);
+ if (phoneAccount != null && phoneAccount.hasCapabilities(
+ PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS)) {
+ return true;
}
- cursor.moveToNext();
}
- cursor.moveToPosition(position);
- return callTypes;
- }
-
- private static Comparator<UiCall> getCallComparator() {
- return new Comparator<UiCall>() {
- @Override
- public int compare(UiCall call, UiCall otherCall) {
- if (call.hasParent() && !otherCall.hasParent()) {
- return 1;
- } else if (!call.hasParent() && otherCall.hasParent()) {
- return -1;
- }
- int carCallRank = sCallStateRank.indexOf(call.getState());
- int otherCarCallRank = sCallStateRank.indexOf(otherCall.getState());
-
- return otherCarCallRank - carCallRank;
- }
- };
+ return false;
}
- private static class TelecomCallListener extends Call.Callback {
- private final WeakReference<UiCallManager> mCarTelecomMangerRef;
- private final WeakReference<UiCall> mCallContainerRef;
-
- TelecomCallListener(UiCallManager carTelecomManager, UiCall uiCall) {
- mCarTelecomMangerRef = new WeakReference<>(carTelecomManager);
- mCallContainerRef = new WeakReference<>(uiCall);
- }
-
- @Override
- public void onStateChanged(Call telecomCall, int state) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "onStateChanged: " + state);
- }
- UiCallManager manager = mCarTelecomMangerRef.get();
- UiCall call = mCallContainerRef.get();
- if (manager != null && call != null) {
- call.setState(state);
- manager.onStateChanged(call, state);
- }
- }
-
- @Override
- public void onParentChanged(Call telecomCall, Call parent) {
- doCallUpdated(telecomCall);
- }
- @Override
- public void onCallDestroyed(Call telecomCall) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "onCallDestroyed");
- }
- }
-
- @Override
- public void onDetailsChanged(Call telecomCall, Call.Details details) {
- doCallUpdated(telecomCall);
- }
-
- @Override
- public void onVideoCallChanged(Call telecomCall, InCallService.VideoCall videoCall) {
- doCallUpdated(telecomCall);
- }
-
- @Override
- public void onCannedTextResponsesLoaded(Call telecomCall,
- List<String> cannedTextResponses) {
- doCallUpdated(telecomCall);
- }
-
- @Override
- public void onChildrenChanged(Call telecomCall, List<Call> children) {
- doCallUpdated(telecomCall);
- }
-
- private void doCallUpdated(Call telecomCall) {
- UiCallManager manager = mCarTelecomMangerRef.get();
- UiCall uiCall = mCallContainerRef.get();
- if (manager != null && uiCall != null) {
- updateCallContainerFromTelecom(uiCall, telecomCall);
- manager.onCallUpdated(uiCall);
- }
- }
+ /** Return the current active call list from delegated {@link InCallServiceImpl} */
+ public List<Call> getCallList() {
+ return mInCallService == null ? Collections.emptyList() : mInCallService.getCalls();
}
}
diff --git a/src/com/android/car/dialer/ui/CallHistoryFragment.java b/src/com/android/car/dialer/ui/CallHistoryFragment.java
deleted file mode 100644
index e44e6b25..00000000
--- a/src/com/android/car/dialer/ui/CallHistoryFragment.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.car.dialer.ui;
-
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import com.android.car.dialer.R;
-import com.android.car.dialer.telecom.PhoneLoader;
-import com.android.car.dialer.ui.viewmodel.CallHistoryViewModel;
-
-import java.util.List;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.car.widget.ListItemAdapter;
-import androidx.car.widget.PagedListView;
-import androidx.fragment.app.Fragment;
-import androidx.lifecycle.LiveData;
-import androidx.lifecycle.ViewModelProviders;
-
-public class CallHistoryFragment extends Fragment {
- public static final String CALL_TYPE_KEY = "CALL_TYPE_KEY";
-
- public static CallHistoryFragment newInstance(@PhoneLoader.CallType int callType) {
- CallHistoryFragment fragment = new CallHistoryFragment();
- Bundle arg = new Bundle();
- arg.putInt(CALL_TYPE_KEY, callType);
- fragment.setArguments(arg);
- return fragment;
- }
-
- @Nullable
- @Override
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
- @Nullable Bundle savedInstanceState) {
- View fragmentView = inflater.inflate(R.layout.call_list_fragment, container, false);
- PagedListView pagedListView = fragmentView.findViewById(R.id.list_view);
- CallHistoryListItemProvider callHistoryListItemProvider = new CallHistoryListItemProvider();
- ListItemAdapter adapter = new ListItemAdapter(getContext(), callHistoryListItemProvider);
- pagedListView.setAdapter(adapter);
-
- int callType = getArguments().getInt(CALL_TYPE_KEY);
-
- CallHistoryViewModel viewModel = ViewModelProviders.of(this).get(
- CallHistoryViewModel.class);
-
- LiveData<List<CallLogListingTask.CallLogItem>> liveData = null;
- if (callType == PhoneLoader.CallType.CALL_TYPE_ALL) {
- liveData = viewModel.getCallHistory();
- } else if (callType == PhoneLoader.CallType.MISSED_TYPE) {
- liveData = viewModel.getMissedCallHistory();
- }
-
- if (liveData != null) {
- liveData.observe(this,
- callHistoryItems -> {
- callHistoryListItemProvider.setCallHistoryListItems(getContext(),
- callHistoryItems);
- adapter.notifyDataSetChanged();
- });
- }
-
- return fragmentView;
- }
-}
diff --git a/src/com/android/car/dialer/ui/CallHistoryListItemProvider.java b/src/com/android/car/dialer/ui/CallHistoryListItemProvider.java
deleted file mode 100644
index adfa655e..00000000
--- a/src/com/android/car/dialer/ui/CallHistoryListItemProvider.java
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.car.dialer.ui;
-
-import android.content.Context;
-import android.graphics.drawable.BitmapDrawable;
-
-import com.android.car.dialer.telecom.UiCallManager;
-import com.android.car.dialer.ui.listitem.CallLogListItem;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import androidx.car.widget.ListItem;
-import androidx.car.widget.ListItemProvider;
-import androidx.car.widget.TextListItem;
-
-public class CallHistoryListItemProvider extends ListItemProvider {
-
- private List<TextListItem> mItems = new ArrayList<>();
-
- public void setCallHistoryListItems(Context context,
- List<CallLogListingTask.CallLogItem> items) {
- for (CallLogListingTask.CallLogItem callLogItem : items) {
- TextListItem callLogListItem = new CallLogListItem(context, callLogItem);
-
- callLogListItem.setPrimaryActionIcon(
- new BitmapDrawable(context.getResources(), callLogItem.mIcon), true);
- callLogListItem.setTitle(callLogItem.mTitle);
- callLogListItem.setBody(callLogItem.mText);
- callLogListItem.setOnClickListener(
- (v) -> UiCallManager.get().safePlaceCall(callLogItem.mNumber, false));
-
- mItems.add(callLogListItem);
- }
- }
-
- @Override
- public ListItem get(int position) {
- return mItems.get(position);
- }
-
- @Override
- public int size() {
- return mItems.size();
- }
-}
diff --git a/src/com/android/car/dialer/ui/CallLogListingTask.java b/src/com/android/car/dialer/ui/CallLogListingTask.java
deleted file mode 100644
index 8f8e06c6..00000000
--- a/src/com/android/car/dialer/ui/CallLogListingTask.java
+++ /dev/null
@@ -1,182 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.car.dialer.ui;
-
-import static android.provider.ContactsContract.CommonDataKinds.Phone.getTypeLabel;
-
-import android.content.Context;
-import android.database.Cursor;
-import android.graphics.Bitmap;
-import android.os.AsyncTask;
-import android.provider.CallLog;
-import android.telephony.PhoneNumberUtils;
-import android.text.TextUtils;
-import android.text.format.DateUtils;
-
-import com.android.car.dialer.ContactEntry;
-import com.android.car.dialer.R;
-import com.android.car.dialer.telecom.InMemoryPhoneBook;
-import com.android.car.dialer.telecom.PhoneLoader;
-import com.android.car.dialer.telecom.TelecomUtils;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import androidx.annotation.NonNull;
-
-/**
- * Async task which loads call history.
- */
-public class CallLogListingTask extends AsyncTask<Void, Void, Void> {
- public static class CallLogItem {
- public final String mTitle;
- public final String mText;
- public final String mNumber;
- public final Bitmap mIcon;
-
- public CallLogItem(String title, String text, String number, Bitmap icon) {
- mTitle = title;
- mText = text;
- mNumber = number;
- mIcon = icon;
- }
- }
-
- public interface LoadCompleteListener {
- void onLoadComplete(List<CallLogItem> items);
- }
-
- private Context mContext;
- private Cursor mCursor;
- private List<CallLogItem> mItems;
- private LoadCompleteListener mListener;
-
- public CallLogListingTask(Context context, Cursor cursor,
- @NonNull LoadCompleteListener listener) {
- mContext = context;
- mCursor = cursor;
- mItems = new ArrayList<>(mCursor.getCount());
- mListener = listener;
- }
-
- private String maybeAppendCount(StringBuilder sb, int count) {
- if (count > 1) {
- sb.append(" (").append(count).append(")");
- }
- return sb.toString();
- }
-
- private String getContactName(String cachedName, String number, int count,
- boolean isVoicemail) {
- StringBuilder sb = new StringBuilder();
- if (isVoicemail) {
- sb.append(mContext.getString(R.string.voicemail));
- } else if (cachedName != null) {
- sb.append(cachedName);
- } else if (!TextUtils.isEmpty(number)) {
- sb.append(TelecomUtils.getFormattedNumber(mContext, number));
- } else {
- sb.append(mContext.getString(R.string.unknown));
- }
- return maybeAppendCount(sb, count);
- }
-
- private static CharSequence getRelativeTime(long millis) {
- boolean validTimestamp = millis > 0;
-
- return validTimestamp ? DateUtils.getRelativeTimeSpanString(
- millis, System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS,
- DateUtils.FORMAT_ABBREV_RELATIVE) : null;
- }
-
- @Override
- protected Void doInBackground(Void... voids) {
- if (mCursor != null) {
- try {
- int numberColumn = PhoneLoader.getNumberColumnIndex(mCursor);
- int dateColumn = mCursor.getColumnIndex(CallLog.Calls.DATE);
-
- while (mCursor.moveToNext()) {
- int count = 1;
- String number = mCursor.getString(numberColumn);
- // We want to group calls to the same number into one so seek
- // forward as many
- // entries as possible as long as the number is the same.
- int position = mCursor.getPosition();
- while (mCursor.moveToNext()) {
- String nextNumber = mCursor.getString(numberColumn);
- if (PhoneNumberUtils.compare(mContext, number, nextNumber)) {
- count++;
- } else {
- break;
- }
- }
-
- mCursor.moveToPosition(position);
-
- boolean isVoicemail = PhoneNumberUtils.isVoiceMailNumber(number);
- ContactEntry contactEntry = InMemoryPhoneBook.get().lookupContactEntry(number);
- String nameWithCount = getContactName(
- contactEntry != null ? contactEntry.getDisplayName() : null,
- number,
- count,
- isVoicemail);
-
- // Not sure why this is the only column checked here but I'm
- // assuming this was to work around some bug on some device.
- long millis = dateColumn == -1 ? 0 : mCursor.getLong(dateColumn);
-
- StringBuffer secondaryTextStringBuilder = new StringBuffer();
- CharSequence relativeDate = getRelativeTime(millis);
-
- // Append the type (work, mobile etc.) if it isn't voicemail.
- if (!isVoicemail) {
- CharSequence type = contactEntry != null
- ? getTypeLabel(mContext.getResources(), contactEntry.getType(),
- contactEntry.getLabel())
- : "";
- secondaryTextStringBuilder.append(type);
- if (!TextUtils.isEmpty(type) && !TextUtils.isEmpty(relativeDate)) {
- secondaryTextStringBuilder.append(", ");
- }
- }
- // Add in the timestamp.
- if (relativeDate != null) {
- secondaryTextStringBuilder.append(relativeDate);
- }
-
- CallLogItem item = new CallLogItem(nameWithCount,
- secondaryTextStringBuilder.toString(),
- number, null);
- mItems.add(item);
- // Since we deduplicated count rows, we can move all the way to that row so the
- // next iteration takes us to the row following the last duplicate row.
- if (count > 1) {
- mCursor.moveToPosition(position + count - 1);
- }
- }
- } finally {
- mCursor.close();
- }
- }
- return null;
- }
-
- @Override
- protected void onPostExecute(Void aVoid) {
- mListener.onLoadComplete(mItems);
- }
-}
diff --git a/src/com/android/car/dialer/ui/ContactListFragment.java b/src/com/android/car/dialer/ui/ContactListFragment.java
deleted file mode 100644
index 93f3acd2..00000000
--- a/src/com/android/car/dialer/ui/ContactListFragment.java
+++ /dev/null
@@ -1,210 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.car.dialer.ui;
-
-import android.content.Context;
-import android.database.Cursor;
-import android.graphics.Bitmap;
-import android.net.Uri;
-import android.os.Bundle;
-import android.provider.ContactsContract;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.TextView;
-
-import com.android.car.dialer.ContactDetailsFragment;
-import com.android.car.dialer.R;
-import com.android.car.dialer.telecom.PhoneLoader;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.car.widget.AlphaJumpBucketer;
-import androidx.car.widget.IAlphaJumpAdapter;
-import androidx.car.widget.ListItemAdapter;
-import androidx.car.widget.PagedListView;
-import androidx.fragment.app.Fragment;
-import androidx.loader.app.LoaderManager;
-import androidx.loader.content.CursorLoader;
-import androidx.loader.content.Loader;
-
-/**
- * Contact Fragment.
- */
-public class ContactListFragment extends Fragment implements LoaderManager.LoaderCallbacks<Cursor>,
- ContactListItemProvider.OnShowContactDetailListener {
- private static final int CONTACT_LOADER_ID = 1;
-
- private PagedListView mPagedListView;
-
- public static ContactListFragment newInstance() {
- ContactListFragment fragment = new ContactListFragment();
- return fragment;
- }
-
- @Override
- public void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- LoaderManager loaderManager = LoaderManager.getInstance(this);
- loaderManager.initLoader(CONTACT_LOADER_ID, null, this);
- }
-
- @Override
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
- @Nullable Bundle savedInstanceState) {
- View fragmentView = inflater.inflate(R.layout.contact_list_fragment, container, false);
- mPagedListView = fragmentView.findViewById(R.id.list_view);
- ((TextView) fragmentView.findViewById(R.id.title)).setText(
- getContext().getString(R.string.contacts_title));
- return fragmentView;
- }
-
- @Override
- public Loader<Cursor> onCreateLoader(int i, @Nullable Bundle bundle) {
- return new CursorLoader(
- getContext(),
- ContactsContract.Data.CONTENT_URI,
- null,
- ContactsContract.Data.MIMETYPE + " = '" + ContactsContract.CommonDataKinds.Phone
- .CONTENT_ITEM_TYPE + "'",
- null,
- ContactsContract.Contacts.DISPLAY_NAME + " ASC "
- );
- }
-
- @Override
- public void onLoadFinished(@NonNull Loader loader, Cursor cursor) {
- List<ContactItem> contactItems = new ArrayList<>();
- while (cursor.moveToNext()) {
- String number = PhoneLoader.getPhoneNumber(cursor, getContext().getContentResolver());
-
- int idColumnIndex = PhoneLoader.getIdColumnIndex(cursor);
- int id = cursor.getInt(idColumnIndex);
-
- int displayNameColumnIndex = cursor.getColumnIndex(
- ContactsContract.Contacts.DISPLAY_NAME);
- String displayName = cursor.getString(displayNameColumnIndex);
-
- int lookupKeyColumnIndex = cursor.getColumnIndex(
- ContactsContract.Contacts.LOOKUP_KEY);
- String lookupKey = cursor.getString(lookupKeyColumnIndex);
-
- contactItems.add(new ContactItem(number, id, displayName, null, lookupKey));
- }
-
- ListItemAdapter contactListAdapter = new ListItemAdapter(getContext(),
- new ContactListItemProvider(getContext(), contactItems, ContactListFragment.this));
- mPagedListView.setAdapter(contactListAdapter);
- contactListAdapter.notifyDataSetChanged();
- }
-
- @Override
- public void onLoaderReset(@NonNull Loader loader) {
- }
-
- @Override
- public void onShowContactDetail(int contactId, String lookupKey) {
- View contactDetailContainer =
- getView().findViewById(R.id.contact_detail_container);
- contactDetailContainer.setVisibility(View.VISIBLE);
- setActivityActionBarVisibility(View.GONE);
-
- final Uri uri = ContactsContract.Contacts.getLookupUri(contactId, lookupKey);
- Fragment contactDetailFragment = ContactDetailsFragment.newInstance(uri, null);
- getChildFragmentManager().beginTransaction().replace(R.id.contact_detail_fragment_container,
- contactDetailFragment).commit();
-
- getView().findViewById(R.id.back_button).setOnClickListener((v) -> {
- getChildFragmentManager().beginTransaction().remove(contactDetailFragment).commit();
- contactDetailContainer.setVisibility(View.GONE);
- setActivityActionBarVisibility(View.VISIBLE);
- });
-
- }
-
- private void setActivityActionBarVisibility(int visibility) {
- View actionBar = getActivity().getWindow().getDecorView().findViewById(R.id.car_toolbar);
- if (actionBar != null) {
- actionBar.setVisibility(visibility);
- }
- }
-
- /**
- * Pojo which holds a contact entry information.
- */
- public static class ContactItem {
- public final String mNumber;
- public final int mId;
- public final String mDisplayName;
- public final Bitmap mIcon;
- public final String mLookupKey;
-
- private ContactItem(String number, int id, String displayName, Bitmap icon,
- String lookupKey) {
- mNumber = number;
- mId = id;
- mDisplayName = displayName;
- mIcon = icon;
- mLookupKey = lookupKey;
- }
- }
-
- /**
- * Use this Adapter to enabled AlphaJump.
- */
- private class ContactListAdapter extends ListItemAdapter implements IAlphaJumpAdapter {
-
- List<ContactItem> mContactItems;
-
- public ContactListAdapter(Context context,
- List<ContactItem> contactItems) {
- super(context,
- new ContactListItemProvider(context, contactItems, ContactListFragment.this));
- mContactItems = contactItems;
- }
-
- public ContactListAdapter(Context context, List<ContactItem> contactItems,
- int backgroundStyle) {
- super(context,
- new ContactListItemProvider(context, contactItems, ContactListFragment.this),
- backgroundStyle);
- mContactItems = contactItems;
- }
-
- @Override
- public Collection<Bucket> getAlphaJumpBuckets() {
- AlphaJumpBucketer alphaJumpBucketer = new AlphaJumpBucketer();
- List<String> values = new ArrayList<>();
- for (ContactItem contactItem : mContactItems) {
- values.add(contactItem.mDisplayName);
- }
-
- return alphaJumpBucketer.createBuckets(values.toArray(new String[]{}));
- }
-
- @Override
- public void onAlphaJumpEnter() {
- }
-
- @Override
- public void onAlphaJumpLeave(Bucket bucket) {
- }
- }
-}
diff --git a/src/com/android/car/dialer/ui/ContactListItemProvider.java b/src/com/android/car/dialer/ui/ContactListItemProvider.java
deleted file mode 100644
index 7feb4f2f..00000000
--- a/src/com/android/car/dialer/ui/ContactListItemProvider.java
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.car.dialer.ui;
-
-import android.content.Context;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
-
-import com.android.car.dialer.R;
-import com.android.car.dialer.telecom.UiCallManager;
-import com.android.car.dialer.ui.listitem.ContactListItem;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import androidx.car.widget.ListItem;
-import androidx.car.widget.ListItemProvider;
-import androidx.car.widget.TextListItem;
-
-/**
- * Provides ListItem for contact list.
- */
-public class ContactListItemProvider extends ListItemProvider {
-
- private final List<TextListItem> mItems = new ArrayList<>();
- private final OnShowContactDetailListener mOnShowContactDetailListener;
-
- public interface OnShowContactDetailListener {
- void onShowContactDetail(int contactId, String lookupKey);
- }
-
- public ContactListItemProvider(Context context, List<ContactListFragment.ContactItem> items,
- OnShowContactDetailListener onShowContactDetailListener) {
- mOnShowContactDetailListener = onShowContactDetailListener;
- for (ContactListFragment.ContactItem contactItem : items) {
- ContactListItem textListItem = new ContactListItem(context, contactItem);
- textListItem.setPrimaryActionIcon(
- new BitmapDrawable(context.getResources(), contactItem.mIcon), true);
- textListItem.setTitle(contactItem.mDisplayName);
- textListItem.setOnClickListener(
- (v) -> UiCallManager.get().safePlaceCall(contactItem.mNumber, true));
- Drawable supplementalIconDrawable = context.getDrawable(R.drawable.ic_contact);
- supplementalIconDrawable.setTint(context.getColor(R.color.car_tint));
- int iconSize = context.getResources().getDimensionPixelSize(
- R.dimen.car_primary_icon_size);
- supplementalIconDrawable.setBounds(0, 0, iconSize, iconSize);
- textListItem.setSupplementalIcon(supplementalIconDrawable, true, (v) -> {
- mOnShowContactDetailListener.onShowContactDetail(contactItem.mId,
- contactItem.mLookupKey);
- });
- mItems.add(textListItem);
- }
- }
-
- @Override
- public ListItem get(int position) {
- return mItems.get(position);
- }
-
- @Override
- public int size() {
- return mItems.size();
- }
-}
diff --git a/src/com/android/car/dialer/ui/DialerInfoController.java b/src/com/android/car/dialer/ui/DialerInfoController.java
deleted file mode 100644
index 37c98f4d..00000000
--- a/src/com/android/car/dialer/ui/DialerInfoController.java
+++ /dev/null
@@ -1,172 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.car.dialer.ui;
-
-import android.content.Context;
-import android.telecom.Call;
-import android.text.TextUtils;
-import android.view.View;
-import android.widget.ImageButton;
-import android.widget.TextView;
-
-import com.android.car.apps.common.FabDrawable;
-import com.android.car.dialer.R;
-import com.android.car.dialer.telecom.TelecomUtils;
-import com.android.car.dialer.telecom.UiCall;
-import com.android.car.dialer.telecom.UiCallManager;
-
-/**
- * Controls dialer information such as dialed number and shows proper action based on current call
- * state.
- */
-public class DialerInfoController {
- private static final int MAX_DIAL_NUMBER = 20;
-
- private TextView mTitleView;
- private TextView mBodyView;
-
- private ImageButton mCallButton;
- private ImageButton mDeleteButton;
-
- private ImageButton mEndCallButton;
- private ImageButton mMuteButton;
-
- private Context mContext;
-
- private final StringBuffer mNumber = new StringBuffer(MAX_DIAL_NUMBER);
-
- public DialerInfoController(Context context, View container) {
- mContext = context;
- init(container);
- }
-
- public View init(View container) {
- mTitleView = container.findViewById(R.id.title);
- mBodyView = container.findViewById(R.id.body);
- mCallButton = container.findViewById(R.id.call_button);
- mDeleteButton = container.findViewById(R.id.delete_button);
- mEndCallButton = container.findViewById(R.id.end_call_button);
- mMuteButton = container.findViewById(R.id.mute_button);
-
- FabDrawable answerCallDrawable = new FabDrawable(mContext);
- answerCallDrawable.setFabAndStrokeColor(mContext.getColor(R.color.phone_call));
- mCallButton.setBackground(answerCallDrawable);
- mCallButton.setOnClickListener((unusedView) -> {
- if (!TextUtils.isEmpty(mNumber.toString())) {
- UiCallManager.get().safePlaceCall(mNumber.toString(), false);
- }
- });
- mDeleteButton.setOnClickListener(v -> {
- removeLastDigit();
- });
- mDeleteButton.setOnLongClickListener(v -> {
- // Clear all on long-press
- clearDialedNumber();
- return true;
- });
-
- updateView();
-
- return container;
- }
-
- /**
- * Append more number to the end of dialed number.
- */
- public void appendDialedNumber(String number) {
- if (mNumber.length() < MAX_DIAL_NUMBER) {
- mNumber.append(number);
- mTitleView.setText(getFormattedNumber(mNumber.toString()));
- }
- }
-
- /**
- * Remove last digit of the dialed number. If there's no number left to delete, there's no
- * operation to be take.
- */
- public void removeLastDigit() {
- if (mNumber.length() != 0) {
- mNumber.deleteCharAt(mNumber.length() - 1);
- mTitleView.setText(getFormattedNumber(mNumber.toString()));
- }
- UiCall primaryCall = UiCallManager.get().getPrimaryCall();
-
- if (mNumber.length() == 0 && primaryCall != null
- && primaryCall.getState() != Call.STATE_ACTIVE) {
- mTitleView.setText(R.string.dial_a_number);
- }
- }
-
- private void updateView() {
- UiCall onGoingCall = UiCallManager.get().getPrimaryCall();
- if (onGoingCall == null) {
- showPreDialUi();
- } else if (onGoingCall.getState() == Call.STATE_CONNECTING) {
- showDialingUi(onGoingCall);
- } else if (onGoingCall.getState() == Call.STATE_ACTIVE) {
- showInCallUi();
- }
- }
-
- private void showPreDialUi() {
- mCallButton.setVisibility(View.VISIBLE);
- mDeleteButton.setVisibility(View.VISIBLE);
-
- mEndCallButton.setVisibility(View.GONE);
- mMuteButton.setVisibility(View.GONE);
- }
-
- private void showDialingUi(UiCall uiCall) {
- if (mTitleView.getText().equals(mContext.getString(R.string.dial_a_number))) {
- mTitleView.setText("");
- }
- FabDrawable endCallDrawable = new FabDrawable(mContext);
- endCallDrawable.setFabAndStrokeColor(mContext.getColor(R.color.phone_end_call));
- mEndCallButton.setBackground(endCallDrawable);
- mEndCallButton.setVisibility(View.VISIBLE);
- mMuteButton.setVisibility(View.VISIBLE);
- mBodyView.setVisibility(View.VISIBLE);
-
- mDeleteButton.setVisibility(View.GONE);
- mCallButton.setVisibility(View.GONE);
- bindUserProfileView(uiCall);
- }
-
- private void showInCallUi() {
- if (mTitleView.getText().equals(mContext.getString(R.string.dial_a_number))) {
- mTitleView.setText("");
- }
- mEndCallButton.setVisibility(View.GONE);
- mDeleteButton.setVisibility(View.GONE);
- mCallButton.setVisibility(View.GONE);
- }
-
- private String getFormattedNumber(String number) {
- return TelecomUtils.getFormattedNumber(mContext, number);
- }
-
- private void clearDialedNumber() {
- mNumber.setLength(0);
- mTitleView.setText(getFormattedNumber(mNumber.toString()));
- }
-
- private void bindUserProfileView(UiCall primaryCall) {
- if (primaryCall == null) {
- return;
- }
- mTitleView.setText(TelecomUtils.getDisplayName(mContext, primaryCall));
- }
-}
diff --git a/src/com/android/car/dialer/ui/DialpadFragment.java b/src/com/android/car/dialer/ui/DialpadFragment.java
deleted file mode 100644
index 7cbd6a32..00000000
--- a/src/com/android/car/dialer/ui/DialpadFragment.java
+++ /dev/null
@@ -1,223 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.car.dialer.ui;
-
-import android.media.AudioManager;
-import android.media.ToneGenerator;
-import android.os.Bundle;
-import android.util.SparseArray;
-import android.util.SparseIntArray;
-import android.view.KeyEvent;
-import android.view.LayoutInflater;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewGroup;
-
-import com.android.car.dialer.R;
-import com.android.car.dialer.telecom.UiCallManager;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.fragment.app.Fragment;
-
-/**
- * Dialpad Fragment which displays a dialpad.
- */
-public class DialpadFragment extends Fragment {
- private static final SparseIntArray sToneMap = new SparseIntArray();
- private static final SparseArray<String> sDialValueMap = new SparseArray<>();
- private static final SparseArray<Integer> sRIdMap = new SparseArray<>();
-
- private static final int TONE_LENGTH_INFINITE = -1;
- private static final int TONE_RELATIVE_VOLUME = 80;
-
- static {
- sToneMap.put(KeyEvent.KEYCODE_1, ToneGenerator.TONE_DTMF_1);
- sToneMap.put(KeyEvent.KEYCODE_2, ToneGenerator.TONE_DTMF_2);
- sToneMap.put(KeyEvent.KEYCODE_3, ToneGenerator.TONE_DTMF_3);
- sToneMap.put(KeyEvent.KEYCODE_4, ToneGenerator.TONE_DTMF_4);
- sToneMap.put(KeyEvent.KEYCODE_5, ToneGenerator.TONE_DTMF_5);
- sToneMap.put(KeyEvent.KEYCODE_6, ToneGenerator.TONE_DTMF_6);
- sToneMap.put(KeyEvent.KEYCODE_7, ToneGenerator.TONE_DTMF_7);
- sToneMap.put(KeyEvent.KEYCODE_8, ToneGenerator.TONE_DTMF_8);
- sToneMap.put(KeyEvent.KEYCODE_9, ToneGenerator.TONE_DTMF_9);
- sToneMap.put(KeyEvent.KEYCODE_0, ToneGenerator.TONE_DTMF_0);
- sToneMap.put(KeyEvent.KEYCODE_STAR, ToneGenerator.TONE_DTMF_S);
- sToneMap.put(KeyEvent.KEYCODE_POUND, ToneGenerator.TONE_DTMF_P);
-
- sDialValueMap.put(KeyEvent.KEYCODE_1, "1");
- sDialValueMap.put(KeyEvent.KEYCODE_2, "2");
- sDialValueMap.put(KeyEvent.KEYCODE_3, "3");
- sDialValueMap.put(KeyEvent.KEYCODE_4, "4");
- sDialValueMap.put(KeyEvent.KEYCODE_5, "5");
- sDialValueMap.put(KeyEvent.KEYCODE_6, "6");
- sDialValueMap.put(KeyEvent.KEYCODE_7, "7");
- sDialValueMap.put(KeyEvent.KEYCODE_8, "8");
- sDialValueMap.put(KeyEvent.KEYCODE_9, "9");
- sDialValueMap.put(KeyEvent.KEYCODE_0, "0");
- sDialValueMap.put(KeyEvent.KEYCODE_STAR, "*");
- sDialValueMap.put(KeyEvent.KEYCODE_POUND, "#");
-
- sRIdMap.put(KeyEvent.KEYCODE_1, R.id.one);
- sRIdMap.put(KeyEvent.KEYCODE_2, R.id.two);
- sRIdMap.put(KeyEvent.KEYCODE_3, R.id.three);
- sRIdMap.put(KeyEvent.KEYCODE_4, R.id.four);
- sRIdMap.put(KeyEvent.KEYCODE_5, R.id.five);
- sRIdMap.put(KeyEvent.KEYCODE_6, R.id.six);
- sRIdMap.put(KeyEvent.KEYCODE_7, R.id.seven);
- sRIdMap.put(KeyEvent.KEYCODE_8, R.id.eight);
- sRIdMap.put(KeyEvent.KEYCODE_9, R.id.nine);
- sRIdMap.put(KeyEvent.KEYCODE_0, R.id.zero);
- sRIdMap.put(KeyEvent.KEYCODE_STAR, R.id.star);
- sRIdMap.put(KeyEvent.KEYCODE_POUND, R.id.pound);
- }
-
- public static DialpadFragment newInstance() {
- return new DialpadFragment();
- }
-
- /**
- * Callback for dialpad to interact with its host.
- */
- public interface DialpadCallback {
- /**
- * Called when voice mail should be dialed.
- */
- void onDialVoiceMail();
-
- /**
- * Called when a digit should be append.
- */
- void onAppendDigit(String digit);
- }
-
- private ToneGenerator mToneGenerator;
- private DialpadCallback mDialpadCallback;
-
- @Override
- public void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- mToneGenerator = new ToneGenerator(AudioManager.STREAM_MUSIC, TONE_RELATIVE_VOLUME);
- }
-
- @Override
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
- @Nullable Bundle savedInstanceState) {
- if (getParentFragment() != null && getParentFragment() instanceof DialpadCallback) {
- mDialpadCallback = (DialpadCallback) getParentFragment();
- } else if (getHost() instanceof DialpadCallback) {
- mDialpadCallback = (DialpadCallback) getHost();
- }
-
- View dialpadView = inflater.inflate(R.layout.dialpad, container, false);
- setupKeypadClickListeners(dialpadView);
- return dialpadView;
- }
-
- @Override
- public void onPause() {
- super.onPause();
- stopTone();
- }
-
- /**
- * The click listener for all dialpad buttons. Reacts to touch-down and touch-up events, as
- * well as long-press for certain keys. Mimics the behavior of the phone dialer app.
- */
- private class DialpadClickListener implements View.OnTouchListener,
- View.OnLongClickListener {
- private final int mTone;
- private final String mValue;
-
- DialpadClickListener(int keyCode) {
- mTone = sToneMap.get(keyCode);
- mValue = sDialValueMap.get(keyCode);
- }
-
- @Override
- public boolean onLongClick(View v) {
- switch (mValue) {
- case "0":
- if (mDialpadCallback != null) {
- mDialpadCallback.onAppendDigit("+");
- }
- stopTone();
- return true;
- case "1":
- // TODO: this currently does not work (at least over bluetooth HFP), because
- // the framework is unable to get the voicemail number. Revisit later...
- if (mDialpadCallback != null) {
- mDialpadCallback.onDialVoiceMail();
- }
- return true;
- default:
- return false;
- }
- }
-
- @Override
- public boolean onTouch(View v, MotionEvent event) {
- UiCallManager uiCallmanager = UiCallManager.get();
- boolean hasActiveCall = uiCallmanager.getPrimaryCall() != null;
- if (event.getAction() == MotionEvent.ACTION_DOWN) {
- if (mDialpadCallback != null) {
- mDialpadCallback.onAppendDigit(mValue);
- }
- if (hasActiveCall) {
- uiCallmanager.playDtmfTone(uiCallmanager.getPrimaryCall(), mValue.charAt(0));
- } else {
- playTone(mTone);
- }
- } else if (event.getAction() == MotionEvent.ACTION_UP) {
- if (hasActiveCall) {
- uiCallmanager.stopDtmfTone(uiCallmanager.getPrimaryCall());
- } else {
- stopTone();
- }
- }
-
- // Continue propagating the touch event
- return false;
- }
- }
-
- private void playTone(int tone) {
- if (mToneGenerator == null) {
- return;
- }
-
- // Start the new tone
- mToneGenerator.startTone(tone, TONE_LENGTH_INFINITE);
- }
-
- private void stopTone() {
- if (mToneGenerator == null) {
- return;
- }
-
- mToneGenerator.stopTone();
- }
-
- private void setupKeypadClickListeners(View parent) {
- for (int i = 0; i < sRIdMap.size(); i++) {
- int key = sRIdMap.keyAt(i);
- DialpadClickListener clickListener = new DialpadClickListener(key);
- View v = parent.findViewById(sRIdMap.get(key));
- v.setOnTouchListener(clickListener);
- v.setOnLongClickListener(clickListener);
- }
- }
-}
diff --git a/src/com/android/car/dialer/ui/InCallFragment.java b/src/com/android/car/dialer/ui/InCallFragment.java
deleted file mode 100644
index 166e5a1c..00000000
--- a/src/com/android/car/dialer/ui/InCallFragment.java
+++ /dev/null
@@ -1,186 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.car.dialer.ui;
-
-import android.os.Bundle;
-import android.os.Handler;
-import android.telecom.Call;
-import android.text.TextUtils;
-import android.text.format.DateUtils;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import com.android.car.dialer.CallListener;
-import com.android.car.dialer.DialerFragment;
-import com.android.car.dialer.R;
-import com.android.car.dialer.telecom.TelecomUtils;
-import com.android.car.dialer.telecom.UiCall;
-import com.android.car.dialer.telecom.UiCallManager;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.fragment.app.Fragment;
-
-/**
- * A fragment that displays information about an on-going call with options to hang up.
- */
-public class InCallFragment extends Fragment implements
- OnGoingCallControllerBarFragment.OnGoingCallControllerBarCallback, CallListener {
-
- private Fragment mDialerFragment;
- private View mUserProfileContainerView;
- private View mDialerFragmentContainer;
- private TextView mUserProfileBodyText;
-
- private Handler mHandler = new Handler();
- private CharSequence mCallInfoLabel;
-
- public static InCallFragment newInstance() {
- return new InCallFragment();
- }
-
- @Override
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
- @Nullable Bundle savedInstanceState) {
- View fragmentView = inflater.inflate(R.layout.in_call_fragment, container, false);
- mUserProfileContainerView = fragmentView.findViewById(R.id.user_profile_container);
- mDialerFragmentContainer = fragmentView.findViewById(R.id.dialer_container);
- mUserProfileBodyText = mUserProfileContainerView.findViewById(R.id.body);
- mDialerFragment = new DialerFragment();
-
- updateControllerBarFragment(UiCallManager.get().getPrimaryCall().getState());
- bindUserProfileView(fragmentView.findViewById(R.id.user_profile_container));
- return fragmentView;
- }
-
- @Override
- public void onPause() {
- super.onPause();
- mHandler.removeCallbacks(mUpdateDurationRunnable);
- }
-
- @Override
- public void onOpenDialpad() {
- mDialerFragment = new DialerFragment();
- getChildFragmentManager().beginTransaction()
- .replace(R.id.dialer_container, mDialerFragment)
- .commit();
- mDialerFragmentContainer.setVisibility(View.VISIBLE);
- mUserProfileContainerView.setVisibility(View.GONE);
- }
-
- @Override
- public void onCloseDialpad() {
- getFragmentManager().beginTransaction()
- .remove(mDialerFragment)
- .commit();
- mDialerFragmentContainer.setVisibility(View.GONE);
- mUserProfileContainerView.setVisibility(View.VISIBLE);
- }
-
- private void bindUserProfileView(View container) {
- UiCall primaryCall = UiCallManager.get().getPrimaryCall();
- if (primaryCall == null) {
- return;
- }
- String number = primaryCall.getNumber();
- String displayName = TelecomUtils.getDisplayName(getContext(), primaryCall);
-
- TextView nameView = container.findViewById(R.id.title);
- nameView.setText(displayName);
-
- ImageView avatar = container.findViewById(R.id.avatar);
- TelecomUtils.setContactBitmapAsync(getContext(), avatar, displayName, number);
-
- mCallInfoLabel = TelecomUtils.getTypeFromNumber(getContext(), primaryCall.getNumber());
- }
-
- private void updateControllerBarFragment(int callState) {
- Fragment controllerBarFragment;
- if (callState == Call.STATE_RINGING) {
- controllerBarFragment = RingingCallControllerBarFragment.newInstance();
- } else {
- controllerBarFragment = OnGoingCallControllerBarFragment.newInstance();
- }
-
- getChildFragmentManager().beginTransaction()
- .replace(R.id.controller_bar_container, controllerBarFragment)
- .commit();
- }
-
- @Override
- public void onAudioStateChanged(boolean isMuted, int route, int supportedRouteMask) {
-
- }
-
- @Override
- public void onCallStateChanged(UiCall call, int state) {
- int callState = call.getState();
- switch (callState) {
- case Call.STATE_NEW:
- case Call.STATE_CONNECTING:
- case Call.STATE_DIALING:
- case Call.STATE_SELECT_PHONE_ACCOUNT:
- case Call.STATE_HOLDING:
- case Call.STATE_DISCONNECTED:
- mHandler.removeCallbacks(mUpdateDurationRunnable);
- updateBody(call);
- break;
- case Call.STATE_ACTIVE:
- mHandler.post(mUpdateDurationRunnable);
- updateControllerBarFragment(call.getState());
- break;
- }
- }
-
- @Override
- public void onCallUpdated(UiCall call) {
-
- }
-
- @Override
- public void onCallAdded(UiCall call) {
-
- }
-
- @Override
- public void onCallRemoved(UiCall call) {
-
- }
-
- private final Runnable mUpdateDurationRunnable = new Runnable() {
- @Override
- public void run() {
- UiCall primaryCall = UiCallManager.get().getPrimaryCall();
- if (primaryCall.getState() != Call.STATE_ACTIVE) {
- return;
- }
- updateBody(primaryCall);
- mHandler.postDelayed(this /* runnable */, DateUtils.SECOND_IN_MILLIS);
- }
- };
-
- private void updateBody(UiCall primaryCall) {
- String callInfoText = TelecomUtils.getCallInfoText(getContext(),
- primaryCall, mCallInfoLabel);
- mUserProfileBodyText.setText(callInfoText);
- mUserProfileBodyText.setVisibility(
- TextUtils.isEmpty(callInfoText) ? View.GONE : View.VISIBLE);
- }
-}
diff --git a/src/com/android/car/dialer/ui/OnGoingCallControllerBarFragment.java b/src/com/android/car/dialer/ui/OnGoingCallControllerBarFragment.java
deleted file mode 100644
index bb6468a5..00000000
--- a/src/com/android/car/dialer/ui/OnGoingCallControllerBarFragment.java
+++ /dev/null
@@ -1,277 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.car.dialer.ui;
-
-import android.app.AlertDialog;
-import android.content.Context;
-import android.os.Bundle;
-import android.telecom.CallAudioState;
-import android.telecom.CallAudioState.CallAudioRoute;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import com.android.car.apps.common.FabDrawable;
-import com.android.car.dialer.R;
-import com.android.car.dialer.log.L;
-import com.android.car.dialer.telecom.UiCall;
-import com.android.car.dialer.telecom.UiCallManager;
-
-import java.util.List;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.car.widget.PagedListView;
-import androidx.fragment.app.Fragment;
-import androidx.recyclerview.widget.RecyclerView;
-
-/**
- * A Fragment of the bar which controls on going call. Its host or parent Fragment is expected to
- * implement {@link OnGoingCallControllerBarCallback}.
- */
-public class OnGoingCallControllerBarFragment extends Fragment {
- private static String TAG = "CDialer.OngoingCallCtlFrg";
- private AlertDialog mAudioRouteSelectionDialog;
- private ImageView mAudioRouteButton;
-
- public static OnGoingCallControllerBarFragment newInstance() {
- return new OnGoingCallControllerBarFragment();
- }
-
- /**
- * Callback for control bar buttons.
- */
- public interface OnGoingCallControllerBarCallback {
- void onOpenDialpad();
-
- void onCloseDialpad();
- }
-
- private OnGoingCallControllerBarCallback mOnGoingCallControllerBarCallback;
-
- @Override
- public void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- if (getParentFragment() != null
- && getParentFragment() instanceof OnGoingCallControllerBarCallback) {
- mOnGoingCallControllerBarCallback =
- (OnGoingCallControllerBarCallback) getParentFragment();
- } else if (getHost() instanceof OnGoingCallControllerBarCallback) {
- mOnGoingCallControllerBarCallback = (OnGoingCallControllerBarCallback) getHost();
- }
-
- View dialogView = LayoutInflater.from(getContext()).inflate(
- R.layout.audio_route_switch_dialog, null, false);
- PagedListView list = dialogView.findViewById(R.id.list);
- List<Integer> availableRoutes = UiCallManager.get().getSupportedAudioRoute();
- list.setDividerVisibilityManager(position -> position == (availableRoutes.size() - 1));
-
- mAudioRouteSelectionDialog = new AlertDialog.Builder(getContext())
- .setView(dialogView)
- .create();
- mAudioRouteSelectionDialog.getWindow().setBackgroundDrawableResource(
- android.R.color.transpare‌​nt);
- list.setAdapter(new AudioRouteListAdapter(getContext(), availableRoutes));
- }
-
- @Nullable
- @Override
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
- @Nullable Bundle savedInstanceState) {
- View fragmentView = inflater.inflate(R.layout.on_going_call_controller_bar_fragment,
- container, false);
- fragmentView.findViewById(R.id.mute_button).setOnClickListener((v) -> {
- if (mOnGoingCallControllerBarCallback == null) {
- return;
- }
- if (v.isActivated()) {
- v.setActivated(false);
- onMuteMic();
- } else {
- v.setActivated(true);
- onUnmuteMic();
- }
- });
-
- fragmentView.findViewById(R.id.toggle_dialpad_button).setOnClickListener((v) -> {
- if (mOnGoingCallControllerBarCallback == null) {
- return;
- }
- if (v.isActivated()) {
- v.setActivated(false);
- mOnGoingCallControllerBarCallback.onCloseDialpad();
- } else {
- v.setActivated(true);
- mOnGoingCallControllerBarCallback.onOpenDialpad();
- }
- });
-
- ImageView endCallButton = fragmentView.findViewById(R.id.end_call_button);
- FabDrawable answerCallDrawable = new FabDrawable(getContext());
- answerCallDrawable.setFabAndStrokeColor(getContext().getColor(R.color.phone_end_call));
- endCallButton.setBackground(answerCallDrawable);
- endCallButton.setOnClickListener((v) -> {
- if (mOnGoingCallControllerBarCallback == null) {
- return;
- }
- onEndCall();
- });
-
-
- List<Integer> audioRoutes = UiCallManager.get().getSupportedAudioRoute();
- mAudioRouteButton = fragmentView.findViewById(R.id.voice_channel_button);
- if (audioRoutes.size() > 1) {
- fragmentView.findViewById(R.id.voice_channel_chevron).setVisibility(View.VISIBLE);
- mAudioRouteButton.setOnClickListener(
- (v) -> mAudioRouteSelectionDialog.show());
- } else {
- fragmentView.findViewById(R.id.voice_channel_chevron).setVisibility(View.GONE);
- }
-
- fragmentView.findViewById(R.id.pause_button).setOnClickListener((v) -> {
- if (mOnGoingCallControllerBarCallback == null) {
- return;
- }
- if (v.isActivated()) {
- v.setActivated(false);
- onHoldCall();
- } else {
- v.setActivated(true);
- onUnholdCall();
- }
- });
- return fragmentView;
- }
-
- @Override
- public void onPause() {
- super.onPause();
- if (mAudioRouteSelectionDialog.isShowing()) {
- mAudioRouteSelectionDialog.dismiss();
- }
- }
-
- private void onMuteMic() {
- UiCallManager.get().setMuted(true);
- }
-
- private void onUnmuteMic() {
- UiCallManager.get().setMuted(false);
- }
-
- private void onHoldCall() {
- UiCallManager uiCallManager = UiCallManager.get();
- UiCall primaryCall = UiCallManager.get().getPrimaryCall();
- uiCallManager.holdCall(primaryCall);
- }
-
- private void onUnholdCall() {
- UiCallManager uiCallManager = UiCallManager.get();
- UiCall primaryCall = UiCallManager.get().getPrimaryCall();
- uiCallManager.unholdCall(primaryCall);
- }
-
- private void onVoiceOutputChannelChanged(@CallAudioRoute int audioRoute) {
- UiCallManager.get().setAudioRoute(audioRoute);
- mAudioRouteSelectionDialog.dismiss();
- mAudioRouteButton.setImageResource(getAudioRouteIconRes(audioRoute));
- }
-
- private void onEndCall() {
- UiCallManager uiCallManager = UiCallManager.get();
- UiCall primaryCall = UiCallManager.get().getPrimaryCall();
- uiCallManager.disconnectCall(primaryCall);
- }
-
- private int getAudioRouteIconRes(@CallAudioRoute int audioRoute) {
- switch (audioRoute) {
- case CallAudioState.ROUTE_WIRED_HEADSET:
- case CallAudioState.ROUTE_EARPIECE:
- return R.drawable.ic_smartphone;
- case CallAudioState.ROUTE_BLUETOOTH:
- return R.drawable.ic_bluetooth;
- case CallAudioState.ROUTE_SPEAKER:
- return R.drawable.ic_speaker_phone;
- default:
- L.w(TAG, "Unknown audio route: " + audioRoute);
- return -1;
- }
- }
-
- private int getAudioRouteLabelRes(@CallAudioRoute int audioRoute) {
- switch (audioRoute) {
- case CallAudioState.ROUTE_WIRED_HEADSET:
- case CallAudioState.ROUTE_EARPIECE:
- return R.string.audio_route_handset;
- case CallAudioState.ROUTE_BLUETOOTH:
- return R.string.audio_route_vehicle;
- case CallAudioState.ROUTE_SPEAKER:
- return R.string.audio_route_phone_speaker;
- default:
- L.w(TAG, "Unknown audio route: " + audioRoute);
- return -1;
- }
- }
-
- private class AudioRouteListAdapter extends
- RecyclerView.Adapter<AudioRouteItemViewHolder> {
- private List<Integer> mSupportedRoutes;
- private Context mContext;
-
- public AudioRouteListAdapter(Context context, List<Integer> supportedRoutes) {
- mSupportedRoutes = supportedRoutes;
- mContext = context;
- if (mSupportedRoutes.contains(CallAudioState.ROUTE_EARPIECE)
- && mSupportedRoutes.contains(CallAudioState.ROUTE_WIRED_HEADSET)) {
- // Keep either ROUTE_EARPIECE or ROUTE_WIRED_HEADSET, but not both of them.
- mSupportedRoutes.remove(CallAudioState.ROUTE_WIRED_HEADSET);
- }
- }
-
- @Override
- public AudioRouteItemViewHolder onCreateViewHolder(ViewGroup container, int position) {
- View listItemView = LayoutInflater.from(mContext).inflate(
- R.layout.audio_route_list_item, container, false);
- return new AudioRouteItemViewHolder(listItemView);
- }
-
- @Override
- public void onBindViewHolder(AudioRouteItemViewHolder viewHolder, int position) {
- int audioRoute = mSupportedRoutes.get(position);
- viewHolder.mBody.setText(mContext.getString(getAudioRouteLabelRes(audioRoute)));
- viewHolder.mIcon.setImageResource(getAudioRouteIconRes(audioRoute));
- viewHolder.itemView.setOnClickListener((v) -> onVoiceOutputChannelChanged(audioRoute));
- }
-
- @Override
- public int getItemCount() {
- return mSupportedRoutes.size();
- }
- }
-
- private static class AudioRouteItemViewHolder extends RecyclerView.ViewHolder {
- public final ImageView mIcon;
- public final TextView mBody;
-
- public AudioRouteItemViewHolder(View itemView) {
- super(itemView);
- mIcon = itemView.findViewById(R.id.icon);
- mBody = itemView.findViewById(R.id.body);
- }
- }
-} \ No newline at end of file
diff --git a/src/com/android/car/dialer/ui/TelecomActivity.java b/src/com/android/car/dialer/ui/TelecomActivity.java
new file mode 100644
index 00000000..32737d11
--- /dev/null
+++ b/src/com/android/car/dialer/ui/TelecomActivity.java
@@ -0,0 +1,436 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui;
+
+import android.app.SearchManager;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.telecom.Call;
+import android.telephony.PhoneNumberUtils;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.Toolbar;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.fragment.app.FragmentManager;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.ViewModelProviders;
+import androidx.preference.PreferenceManager;
+
+import com.android.car.apps.common.util.Themes;
+import com.android.car.apps.common.widget.CarTabLayout;
+import com.android.car.dialer.Constants;
+import com.android.car.dialer.R;
+import com.android.car.dialer.log.L;
+import com.android.car.dialer.notification.NotificationService;
+import com.android.car.dialer.telecom.UiCallManager;
+import com.android.car.dialer.ui.activecall.InCallActivity;
+import com.android.car.dialer.ui.activecall.InCallViewModel;
+import com.android.car.dialer.ui.calllog.CallHistoryFragment;
+import com.android.car.dialer.ui.common.DialerBaseFragment;
+import com.android.car.dialer.ui.contact.ContactListFragment;
+import com.android.car.dialer.ui.dialpad.DialpadFragment;
+import com.android.car.dialer.ui.favorite.FavoriteFragment;
+import com.android.car.dialer.ui.search.ContactResultsFragment;
+import com.android.car.dialer.ui.settings.DialerSettingsActivity;
+import com.android.car.dialer.ui.warning.NoHfpFragment;
+
+import java.util.List;
+
+/**
+ * Main activity for the Dialer app. It contains two layers:
+ * <ul>
+ * <li>Overlay layer for {@link NoHfpFragment}
+ * <li>Content layer for {@link FavoriteFragment} {@link CallHistoryFragment} {@link
+ * ContactListFragment} and {@link DialpadFragment}
+ *
+ * <p>Start {@link InCallActivity} if there are ongoing calls
+ *
+ * <p>Based on call and connectivity status, it will choose the right page to display.
+ */
+public class TelecomActivity extends FragmentActivity implements
+ DialerBaseFragment.DialerFragmentParent, FragmentManager.OnBackStackChangedListener {
+ private static final String TAG = "CD.TelecomActivity";
+
+ private LiveData<String> mBluetoothErrorMsgLiveData;
+ private LiveData<Integer> mDialerAppStateLiveData;
+ private LiveData<List<Call>> mOngoingCallListLiveData;
+
+ // View objects for this activity.
+ private CarTabLayout<TelecomPageTab> mTabLayout;
+ private TelecomPageTab.Factory mTabFactory;
+ private Toolbar mToolbar;
+ private View mToolbarContainer;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ L.d(TAG, "onCreate");
+ setContentView(R.layout.telecom_activity);
+
+ mToolbar = findViewById(R.id.car_toolbar);
+ setActionBar(mToolbar);
+ getActionBar().setLogo(R.drawable.sized_logo);
+
+ mToolbarContainer = findViewById(R.id.car_toolbar_container);
+
+ setupTabLayout();
+
+ TelecomActivityViewModel viewModel = ViewModelProviders.of(this).get(
+ TelecomActivityViewModel.class);
+ mBluetoothErrorMsgLiveData = viewModel.getErrorMessage();
+ mDialerAppStateLiveData = viewModel.getDialerAppState();
+ mDialerAppStateLiveData.observe(this,
+ dialerAppState -> updateCurrentFragment(dialerAppState));
+
+ InCallViewModel inCallViewModel = ViewModelProviders.of(this).get(InCallViewModel.class);
+ mOngoingCallListLiveData = inCallViewModel.getOngoingCallList();
+ // The mOngoingCallListLiveData needs to be active to get calculated.
+ mOngoingCallListLiveData.observe(this, this::maybeStartInCallActivity);
+
+ handleIntent();
+ }
+
+ @Override
+ public void onStart() {
+ getSupportFragmentManager().addOnBackStackChangedListener(this);
+ onBackStackChanged();
+ super.onStart();
+ L.d(TAG, "onStart");
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ L.d(TAG, "onStop");
+ getSupportFragmentManager().removeOnBackStackChangedListener(this);
+ }
+
+ @Override
+ protected void onNewIntent(Intent i) {
+ super.onNewIntent(i);
+ setIntent(i);
+ handleIntent();
+ }
+
+ @Override
+ public void setBackground(Drawable background) {
+ findViewById(android.R.id.content).setBackground(background);
+ }
+
+ private void handleIntent() {
+ Intent intent = getIntent();
+ String action = intent != null ? intent.getAction() : null;
+ L.d(TAG, "handleIntent, intent: %s, action: %s", intent, action);
+ if (action == null || action.length() == 0) {
+ return;
+ }
+
+ String number;
+ switch (action) {
+ case Intent.ACTION_DIAL:
+ number = PhoneNumberUtils.getNumberFromIntent(intent, this);
+ if (TelecomActivityViewModel.DialerAppState.BLUETOOTH_ERROR
+ != mDialerAppStateLiveData.getValue()) {
+ showDialPadFragment(number);
+ }
+ break;
+
+ case Intent.ACTION_CALL:
+ number = PhoneNumberUtils.getNumberFromIntent(intent, this);
+ UiCallManager.get().placeCall(number);
+ break;
+
+ case Intent.ACTION_SEARCH:
+ String searchQuery = intent.getStringExtra(SearchManager.QUERY);
+ navigateToContactResultsFragment(searchQuery);
+ break;
+
+ case Constants.Intents.ACTION_SHOW_PAGE:
+ if (TelecomActivityViewModel.DialerAppState.BLUETOOTH_ERROR
+ != mDialerAppStateLiveData.getValue()) {
+ showTabPage(intent.getStringExtra(Constants.Intents.EXTRA_SHOW_PAGE));
+ if (intent.getBooleanExtra(Constants.Intents.EXTRA_ACTION_READ_MISSED, false)) {
+ NotificationService.readAllMissedCall(this);
+ }
+ }
+ break;
+
+ default:
+ // Do nothing.
+ }
+
+ setIntent(null);
+
+ // This is to start the incall activity when user taps on the dialer launch icon rapidly
+ maybeStartInCallActivity(mOngoingCallListLiveData.getValue());
+ }
+
+ /**
+ * Update the current visible fragment of this Activity based on the state of the application.
+ * <ul>
+ * <li> If bluetooth is not connected or there is an active call, show overlay, lock drawer,
+ * hide action bar and hide the content layer.
+ * <li> Otherwise, show the content layer, show action bar, hide the overlay and reset drawer
+ * lock mode.
+ */
+ private void updateCurrentFragment(
+ @TelecomActivityViewModel.DialerAppState int dialerAppState) {
+ L.d(TAG, "updateCurrentFragment, dialerAppState: %d", dialerAppState);
+
+ boolean isOverlayFragmentVisible =
+ TelecomActivityViewModel.DialerAppState.DEFAULT != dialerAppState;
+ findViewById(R.id.content_container)
+ .setVisibility(isOverlayFragmentVisible ? View.GONE : View.VISIBLE);
+ findViewById(R.id.overlay_container)
+ .setVisibility(isOverlayFragmentVisible ? View.VISIBLE : View.GONE);
+
+ switch (dialerAppState) {
+ case TelecomActivityViewModel.DialerAppState.BLUETOOTH_ERROR:
+ showNoHfpOverlay(mBluetoothErrorMsgLiveData.getValue());
+ break;
+
+ case TelecomActivityViewModel.DialerAppState.EMERGENCY_DIALPAD:
+ setOverlayFragment(DialpadFragment.newEmergencyDialpad());
+ break;
+
+ case TelecomActivityViewModel.DialerAppState.DEFAULT:
+ default:
+ clearOverlayFragment();
+ break;
+ }
+ }
+
+ private void showNoHfpOverlay(String errorMsg) {
+ Fragment overlayFragment = getCurrentOverlayFragment();
+ if (overlayFragment instanceof NoHfpFragment) {
+ ((NoHfpFragment) overlayFragment).setErrorMessage(errorMsg);
+ } else {
+ setOverlayFragment(NoHfpFragment.newInstance(errorMsg));
+ }
+ }
+
+ private void setOverlayFragment(@NonNull Fragment overlayFragment) {
+ L.d(TAG, "setOverlayFragment: %s", overlayFragment);
+
+ getSupportFragmentManager()
+ .beginTransaction()
+ .replace(R.id.overlay_container, overlayFragment)
+ .commitNow();
+ }
+
+ private void clearOverlayFragment() {
+ L.d(TAG, "clearOverlayFragment");
+
+ Fragment overlayFragment = getCurrentOverlayFragment();
+ if (overlayFragment == null) {
+ return;
+ }
+
+ getSupportFragmentManager()
+ .beginTransaction()
+ .remove(overlayFragment)
+ .commitNow();
+ }
+
+ /** Returns the fragment that is currently being displayed as the overlay view on top. */
+ @Nullable
+ private Fragment getCurrentOverlayFragment() {
+ return getSupportFragmentManager().findFragmentById(R.id.overlay_container);
+ }
+
+ private void setupTabLayout() {
+ mTabLayout = findViewById(R.id.tab_layout);
+
+ boolean hasContentFragment = false;
+
+ mTabFactory = new TelecomPageTab.Factory(this, getSupportFragmentManager());
+ for (int i = 0; i < mTabFactory.getTabCount(); i++) {
+ TelecomPageTab telecomPageTab = mTabFactory.createTab(getBaseContext(), i);
+ mTabLayout.addCarTab(telecomPageTab);
+
+ if (telecomPageTab.wasFragmentRestored()) {
+ mTabLayout.selectCarTab(i);
+ hasContentFragment = true;
+ }
+ }
+
+ // Select the starting tab and set up the fragment for it.
+ if (!hasContentFragment) {
+ int startTabIndex = getTabFromSharedPreference();
+ TelecomPageTab startTab = mTabLayout.get(startTabIndex);
+ mTabLayout.selectCarTab(startTabIndex);
+ setContentFragment(startTab.getFragment(), startTab.getFragmentTag());
+ }
+
+ mTabLayout.addOnCarTabSelectedListener(
+ new CarTabLayout.SimpleOnCarTabSelectedListener<TelecomPageTab>() {
+ @Override
+ public void onCarTabSelected(TelecomPageTab telecomPageTab) {
+ Fragment fragment = telecomPageTab.getFragment();
+ setContentFragment(fragment, telecomPageTab.getFragmentTag());
+ }
+ });
+ }
+
+ /** Switch to {@link DialpadFragment} and set the given number as dialed number. */
+ private void showDialPadFragment(String number) {
+ int dialpadTabIndex = showTabPage(TelecomPageTab.Page.DIAL_PAD);
+
+ if (dialpadTabIndex == -1) {
+ return;
+ }
+
+ TelecomPageTab dialpadTab = mTabLayout.get(dialpadTabIndex);
+ Fragment fragment = dialpadTab.getFragment();
+ if (fragment instanceof DialpadFragment) {
+ ((DialpadFragment) fragment).setDialedNumber(number);
+ } else {
+ L.w(TAG, "Current tab is not a dialpad fragment!");
+ }
+ }
+
+ private int showTabPage(@TelecomPageTab.Page String tabPage) {
+ int tabIndex = mTabFactory.getTabIndex(tabPage);
+ if (tabIndex == -1) {
+ L.w(TAG, "Page %s is not a tab.", tabPage);
+ return -1;
+ }
+ getSupportFragmentManager().executePendingTransactions();
+ while (getSupportFragmentManager().getBackStackEntryCount() > 1) {
+ getSupportFragmentManager().popBackStackImmediate();
+ }
+
+ mTabLayout.selectCarTab(tabIndex);
+ return tabIndex;
+ }
+
+ private void setContentFragment(Fragment fragment, String fragmentTag) {
+ L.d(TAG, "setContentFragment: %s", fragment);
+
+ getSupportFragmentManager().executePendingTransactions();
+ while (getSupportFragmentManager().getBackStackEntryCount() > 0) {
+ getSupportFragmentManager().popBackStackImmediate();
+ }
+
+ getSupportFragmentManager()
+ .beginTransaction()
+ .replace(R.id.content_fragment_container, fragment, fragmentTag)
+ .addToBackStack(fragmentTag)
+ .commit();
+ }
+
+ @Override
+ public void pushContentFragment(@NonNull Fragment topContentFragment, String fragmentTag) {
+ L.d(TAG, "pushContentFragment: %s", topContentFragment);
+
+ getSupportFragmentManager()
+ .beginTransaction()
+ .replace(R.id.content_fragment_container, topContentFragment)
+ .addToBackStack(fragmentTag)
+ .commit();
+ }
+
+ @Override
+ public void onBackStackChanged() {
+ boolean isBackNavigationAvailable = isBackNavigationAvailable();
+ mTabLayout.setVisibility(isBackNavigationAvailable ? View.GONE : View.VISIBLE);
+ int displayOptions = Themes.getAttrInteger(
+ this,
+ isBackNavigationAvailable ? R.style.HomeAsUpDisplayOptions
+ : R.style.RootToolbarDisplayOptions,
+ android.R.attr.displayOptions);
+ getActionBar().setDisplayOptions(displayOptions);
+ }
+
+ @Override
+ public boolean onNavigateUp() {
+ if (isBackNavigationAvailable()) {
+ onBackPressed();
+ return true;
+ }
+ return super.onNavigateUp();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.main_menu, menu);
+
+ MenuItem searchMenu = menu.findItem(R.id.menu_contacts_search);
+ Intent searchIntent = new Intent(getApplicationContext(), TelecomActivity.class);
+ searchIntent.setAction(Intent.ACTION_SEARCH);
+ searchMenu.setIntent(searchIntent);
+
+ MenuItem settingsMenu = menu.findItem(R.id.menu_dialer_setting);
+ Intent settingsIntent = new Intent(getApplicationContext(), DialerSettingsActivity.class);
+ settingsMenu.setIntent(settingsIntent);
+ return true;
+ }
+
+ private void navigateToContactResultsFragment(String query) {
+ Fragment topFragment = getSupportFragmentManager().findFragmentById(
+ R.id.content_fragment_container);
+
+ // Top fragment is ContactResultsFragment, update search query
+ if (topFragment instanceof ContactResultsFragment) {
+ ((ContactResultsFragment) topFragment).setSearchQuery(query);
+ return;
+ }
+
+ ContactResultsFragment fragment = ContactResultsFragment.newInstance(query);
+ pushContentFragment(fragment, ContactResultsFragment.FRAGMENT_TAG);
+ }
+
+ private void maybeStartInCallActivity(List<Call> callList) {
+ if (callList == null || callList.isEmpty()) {
+ return;
+ }
+
+ L.d(TAG, "Start InCallActivity");
+ Intent launchIntent = new Intent(getApplicationContext(), InCallActivity.class);
+ startActivity(launchIntent);
+ }
+
+ /** If the back button on action bar is available to navigate up. */
+ private boolean isBackNavigationAvailable() {
+ return getSupportFragmentManager().getBackStackEntryCount() > 1;
+ }
+
+ private int getTabFromSharedPreference() {
+ String key = getResources().getString(R.string.pref_start_page_key);
+ String defaultValue = getResources().getStringArray(R.array.tabs_config)[0];
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+ return mTabFactory.getTabIndex(sharedPreferences.getString(key, defaultValue));
+ }
+
+ /** Sets the background of the Activity's action bar to a {@link Drawable} */
+ public void setActionBarBackground(@Nullable Drawable drawable) {
+ if (mToolbarContainer != null) {
+ mToolbarContainer.setBackground(drawable);
+ } else {
+ mToolbar.setBackground(drawable);
+ }
+ }
+}
diff --git a/src/com/android/car/dialer/ui/TelecomActivityViewModel.java b/src/com/android/car/dialer/ui/TelecomActivityViewModel.java
new file mode 100644
index 00000000..c9125c15
--- /dev/null
+++ b/src/com/android/car/dialer/ui/TelecomActivityViewModel.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui;
+
+import android.annotation.IntDef;
+import android.app.Application;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
+import android.content.Context;
+
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MediatorLiveData;
+import androidx.lifecycle.MutableLiveData;
+
+import com.android.car.dialer.R;
+import com.android.car.dialer.livedata.BluetoothHfpStateLiveData;
+import com.android.car.dialer.livedata.BluetoothPairListLiveData;
+import com.android.car.dialer.livedata.BluetoothStateLiveData;
+import com.android.car.dialer.log.L;
+import com.android.car.dialer.telecom.UiBluetoothMonitor;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Set;
+
+/**
+ * View model for {@link TelecomActivity}.
+ */
+public class TelecomActivityViewModel extends AndroidViewModel {
+ private static final String TAG = "CD.TelecomActivityViewModel";
+ /** A constant which indicates that there's no Bluetooth error. */
+ public static final String NO_BT_ERROR = "NO_ERROR";
+
+ private final Context mApplicationContext;
+ private final LiveData<String> mErrorStringLiveData;
+ private final MutableLiveData<Integer> mDialerAppStateLiveData;
+
+ /**
+ * App state indicates if bluetooth is connected or it should just show the content fragments.
+ */
+ @IntDef({DialerAppState.DEFAULT, DialerAppState.BLUETOOTH_ERROR,
+ DialerAppState.EMERGENCY_DIALPAD})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface DialerAppState {
+ int DEFAULT = 0;
+ int BLUETOOTH_ERROR = 1;
+ int EMERGENCY_DIALPAD = 2;
+ }
+
+ public TelecomActivityViewModel(Application application) {
+ super(application);
+ mApplicationContext = application.getApplicationContext();
+
+ if (BluetoothAdapter.getDefaultAdapter() == null) {
+ MutableLiveData<String> bluetoothUnavailableLiveData = new MutableLiveData<>();
+ bluetoothUnavailableLiveData.setValue(
+ mApplicationContext.getString(R.string.bluetooth_unavailable));
+ mErrorStringLiveData = bluetoothUnavailableLiveData;
+ } else {
+ UiBluetoothMonitor uiBluetoothMonitor = UiBluetoothMonitor.get();
+ mErrorStringLiveData = new ErrorStringLiveData(
+ mApplicationContext,
+ uiBluetoothMonitor.getHfpStateLiveData(),
+ uiBluetoothMonitor.getPairListLiveData(),
+ uiBluetoothMonitor.getBluetoothStateLiveData());
+ }
+
+ mDialerAppStateLiveData = new DialerAppStateLiveData(mErrorStringLiveData);
+ }
+
+ public MutableLiveData<Integer> getDialerAppState() {
+ return mDialerAppStateLiveData;
+ }
+
+ /**
+ * Returns a LiveData which provides the warning string based on Bluetooth states. Returns
+ * {@link #NO_BT_ERROR} if there's no error.
+ */
+ public LiveData<String> getErrorMessage() {
+ return mErrorStringLiveData;
+ }
+
+ private static class DialerAppStateLiveData extends MediatorLiveData<Integer> {
+ private final LiveData<String> mErrorStringLiveData;
+
+ private DialerAppStateLiveData(LiveData<String> errorStringLiveData) {
+ this.mErrorStringLiveData = errorStringLiveData;
+ setValue(DialerAppState.DEFAULT);
+
+ addSource(mErrorStringLiveData, errorMsg -> updateDialerAppState());
+ }
+
+ private void updateDialerAppState() {
+ L.d(TAG, "updateDialerAppState, error: %s", mErrorStringLiveData.getValue());
+
+ // If bluetooth is not connected, user can make an emergency call. So show the in
+ // call fragment no matter if bluetooth is connected or not.
+ // Bluetooth error
+ if (!NO_BT_ERROR.equals(mErrorStringLiveData.getValue())) {
+ // Currently bluetooth is not connected, stay on the emergency dial pad page.
+ if (getValue() == DialerAppState.EMERGENCY_DIALPAD) {
+ return;
+ }
+ setValue(DialerAppState.BLUETOOTH_ERROR);
+ return;
+ }
+
+ // Bluetooth connected.
+ setValue(DialerAppState.DEFAULT);
+ }
+
+ @Override
+ public void setValue(@DialerAppState Integer newValue) {
+ // Only set value and notify observers when the value changes.
+ if (getValue() != newValue) {
+ super.setValue(newValue);
+ }
+ }
+ }
+
+ private static class ErrorStringLiveData extends MediatorLiveData<String> {
+ private LiveData<Integer> mHfpStateLiveData;
+ private LiveData<Set<BluetoothDevice>> mPairedListLiveData;
+ private LiveData<Integer> mBluetoothStateLiveData;
+
+ private Context mContext;
+
+ ErrorStringLiveData(Context context,
+ BluetoothHfpStateLiveData hfpStateLiveData,
+ BluetoothPairListLiveData pairListLiveData,
+ BluetoothStateLiveData bluetoothStateLiveData) {
+ mContext = context;
+ mHfpStateLiveData = hfpStateLiveData;
+ mPairedListLiveData = pairListLiveData;
+ mBluetoothStateLiveData = bluetoothStateLiveData;
+ setValue(NO_BT_ERROR);
+
+ addSource(hfpStateLiveData, this::onHfpStateChanged);
+ addSource(pairListLiveData, this::onPairListChanged);
+ addSource(bluetoothStateLiveData, this::onBluetoothStateChanged);
+ }
+
+ private void onHfpStateChanged(Integer state) {
+ update();
+ }
+
+ private void onPairListChanged(Set<BluetoothDevice> pairedDevices) {
+ update();
+ }
+
+ private void onBluetoothStateChanged(Integer state) {
+ update();
+ }
+
+ private void update() {
+ boolean isBluetoothEnabled = isBluetoothEnabled();
+ boolean hasPairedDevices = hasPairedDevices();
+ boolean isHfpConnected = isHfpConnected();
+ L.d(TAG, "Update error string."
+ + " isBluetoothEnabled: %s"
+ + " hasPairedDevices: %s"
+ + " isHfpConnected: %s",
+ isBluetoothEnabled,
+ hasPairedDevices,
+ isHfpConnected);
+ if (!isBluetoothEnabled) {
+ setValue(mContext.getString(R.string.bluetooth_disabled));
+ } else if (!hasPairedDevices) {
+ setValue(mContext.getString(R.string.bluetooth_unpaired));
+ } else if (!isHfpConnected) {
+ setValue(mContext.getString(R.string.no_hfp));
+ } else {
+ if (!NO_BT_ERROR.equals(getValue())) {
+ setValue(NO_BT_ERROR);
+ }
+ }
+ }
+
+ private boolean isHfpConnected() {
+ Integer hfpState = mHfpStateLiveData.getValue();
+ return hfpState == null || hfpState == BluetoothProfile.STATE_CONNECTED;
+ }
+
+ private boolean isBluetoothEnabled() {
+ Integer bluetoothState = mBluetoothStateLiveData.getValue();
+ return bluetoothState == null
+ || bluetoothState != BluetoothStateLiveData.BluetoothState.DISABLED;
+ }
+
+ private boolean hasPairedDevices() {
+ Set<BluetoothDevice> pairedDevices = mPairedListLiveData.getValue();
+ return pairedDevices == null || !pairedDevices.isEmpty();
+ }
+ }
+}
diff --git a/src/com/android/car/dialer/ui/TelecomPageTab.java b/src/com/android/car/dialer/ui/TelecomPageTab.java
new file mode 100644
index 00000000..2c6bf335
--- /dev/null
+++ b/src/com/android/car/dialer/ui/TelecomPageTab.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.StringDef;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+
+import com.android.car.apps.common.widget.CarTabLayout;
+import com.android.car.dialer.R;
+import com.android.car.dialer.ui.calllog.CallHistoryFragment;
+import com.android.car.dialer.ui.contact.ContactListFragment;
+import com.android.car.dialer.ui.dialpad.DialpadFragment;
+import com.android.car.dialer.ui.favorite.FavoriteFragment;
+
+import com.google.common.collect.ImmutableMap;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/** Tab presenting fragments. */
+public class TelecomPageTab extends CarTabLayout.CarTab {
+
+ /** Note: the strings must be consist with the items in string array tabs_config */
+ @StringDef({
+ TelecomPageTab.Page.FAVORITES,
+ TelecomPageTab.Page.CALL_HISTORY,
+ TelecomPageTab.Page.CONTACTS,
+ TelecomPageTab.Page.DIAL_PAD
+ })
+ public @interface Page {
+ String FAVORITES = "FAVORITE";
+ String CALL_HISTORY = "CALL_HISTORY";
+ String CONTACTS = "CONTACTS";
+ String DIAL_PAD = "DIAL_PAD";
+ }
+
+ private final Factory mFactory;
+ private Fragment mFragment;
+ private String mFragmentTag;
+ private boolean mWasFragmentRestored;
+
+ private TelecomPageTab(@Nullable Drawable icon, @Nullable CharSequence text, Factory factory) {
+ super(icon, text);
+ mFactory = factory;
+ }
+
+ /**
+ * Either restore fragment from saved state or create new instance.
+ */
+ private void initFragment(FragmentManager fragmentManager, @Page String page) {
+ mFragmentTag = makeFragmentTag(page);
+ mFragment = fragmentManager.findFragmentByTag(mFragmentTag);
+ if (mFragment == null) {
+ mFragment = mFactory.createFragment(page);
+ mWasFragmentRestored = false;
+ return;
+ }
+ mWasFragmentRestored = true;
+ }
+
+ /** Returns true if the fragment for this tab is restored from a saved state. */
+ public boolean wasFragmentRestored() {
+ return mWasFragmentRestored;
+ }
+
+ /** Returns the fragment for this tab. */
+ public Fragment getFragment() {
+ return mFragment;
+ }
+
+ /** Returns the fragment tag for this tab. */
+ public String getFragmentTag() {
+ return mFragmentTag;
+ }
+
+ private String makeFragmentTag(@Page String page) {
+ return String.format("%s:%s", getClass().getSimpleName(), page);
+ }
+
+ /** Responsible for creating the top tab items and their fragments. */
+ public static class Factory {
+
+ private static final ImmutableMap<String, Integer> TAB_LABELS =
+ ImmutableMap.<String, Integer>builder()
+ .put(Page.FAVORITES, R.string.favorites_title)
+ .put(Page.CALL_HISTORY, R.string.call_history_title)
+ .put(Page.CONTACTS, R.string.contacts_title)
+ .put(Page.DIAL_PAD, R.string.dialpad_title)
+ .build();
+
+ private static final ImmutableMap<String, Integer> TAB_ICONS =
+ ImmutableMap.<String, Integer>builder()
+ .put(Page.FAVORITES, R.drawable.ic_favorite)
+ .put(Page.CALL_HISTORY, R.drawable.ic_history)
+ .put(Page.CONTACTS, R.drawable.ic_contact)
+ .put(Page.DIAL_PAD, R.drawable.ic_dialpad)
+ .build();
+
+ private final FragmentManager mFragmentManager;
+ private final Map<String, Integer> mTabPageIndexMap;
+ private final String[] mTabs;
+
+ public Factory(Context context, FragmentManager fragmentManager) {
+ mFragmentManager = fragmentManager;
+
+ mTabs = context.getResources().getStringArray(R.array.tabs_config);
+
+ mTabPageIndexMap = new HashMap<>();
+ for (int i = 0; i < getTabCount(); i++) {
+ mTabPageIndexMap.put(mTabs[i], i);
+ }
+ }
+
+ private Fragment createFragment(@Page String page) {
+ switch (page) {
+ case Page.FAVORITES:
+ return FavoriteFragment.newInstance();
+ case Page.CALL_HISTORY:
+ return CallHistoryFragment.newInstance();
+ case Page.CONTACTS:
+ return ContactListFragment.newInstance();
+ case Page.DIAL_PAD:
+ return DialpadFragment.newPlaceCallDialpad();
+ default:
+ throw new UnsupportedOperationException("Tab is not supported.");
+ }
+ }
+
+ public TelecomPageTab createTab(Context context, int tabIndex) {
+ String page = mTabs[tabIndex];
+ TelecomPageTab telecomPageTab = new TelecomPageTab(
+ context.getDrawable(TAB_ICONS.get(page)),
+ context.getString(TAB_LABELS.get(page)), this);
+ telecomPageTab.initFragment(mFragmentManager, page);
+ return telecomPageTab;
+ }
+
+ public int getTabCount() {
+ return mTabs.length;
+ }
+
+ public int getTabIndex(@Page String page) {
+ return mTabPageIndexMap.containsKey(page) ? mTabPageIndexMap.get(page) : -1;
+ }
+ }
+}
diff --git a/src/com/android/car/dialer/ui/activecall/InCallActivity.java b/src/com/android/car/dialer/ui/activecall/InCallActivity.java
new file mode 100644
index 00000000..e374564b
--- /dev/null
+++ b/src/com/android/car/dialer/ui/activecall/InCallActivity.java
@@ -0,0 +1,105 @@
+/**
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.activecall;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.telecom.Call;
+
+import androidx.core.util.Pair;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModelProviders;
+
+import com.android.car.arch.common.LiveDataFunctions;
+import com.android.car.dialer.Constants;
+import com.android.car.dialer.R;
+import com.android.car.dialer.log.L;
+
+import java.util.List;
+
+/** Activity for ongoing call and incoming call. */
+public class InCallActivity extends FragmentActivity {
+ private static final String TAG = "CD.InCallActivity";
+ private Fragment mOngoingCallFragment;
+ private Fragment mIncomingCallFragment;
+
+ private MutableLiveData<Boolean> mShowIncomingCall;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ L.d(TAG, "onCreate");
+
+ setContentView(R.layout.in_call_activity);
+
+ mOngoingCallFragment = getSupportFragmentManager().findFragmentById(
+ R.id.ongoing_call_fragment);
+ mIncomingCallFragment = getSupportFragmentManager().findFragmentById(
+ R.id.incoming_call_fragment);
+
+ mShowIncomingCall = new MutableLiveData();
+ InCallViewModel inCallViewModel = ViewModelProviders.of(this).get(InCallViewModel.class);
+ LiveData<Call> incomingCallLiveData = LiveDataFunctions.iff(mShowIncomingCall,
+ inCallViewModel.getIncomingCall());
+ incomingCallLiveData.observe(this, this::updateIncomingCallVisibility);
+ LiveDataFunctions.pair(inCallViewModel.getOngoingCallList(), incomingCallLiveData).observe(
+ this, this::maybeFinishActivity);
+
+ handleIntent();
+ }
+
+ @Override
+ protected void onNewIntent(Intent i) {
+ super.onNewIntent(i);
+ L.d(TAG, "onNewIntent");
+ setIntent(i);
+ handleIntent();
+ }
+
+ private void maybeFinishActivity(Pair<List<Call>, Call> callList) {
+ if ((callList.first == null || callList.first.isEmpty()) && callList.second == null) {
+ L.d(TAG, "No call to show. Finish InCallActivity");
+ finish();
+ }
+ }
+
+ private void handleIntent() {
+ Intent intent = getIntent();
+
+ if (intent != null && getIntent().getBooleanExtra(
+ Constants.Intents.EXTRA_SHOW_INCOMING_CALL, false)) {
+ mShowIncomingCall.setValue(true);
+ } else {
+ mShowIncomingCall.setValue(false);
+ }
+ }
+
+ private void updateIncomingCallVisibility(Call incomingCall) {
+ if (incomingCall == null) {
+ getSupportFragmentManager().beginTransaction().show(mOngoingCallFragment).hide(
+ mIncomingCallFragment).commit();
+ mShowIncomingCall.setValue(false);
+ setIntent(null);
+ } else {
+ getSupportFragmentManager().beginTransaction().show(mIncomingCallFragment).hide(
+ mOngoingCallFragment).commit();
+ }
+ }
+}
diff --git a/src/com/android/car/dialer/ui/activecall/InCallFragment.java b/src/com/android/car/dialer/ui/activecall/InCallFragment.java
new file mode 100644
index 00000000..c14a7a74
--- /dev/null
+++ b/src/com/android/car/dialer/ui/activecall/InCallFragment.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.activecall;
+
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.telecom.Call;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.Chronometer;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.util.Pair;
+import androidx.fragment.app.Fragment;
+
+import com.android.car.apps.common.BackgroundImageView;
+import com.android.car.apps.common.LetterTileDrawable;
+import com.android.car.dialer.R;
+import com.android.car.dialer.log.L;
+import com.android.car.dialer.ui.view.ContactAvatarOutputlineProvider;
+import com.android.car.telephony.common.CallDetail;
+import com.android.car.telephony.common.TelecomUtils;
+
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.request.RequestOptions;
+import com.bumptech.glide.request.target.SimpleTarget;
+import com.bumptech.glide.request.transition.Transition;
+
+/** A fragment that displays information about a call with actions. */
+public abstract class InCallFragment extends Fragment {
+ private static final String TAG = "CD.InCallFragment";
+
+ private View mUserProfileContainerView;
+ private TextView mPhoneNumberView;
+ private Chronometer mUserProfileCallStateText;
+ private TextView mNameView;
+ private ImageView mAvatarView;
+ private BackgroundImageView mBackgroundImage;
+
+ /**
+ * Shared UI elements between ongoing call and incoming call page: {@link BackgroundImageView}
+ * and {@link R.layout#user_profile_large}.
+ */
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ mUserProfileContainerView = view.findViewById(R.id.user_profile_container);
+ mNameView = mUserProfileContainerView.findViewById(R.id.user_profile_title);
+ mAvatarView = mUserProfileContainerView.findViewById(R.id.user_profile_avatar);
+ mAvatarView.setOutlineProvider(ContactAvatarOutputlineProvider.get());
+ mPhoneNumberView = mUserProfileContainerView.findViewById(R.id.user_profile_phone_number);
+ mUserProfileCallStateText = mUserProfileContainerView.findViewById(
+ R.id.user_profile_call_state);
+ mBackgroundImage = view.findViewById(R.id.background_image);
+ }
+
+ /** Presents the user profile. */
+ protected void bindUserProfileView(@Nullable CallDetail callDetail) {
+ L.i(TAG, "bindUserProfileView: %s", callDetail);
+ if (callDetail == null) {
+ return;
+ }
+
+ String number = callDetail.getNumber();
+ Pair<String, Uri> displayNameAndAvatarUri = TelecomUtils.getDisplayNameAndAvatarUri(
+ getContext(), number);
+
+ mNameView.setText(displayNameAndAvatarUri.first);
+
+ String phoneNumberLabel = TelecomUtils.getTypeFromNumber(getContext(), number).toString();
+ if (!phoneNumberLabel.isEmpty()) {
+ phoneNumberLabel += " ";
+ }
+ phoneNumberLabel += TelecomUtils.getFormattedNumber(getContext(), number);
+ if (!TextUtils.isEmpty(phoneNumberLabel) && !phoneNumberLabel.equals(
+ displayNameAndAvatarUri.first)) {
+ mPhoneNumberView.setText(phoneNumberLabel);
+ mPhoneNumberView.setVisibility(View.VISIBLE);
+ } else {
+ mPhoneNumberView.setVisibility(View.GONE);
+ }
+
+ LetterTileDrawable letterTile = TelecomUtils.createLetterTile(
+ getContext(), displayNameAndAvatarUri.first);
+
+ Glide.with(getContext())
+ .asBitmap()
+ .load(displayNameAndAvatarUri.second)
+ .apply(new RequestOptions().centerCrop().error(letterTile))
+ .into(new SimpleTarget<Bitmap>() {
+ @Override
+ public void onResourceReady(Bitmap resource,
+ Transition<? super Bitmap> glideAnimation) {
+ // set showAnimation to false mostly because bindUserProfileView will be
+ // called several times, and we don't want the image to flicker
+ mBackgroundImage.setBackgroundImage(resource, false);
+ mAvatarView.setImageBitmap(resource);
+ }
+
+ @Override
+ public void onLoadFailed(Drawable errorDrawable) {
+ mBackgroundImage.setBackgroundColor(letterTile.getColor());
+ mAvatarView.setImageDrawable(letterTile);
+ }
+ });
+ }
+
+ /** Presents the call state and call duration. */
+ protected void updateCallDescription(@Nullable Pair<Integer, Long> callStateAndConnectTime) {
+ if (callStateAndConnectTime == null || callStateAndConnectTime.first == null) {
+ mUserProfileCallStateText.stop();
+ mUserProfileCallStateText.setText("");
+ return;
+ }
+ if (callStateAndConnectTime.first == Call.STATE_ACTIVE) {
+ mUserProfileCallStateText.setBase(callStateAndConnectTime.second
+ - System.currentTimeMillis() + SystemClock.elapsedRealtime());
+ mUserProfileCallStateText.start();
+ } else {
+ mUserProfileCallStateText.stop();
+ mUserProfileCallStateText.setText(
+ TelecomUtils.callStateToUiString(getContext(),
+ callStateAndConnectTime.first));
+ }
+ }
+}
diff --git a/src/com/android/car/dialer/ui/activecall/InCallViewModel.java b/src/com/android/car/dialer/ui/activecall/InCallViewModel.java
new file mode 100644
index 00000000..f5f8704e
--- /dev/null
+++ b/src/com/android/car/dialer/ui/activecall/InCallViewModel.java
@@ -0,0 +1,294 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.activecall;
+
+import android.app.Application;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+import android.telecom.Call;
+
+import androidx.annotation.NonNull;
+import androidx.core.util.Pair;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.Transformations;
+
+import com.android.car.arch.common.LiveDataFunctions;
+import com.android.car.dialer.livedata.AudioRouteLiveData;
+import com.android.car.dialer.livedata.CallDetailLiveData;
+import com.android.car.dialer.livedata.CallStateLiveData;
+import com.android.car.dialer.log.L;
+import com.android.car.dialer.telecom.InCallServiceImpl;
+import com.android.car.telephony.common.CallDetail;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.Lists;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * View model for {@link InCallActivity} and {@link OngoingCallFragment}. UI that doesn't belong to
+ * in call page should use a different ViewModel.
+ */
+public class InCallViewModel extends AndroidViewModel implements
+ InCallServiceImpl.ActiveCallListChangedCallback {
+ private static final String TAG = "CD.InCallViewModel";
+
+ private final MutableLiveData<List<Call>> mCallListLiveData;
+ private final LiveData<List<Call>> mOngoingCallListLiveData;
+ private final Comparator<Call> mCallComparator;
+
+ private final LiveData<Call> mIncomingCallLiveData;
+
+ private final LiveData<CallDetail> mCallDetailLiveData;
+ private final LiveData<Integer> mCallStateLiveData;
+ private final LiveData<Call> mPrimaryCallLiveData;
+ private final LiveData<Call> mSecondaryCallLiveData;
+ private final LiveData<CallDetail> mSecondaryCallDetailLiveData;
+ private final LiveData<Integer> mAudioRouteLiveData;
+ private LiveData<Long> mCallConnectTimeLiveData;
+ private LiveData<Pair<Integer, Long>> mCallStateAndConnectTimeLiveData;
+ private final Context mContext;
+
+ private InCallServiceImpl mInCallService;
+ private final ServiceConnection mInCallServiceConnection = new ServiceConnection() {
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder binder) {
+ L.d(TAG, "onServiceConnected: %s, service: %s", name, binder);
+ mInCallService = ((InCallServiceImpl.LocalBinder) binder).getService();
+ updateCallList();
+ mInCallService.addActiveCallListChangedCallback(InCallViewModel.this);
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ L.d(TAG, "onServiceDisconnected: %s", name);
+ mInCallService = null;
+ }
+ };
+
+ // Reuse the same instance so the callback won't be registered more than once.
+ private final Call.Callback mCallStateChangedCallback = new Call.Callback() {
+ @Override
+ public void onStateChanged(Call call, int state) {
+ // Sets value to trigger the live data for incoming call and active call list to update.
+ mCallListLiveData.setValue(mCallListLiveData.getValue());
+ }
+ };
+
+ public InCallViewModel(@NonNull Application application) {
+ super(application);
+ mContext = application.getApplicationContext();
+
+ mCallListLiveData = new MutableLiveData<>();
+ mCallComparator = new CallComparator();
+
+ mIncomingCallLiveData = Transformations.map(mCallListLiveData,
+ callList -> firstMatch(callList,
+ call -> call != null && call.getState() == Call.STATE_RINGING));
+
+ mOngoingCallListLiveData = Transformations.map(mCallListLiveData,
+ callList -> {
+ List<Call> activeCallList = filter(callList,
+ call -> call != null && call.getState() != Call.STATE_RINGING);
+ activeCallList.sort(mCallComparator);
+ return activeCallList;
+ });
+
+ mPrimaryCallLiveData = Transformations.map(mOngoingCallListLiveData,
+ input -> input.isEmpty() ? null : input.get(0));
+ mCallDetailLiveData = Transformations.switchMap(mPrimaryCallLiveData,
+ input -> input != null ? new CallDetailLiveData(input) : null);
+ mCallStateLiveData = Transformations.switchMap(mPrimaryCallLiveData,
+ input -> input != null ? new CallStateLiveData(input) : null);
+ mCallConnectTimeLiveData = Transformations.map(mCallDetailLiveData, (details) -> {
+ if (details == null) {
+ return 0L;
+ }
+ return details.getConnectTimeMillis();
+ });
+ mCallStateAndConnectTimeLiveData =
+ LiveDataFunctions.pair(mCallStateLiveData, mCallConnectTimeLiveData);
+
+ mSecondaryCallLiveData = Transformations.map(mOngoingCallListLiveData,
+ callList -> (callList != null && callList.size() > 1) ? callList.get(1) : null);
+
+ mSecondaryCallDetailLiveData = Transformations.switchMap(mSecondaryCallLiveData,
+ input -> input != null ? new CallDetailLiveData(input) : null);
+
+ mAudioRouteLiveData = new AudioRouteLiveData(mContext);
+
+ Intent intent = new Intent(mContext, InCallServiceImpl.class);
+ intent.setAction(InCallServiceImpl.ACTION_LOCAL_BIND);
+ mContext.bindService(intent, mInCallServiceConnection, Context.BIND_AUTO_CREATE);
+ }
+
+ /** Returns the live data which monitors the current incoming call. */
+ public LiveData<Call> getIncomingCall() {
+ return mIncomingCallLiveData;
+ }
+
+ /** Returns {@link LiveData} for the ongoing call list which excludes the ringing call. */
+ public LiveData<List<Call>> getOngoingCallList() {
+ return mOngoingCallListLiveData;
+ }
+
+ /**
+ * Returns the live data which monitors the primary call details.
+ */
+ public LiveData<CallDetail> getPrimaryCallDetail() {
+ return mCallDetailLiveData;
+ }
+
+ /**
+ * Returns the live data which monitors the primary call state.
+ */
+ public LiveData<Integer> getPrimaryCallState() {
+ return mCallStateLiveData;
+ }
+
+ /**
+ * Returns the live data which monitors the primary call state and the start time of the call.
+ */
+ public LiveData<Pair<Integer, Long>> getCallStateAndConnectTime() {
+ return mCallStateAndConnectTimeLiveData;
+ }
+
+ /**
+ * Returns the live data which monitor the primary call.
+ * A primary call in the first call in the ongoing call list,
+ * which is sorted based on {@link CallComparator}.
+ */
+ public LiveData<Call> getPrimaryCall() {
+ return mPrimaryCallLiveData;
+ }
+
+ /**
+ * Returns the live data which monitor the secondary call.
+ * A secondary call in the second call in the ongoing call list,
+ * which is sorted based on {@link CallComparator}.
+ * The value will be null if there is no second call in the call list.
+ */
+ public LiveData<Call> getSecondaryCall() {
+ return mSecondaryCallLiveData;
+ }
+
+ /**
+ * Returns the live data which monitors the secondary call details.
+ */
+ public LiveData<CallDetail> getSecondaryCallDetail() {
+ return mSecondaryCallDetailLiveData;
+ }
+
+ /**
+ * Returns current audio route.
+ */
+ public LiveData<Integer> getAudioRoute() {
+ return mAudioRouteLiveData;
+ }
+
+ @Override
+ public boolean onTelecomCallAdded(Call telecomCall) {
+ L.i(TAG, "onTelecomCallAdded %s %s", telecomCall, this);
+ telecomCall.registerCallback(mCallStateChangedCallback);
+ updateCallList();
+ return false;
+ }
+
+ @Override
+ public boolean onTelecomCallRemoved(Call telecomCall) {
+ L.i(TAG, "onTelecomCallRemoved %s %s", telecomCall, this);
+ telecomCall.unregisterCallback(mCallStateChangedCallback);
+ updateCallList();
+ return false;
+ }
+
+ private void updateCallList() {
+ List<Call> callList = new ArrayList<>();
+ callList.addAll(mInCallService.getCalls());
+ mCallListLiveData.setValue(callList);
+ }
+
+ @Override
+ protected void onCleared() {
+ mContext.unbindService(mInCallServiceConnection);
+ if (mInCallService != null) {
+ mInCallService.removeActiveCallListChangedCallback(this);
+ }
+ mInCallService = null;
+ }
+
+ private static class CallComparator implements Comparator<Call> {
+ /**
+ * The rank of call state. Used for sorting active calls. Rank is listed from lowest to
+ * highest.
+ */
+ private static final List<Integer> CALL_STATE_RANK = Lists.newArrayList(
+ Call.STATE_RINGING,
+ Call.STATE_DISCONNECTED,
+ Call.STATE_DISCONNECTING,
+ Call.STATE_NEW,
+ Call.STATE_CONNECTING,
+ Call.STATE_SELECT_PHONE_ACCOUNT,
+ Call.STATE_HOLDING,
+ Call.STATE_ACTIVE,
+ Call.STATE_DIALING);
+
+ @Override
+ public int compare(Call call, Call otherCall) {
+ boolean callHasParent = call.getParent() != null;
+ boolean otherCallHasParent = otherCall.getParent() != null;
+
+ if (callHasParent && !otherCallHasParent) {
+ return 1;
+ } else if (!callHasParent && otherCallHasParent) {
+ return -1;
+ }
+ int carCallRank = CALL_STATE_RANK.indexOf(call.getState());
+ int otherCarCallRank = CALL_STATE_RANK.indexOf(otherCall.getState());
+
+ return otherCarCallRank - carCallRank;
+ }
+ }
+
+ private static Call firstMatch(List<Call> callList, Predicate<Call> predicate) {
+ List<Call> filteredResults = filter(callList, predicate);
+ return filteredResults.isEmpty() ? null : filteredResults.get(0);
+ }
+
+ private static List<Call> filter(List<Call> callList, Predicate<Call> predicate) {
+ if (callList == null || predicate == null) {
+ return Collections.emptyList();
+ }
+
+ List<Call> filteredResults = new ArrayList<>();
+ for (Call call : callList) {
+ if (predicate.apply(call)) {
+ filteredResults.add(call);
+ }
+ }
+ return filteredResults;
+ }
+}
diff --git a/src/com/android/car/dialer/ui/activecall/IncomingCallFragment.java b/src/com/android/car/dialer/ui/activecall/IncomingCallFragment.java
new file mode 100644
index 00000000..aa5c7c45
--- /dev/null
+++ b/src/com/android/car/dialer/ui/activecall/IncomingCallFragment.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.activecall;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.ViewModelProviders;
+
+import com.android.car.dialer.R;
+import com.android.car.telephony.common.CallDetail;
+
+/** Fragment that presents the incoming call. */
+public class IncomingCallFragment extends InCallFragment {
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ View fragmentView = inflater.inflate(R.layout.incoming_call_fragment, container, false);
+ TextView mCallStateView = fragmentView.findViewById(R.id.user_profile_call_state);
+ mCallStateView.setText(getString(R.string.call_state_call_ringing));
+
+ InCallViewModel inCallViewModel = ViewModelProviders.of(getActivity()).get(
+ InCallViewModel.class);
+ inCallViewModel.getIncomingCall().observe(this, call -> bindUserProfileView(
+ call == null ? null : CallDetail.fromTelecomCallDetail(call.getDetails())));
+ return fragmentView;
+ }
+}
diff --git a/src/com/android/car/dialer/ui/activecall/OnGoingCallControllerBarFragment.java b/src/com/android/car/dialer/ui/activecall/OnGoingCallControllerBarFragment.java
new file mode 100644
index 00000000..89538229
--- /dev/null
+++ b/src/com/android/car/dialer/ui/activecall/OnGoingCallControllerBarFragment.java
@@ -0,0 +1,328 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.activecall;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.os.Bundle;
+import android.telecom.Call;
+import android.telecom.CallAudioState;
+import android.telecom.CallAudioState.CallAudioRoute;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModelProviders;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.car.dialer.R;
+import com.android.car.dialer.log.L;
+import com.android.car.dialer.telecom.UiCallManager;
+
+import com.google.common.collect.ImmutableMap;
+
+import java.util.List;
+
+/** A Fragment of the bar which controls on going call. */
+public class OnGoingCallControllerBarFragment extends Fragment {
+ private static String TAG = "CDialer.OngoingCallCtlFrg";
+
+ private static final ImmutableMap<Integer, AudioRouteInfo> AUDIO_ROUTES =
+ ImmutableMap.<Integer, AudioRouteInfo>builder()
+ .put(CallAudioState.ROUTE_WIRED_HEADSET, new AudioRouteInfo(
+ R.string.audio_route_handset,
+ R.drawable.ic_smartphone,
+ R.drawable.ic_smartphone_activatable))
+ .put(CallAudioState.ROUTE_EARPIECE, new AudioRouteInfo(
+ R.string.audio_route_handset,
+ R.drawable.ic_smartphone,
+ R.drawable.ic_smartphone_activatable))
+ .put(CallAudioState.ROUTE_BLUETOOTH, new AudioRouteInfo(
+ R.string.audio_route_vehicle,
+ R.drawable.ic_bluetooth,
+ R.drawable.ic_bluetooth_activatable))
+ .put(CallAudioState.ROUTE_SPEAKER, new AudioRouteInfo(
+ R.string.audio_route_phone_speaker,
+ R.drawable.ic_speaker_phone,
+ R.drawable.ic_speaker_phone_activatable))
+ .build();
+
+ private AlertDialog mAudioRouteSelectionDialog;
+ private AudioRouteListAdapter mAudioRouteAdapter;
+ private ImageView mMuteButton;
+ private ImageView mAudioRouteButton;
+ private ImageView mPauseButton;
+ private LiveData<Call> mCallLiveData;
+ private MutableLiveData<Boolean> mDialpadState;
+ private int mCallState;
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ View dialogView = LayoutInflater.from(getContext()).inflate(
+ R.layout.audio_route_switch_dialog, null, false);
+ RecyclerView list = dialogView.findViewById(R.id.list);
+ list.setLayoutManager(new LinearLayoutManager(getContext()));
+
+ mAudioRouteSelectionDialog = new AlertDialog.Builder(getContext())
+ .setView(dialogView)
+ .create();
+
+ List<Integer> availableRoutes = UiCallManager.get().getSupportedAudioRoute();
+ int activeRoute = UiCallManager.get().getAudioRoute();
+ mAudioRouteAdapter = new AudioRouteListAdapter(getContext(), availableRoutes, activeRoute);
+ list.setAdapter(mAudioRouteAdapter);
+
+ InCallViewModel inCallViewModel = ViewModelProviders.of(getActivity()).get(
+ InCallViewModel.class);
+
+ inCallViewModel.getPrimaryCallState().observe(this, this::setCallState);
+ mCallLiveData = inCallViewModel.getPrimaryCall();
+ inCallViewModel.getAudioRoute().observe(this, this::updateViewBasedOnAudioRoute);
+
+ OngoingCallStateViewModel ongoingCallStateViewModel = ViewModelProviders.of(
+ getActivity()).get(OngoingCallStateViewModel.class);
+ mDialpadState = ongoingCallStateViewModel.getDialpadState();
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ View fragmentView = inflater.inflate(R.layout.on_going_call_controller_bar_fragment,
+ container, false);
+
+ mMuteButton = fragmentView.findViewById(R.id.mute_button);
+ mMuteButton.setOnClickListener((v) -> {
+ if (v.isActivated()) {
+ v.setActivated(false);
+ onUnmuteMic();
+ } else {
+ v.setActivated(true);
+ onMuteMic();
+ }
+ });
+
+ View dialPadButton = fragmentView.findViewById(R.id.toggle_dialpad_button);
+ dialPadButton.setOnClickListener(v -> mDialpadState.setValue(!mDialpadState.getValue()));
+ mDialpadState.observe(this, activated -> dialPadButton.setActivated(activated));
+
+ View endCallButton = fragmentView.findViewById(R.id.end_call_button);
+ endCallButton.setOnClickListener(v -> onEndCall());
+
+ List<Integer> audioRoutes = UiCallManager.get().getSupportedAudioRoute();
+ mAudioRouteButton = fragmentView.findViewById(R.id.voice_channel_button);
+ if (audioRoutes.size() > 1) {
+ mAudioRouteButton.setOnClickListener((v) -> {
+ mAudioRouteButton.setActivated(true);
+ mAudioRouteAdapter.setActiveAudioRoute(UiCallManager.get().getAudioRoute());
+ mAudioRouteSelectionDialog.show();
+ });
+ }
+
+ mAudioRouteSelectionDialog.setOnDismissListener(
+ (dialog) -> mAudioRouteButton.setActivated(false));
+
+ mPauseButton = fragmentView.findViewById(R.id.pause_button);
+ mPauseButton.setOnClickListener((v) -> {
+ if (mCallState == Call.STATE_ACTIVE) {
+ onHoldCall();
+ } else if (mCallState == Call.STATE_HOLDING) {
+ onUnholdCall();
+ } else {
+ L.i(TAG, "Pause button is clicked while call in %s state", mCallState);
+ }
+ });
+ updatePauseButtonEnabledState();
+
+ return fragmentView;
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ L.i(TAG, "onPause");
+ if (mAudioRouteSelectionDialog.isShowing()) {
+ mAudioRouteSelectionDialog.dismiss();
+ }
+ }
+
+ /** Set the call state and change the view for the pause button accordingly */
+ private void setCallState(int callState) {
+ L.d(TAG, "Call State: %s", callState);
+ mCallState = callState;
+ updatePauseButtonEnabledState();
+ }
+
+ private void updatePauseButtonEnabledState() {
+ if (mCallState == Call.STATE_HOLDING) {
+ mPauseButton.setEnabled(true);
+ mPauseButton.setActivated(true);
+ } else if (mCallState == Call.STATE_ACTIVE) {
+ mPauseButton.setEnabled(true);
+ mPauseButton.setActivated(false);
+ } else {
+ mPauseButton.setEnabled(false);
+ }
+ }
+
+ private void onMuteMic() {
+ UiCallManager.get().setMuted(true);
+ }
+
+ private void onUnmuteMic() {
+ UiCallManager.get().setMuted(false);
+ }
+
+ private void onHoldCall() {
+ if (mCallLiveData.getValue() != null) {
+ mCallLiveData.getValue().hold();
+ }
+ }
+
+ private void onUnholdCall() {
+ if (mCallLiveData.getValue() != null) {
+ mCallLiveData.getValue().unhold();
+ }
+ }
+
+ private void onEndCall() {
+ if (mCallLiveData.getValue() != null) {
+ mCallLiveData.getValue().disconnect();
+ }
+ }
+
+ private void onSetAudioRoute(@CallAudioRoute int audioRoute) {
+ UiCallManager.get().setAudioRoute(audioRoute);
+ mAudioRouteSelectionDialog.dismiss();
+ }
+
+ private void updateViewBasedOnAudioRoute(@Nullable Integer audioRoute) {
+ if (audioRoute == null) {
+ return;
+ }
+
+ L.i(TAG, "Audio Route State: " + audioRoute);
+ mAudioRouteButton.setImageResource(getAudioRouteInfo(audioRoute).mIconActivatable);
+
+ updateMuteButtonEnabledState(audioRoute);
+ }
+
+ private void updateMuteButtonEnabledState(Integer audioRoute) {
+ if (audioRoute == CallAudioState.ROUTE_BLUETOOTH) {
+ mMuteButton.setEnabled(true);
+ mMuteButton.setActivated(UiCallManager.get().getMuted());
+ } else {
+ mMuteButton.setEnabled(false);
+ }
+ }
+
+ private static AudioRouteInfo getAudioRouteInfo(int route) {
+ AudioRouteInfo routeInfo = AUDIO_ROUTES.get(route);
+ if (routeInfo != null) {
+ return routeInfo;
+ } else {
+ L.e(TAG, "Unknown audio route: %s", route);
+ throw new RuntimeException("Unknown audio route: " + route);
+ }
+ }
+
+ private static final class AudioRouteInfo {
+ private final int mLabel;
+ private final int mIcon;
+ private final int mIconActivatable;
+
+ private AudioRouteInfo(@StringRes int label,
+ @DrawableRes int icon,
+ @DrawableRes int iconActivatable) {
+ mLabel = label;
+ mIcon = icon;
+ mIconActivatable = iconActivatable;
+ }
+ }
+
+ private class AudioRouteListAdapter extends
+ RecyclerView.Adapter<AudioRouteItemViewHolder> {
+ private List<Integer> mSupportedRoutes;
+ private Context mContext;
+ private int mActiveAudioRoute;
+
+ AudioRouteListAdapter(Context context,
+ List<Integer> supportedRoutes,
+ int activeAudioRoute) {
+ mSupportedRoutes = supportedRoutes;
+ mActiveAudioRoute = activeAudioRoute;
+ mContext = context;
+ if (mSupportedRoutes.contains(CallAudioState.ROUTE_EARPIECE)
+ && mSupportedRoutes.contains(CallAudioState.ROUTE_WIRED_HEADSET)) {
+ // Keep either ROUTE_EARPIECE or ROUTE_WIRED_HEADSET, but not both of them.
+ mSupportedRoutes.remove(CallAudioState.ROUTE_WIRED_HEADSET);
+ }
+ }
+
+ public void setActiveAudioRoute(int route) {
+ if (mActiveAudioRoute != route) {
+ mActiveAudioRoute = route;
+ notifyDataSetChanged();
+ }
+ }
+
+ @Override
+ public AudioRouteItemViewHolder onCreateViewHolder(ViewGroup container, int position) {
+ View listItemView = LayoutInflater.from(mContext).inflate(
+ R.layout.audio_route_list_item, container, false);
+ return new AudioRouteItemViewHolder(listItemView);
+ }
+
+ @Override
+ public void onBindViewHolder(AudioRouteItemViewHolder viewHolder, int position) {
+ int audioRoute = mSupportedRoutes.get(position);
+ AudioRouteInfo routeInfo = getAudioRouteInfo(audioRoute);
+ viewHolder.mBody.setText(routeInfo.mLabel);
+ viewHolder.mIcon.setImageResource(routeInfo.mIcon);
+ viewHolder.itemView.setActivated(audioRoute == mActiveAudioRoute);
+ viewHolder.itemView.setOnClickListener((v) -> onSetAudioRoute(audioRoute));
+ }
+
+ @Override
+ public int getItemCount() {
+ return mSupportedRoutes.size();
+ }
+ }
+
+ private static class AudioRouteItemViewHolder extends RecyclerView.ViewHolder {
+ public final ImageView mIcon;
+ public final TextView mBody;
+
+ public AudioRouteItemViewHolder(View itemView) {
+ super(itemView);
+ mIcon = itemView.findViewById(R.id.icon);
+ mBody = itemView.findViewById(R.id.body);
+ }
+ }
+}
diff --git a/src/com/android/car/dialer/ui/activecall/OnHoldCallUserProfileFragment.java b/src/com/android/car/dialer/ui/activecall/OnHoldCallUserProfileFragment.java
new file mode 100644
index 00000000..7740392a
--- /dev/null
+++ b/src/com/android/car/dialer/ui/activecall/OnHoldCallUserProfileFragment.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.activecall;
+
+import android.net.Uri;
+import android.os.Bundle;
+import android.telecom.Call;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.util.Pair;
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.ViewModelProviders;
+
+import com.android.car.dialer.R;
+import com.android.car.dialer.ui.view.ContactAvatarOutputlineProvider;
+import com.android.car.telephony.common.CallDetail;
+import com.android.car.telephony.common.TelecomUtils;
+
+/**
+ * A fragment that displays information about onhold call.
+ */
+public class OnHoldCallUserProfileFragment extends Fragment {
+
+ private TextView mTitle;
+ private ImageView mAvatarView;
+ private ImageView mSwapCallsButton;
+ private LiveData<Call> mPrimaryCallLiveData;
+ private LiveData<Call> mSecondaryCallLiveData;
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ View fragmentView = inflater.inflate(R.layout.onhold_user_profile, container, false);
+
+ mTitle = fragmentView.findViewById(R.id.title);
+ mAvatarView = fragmentView.findViewById(R.id.icon);
+ mAvatarView.setOutlineProvider(ContactAvatarOutputlineProvider.get());
+
+ mSwapCallsButton = fragmentView.findViewById(R.id.swap_calls_button);
+ mSwapCallsButton.setOnClickListener(v -> swapCalls());
+
+ InCallViewModel inCallViewModel = ViewModelProviders.of(getActivity()).get(
+ InCallViewModel.class);
+ inCallViewModel.getSecondaryCallDetail().observe(this, this::updateProfile);
+ mPrimaryCallLiveData = inCallViewModel.getPrimaryCall();
+ mSecondaryCallLiveData = inCallViewModel.getSecondaryCall();
+
+ return fragmentView;
+ }
+
+ private void updateProfile(@Nullable CallDetail callDetail) {
+ if (callDetail == null) {
+ return;
+ }
+
+ String number = callDetail.getNumber();
+ Pair<String, Uri> displayNameAndAvatarUri = TelecomUtils.getDisplayNameAndAvatarUri(
+ getContext(), number);
+
+ mTitle.setText(displayNameAndAvatarUri.first);
+ TelecomUtils.setContactBitmapAsync(getContext(), mAvatarView,
+ displayNameAndAvatarUri.second, displayNameAndAvatarUri.first);
+ }
+
+ private void swapCalls() {
+ // Unholds onhold call
+ if (mSecondaryCallLiveData.getValue() != null) {
+ mSecondaryCallLiveData.getValue().unhold();
+ }
+
+ // hold primary call
+ if (mPrimaryCallLiveData.getValue().getState() != Call.STATE_HOLDING) {
+ mPrimaryCallLiveData.getValue().hold();
+ }
+ }
+}
diff --git a/src/com/android/car/dialer/ui/activecall/OngoingCallFragment.java b/src/com/android/car/dialer/ui/activecall/OngoingCallFragment.java
new file mode 100644
index 00000000..c7421521
--- /dev/null
+++ b/src/com/android/car/dialer/ui/activecall/OngoingCallFragment.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.activecall;
+
+import android.os.Bundle;
+import android.telecom.Call;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModelProviders;
+
+import com.android.car.apps.common.BackgroundImageView;
+import com.android.car.dialer.R;
+
+import com.google.common.annotations.VisibleForTesting;
+
+/**
+ * A fragment that displays information about an on-going call with options to hang up.
+ */
+public class OngoingCallFragment extends InCallFragment {
+ private Fragment mDialpadFragment;
+ private Fragment mOnholdCallFragment;
+ private View mUserProfileContainerView;
+ private BackgroundImageView mBackgroundImage;
+ private MutableLiveData<Boolean> mDialpadState;
+
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ View fragmentView = inflater.inflate(R.layout.ongoing_call_fragment, container, false);
+
+ mUserProfileContainerView = fragmentView.findViewById(R.id.user_profile_container);
+ mBackgroundImage = fragmentView.findViewById(R.id.background_image);
+ mOnholdCallFragment = getChildFragmentManager().findFragmentById(R.id.onhold_user_profile);
+ mDialpadFragment = getChildFragmentManager().findFragmentById(R.id.incall_dialpad_fragment);
+
+ InCallViewModel inCallViewModel = ViewModelProviders.of(getActivity()).get(
+ InCallViewModel.class);
+
+ inCallViewModel.getPrimaryCallDetail().observe(this, this::bindUserProfileView);
+ inCallViewModel.getCallStateAndConnectTime().observe(this, this::updateCallDescription);
+ inCallViewModel.getSecondaryCall().observe(this, this::maybeShowOnholdCallFragment);
+
+ OngoingCallStateViewModel ongoingCallStateViewModel = ViewModelProviders.of(
+ getActivity()).get(OngoingCallStateViewModel.class);
+ mDialpadState = ongoingCallStateViewModel.getDialpadState();
+ mDialpadState.setValue(savedInstanceState == null ? false : !mDialpadFragment.isHidden());
+ mDialpadState.observe(this, isDialpadOpen -> {
+ if (isDialpadOpen) {
+ onOpenDialpad();
+ } else {
+ onCloseDialpad();
+ }
+ });
+ return fragmentView;
+ }
+
+ @VisibleForTesting
+ void onOpenDialpad() {
+ getChildFragmentManager().beginTransaction()
+ .show(mDialpadFragment)
+ .commit();
+ mUserProfileContainerView.setVisibility(View.GONE);
+ mBackgroundImage.setDimmed(true);
+ }
+
+ @VisibleForTesting
+ void onCloseDialpad() {
+ getChildFragmentManager().beginTransaction()
+ .hide(mDialpadFragment)
+ .commit();
+ mUserProfileContainerView.setVisibility(View.VISIBLE);
+ mBackgroundImage.setDimmed(false);
+ }
+
+ private void maybeShowOnholdCallFragment(@Nullable Call secondaryCall) {
+ if (secondaryCall == null || secondaryCall.getState() != Call.STATE_HOLDING) {
+ getChildFragmentManager().beginTransaction().hide(mOnholdCallFragment).commit();
+ } else {
+ getChildFragmentManager().beginTransaction().show(mOnholdCallFragment).commit();
+ }
+ }
+}
diff --git a/src/com/android/car/dialer/ui/activecall/OngoingCallStateViewModel.java b/src/com/android/car/dialer/ui/activecall/OngoingCallStateViewModel.java
new file mode 100644
index 00000000..fafe5815
--- /dev/null
+++ b/src/com/android/car/dialer/ui/activecall/OngoingCallStateViewModel.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.activecall;
+
+import android.app.Application;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.MutableLiveData;
+
+/** {@link AndroidViewModel} for {@link OnGoingCallControllerBarFragment} action buttons. */
+public class OngoingCallStateViewModel extends AndroidViewModel {
+ private final MutableLiveData<Boolean> mDialpadState;
+
+ public OngoingCallStateViewModel(@NonNull Application application) {
+ super(application);
+ mDialpadState = new MutableLiveData<>();
+ // Set initial value to avoid NPE
+ mDialpadState.setValue(false);
+ }
+
+ /** Return the {@link MutableLiveData} for dialpad state. */
+ public MutableLiveData<Boolean> getDialpadState() {
+ return mDialpadState;
+ }
+}
diff --git a/src/com/android/car/dialer/ui/RingingCallControllerBarFragment.java b/src/com/android/car/dialer/ui/activecall/RingingCallControllerBarFragment.java
index 239c5a38..1c9d8094 100644
--- a/src/com/android/car/dialer/ui/RingingCallControllerBarFragment.java
+++ b/src/com/android/car/dialer/ui/activecall/RingingCallControllerBarFragment.java
@@ -1,23 +1,22 @@
-package com.android.car.dialer.ui;
+package com.android.car.dialer.ui.activecall;
import android.os.Bundle;
+import android.telecom.Call;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import com.android.car.dialer.R;
-import com.android.car.dialer.telecom.UiCall;
-import com.android.car.dialer.telecom.UiCallManager;
-
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.ViewModelProviders;
+
+import com.android.car.dialer.R;
public class RingingCallControllerBarFragment extends Fragment {
- public static RingingCallControllerBarFragment newInstance() {
- return new RingingCallControllerBarFragment();
- }
+ private LiveData<Call> mIncomingCall;
@Nullable
@Override
@@ -34,15 +33,23 @@ public class RingingCallControllerBarFragment extends Fragment {
return fragmentView;
}
+ @Override
+ public void onStart() {
+ super.onStart();
+ InCallViewModel inCallViewModel = ViewModelProviders.of(getActivity()).get(
+ InCallViewModel.class);
+ mIncomingCall = inCallViewModel.getIncomingCall();
+ }
+
private void answerCall() {
- UiCallManager uiCallManager = UiCallManager.get();
- UiCall primaryCall = uiCallManager.getPrimaryCall();
- uiCallManager.answerCall(primaryCall);
+ if (mIncomingCall.getValue() != null) {
+ mIncomingCall.getValue().answer(/* videoState= */0);
+ }
}
private void declineCall() {
- UiCallManager uiCallManager = UiCallManager.get();
- UiCall primaryCall = uiCallManager.getPrimaryCall();
- uiCallManager.rejectCall(primaryCall, false, null);
+ if (mIncomingCall.getValue() != null) {
+ mIncomingCall.getValue().reject(/* rejectWithMessage= */false, /* textMessage= */null);
+ }
}
}
diff --git a/src/com/android/car/dialer/ui/calllog/CallHistoryFragment.java b/src/com/android/car/dialer/ui/calllog/CallHistoryFragment.java
new file mode 100644
index 00000000..c68682cf
--- /dev/null
+++ b/src/com/android/car/dialer/ui/calllog/CallHistoryFragment.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.dialer.ui.calllog;
+
+import android.os.Bundle;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.ViewModelProviders;
+
+import com.android.car.dialer.ui.common.DialerListBaseFragment;
+import com.android.car.dialer.ui.contact.ContactDetailsFragment;
+import com.android.car.telephony.common.Contact;
+
+/** Fragment for call history page. */
+public class CallHistoryFragment extends DialerListBaseFragment implements
+ CallLogAdapter.OnShowContactDetailListener {
+ private static final String CONTACT_DETAIL_FRAGMENT_TAG = "CONTACT_DETAIL_FRAGMENT_TAG";
+
+ public static CallHistoryFragment newInstance() {
+ return new CallHistoryFragment();
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ CallLogAdapter callLogAdapter = new CallLogAdapter(
+ getContext(), /* onShowContactDetailListener= */this);
+ getRecyclerView().setAdapter(callLogAdapter);
+
+ CallHistoryViewModel viewModel = ViewModelProviders.of(this).get(
+ CallHistoryViewModel.class);
+
+ viewModel.getCallHistory().observe(this, callLogAdapter::setUiCallLogs);
+ }
+
+ @Override
+ public void onShowContactDetail(Contact contact) {
+ Fragment contactDetailsFragment = ContactDetailsFragment.newInstance(contact);
+ pushContentFragment(contactDetailsFragment, CONTACT_DETAIL_FRAGMENT_TAG);
+ }
+}
diff --git a/src/com/android/car/dialer/ui/viewmodel/CallHistoryViewModel.java b/src/com/android/car/dialer/ui/calllog/CallHistoryViewModel.java
index 3a8f6c84..5dc4dc48 100644
--- a/src/com/android/car/dialer/ui/viewmodel/CallHistoryViewModel.java
+++ b/src/com/android/car/dialer/ui/calllog/CallHistoryViewModel.java
@@ -1,4 +1,3 @@
-package com.android.car.dialer.ui.viewmodel;
/*
* Copyright (C) 2018 The Android Open Source Project
*
@@ -15,46 +14,39 @@ package com.android.car.dialer.ui.viewmodel;
* limitations under the License.
*/
-import android.app.Application;
-import android.content.Context;
-
-import com.android.car.dialer.livedata.CallHistoryLiveData;
-import com.android.car.dialer.livedata.MissedCallHistoryLiveData;
-import com.android.car.dialer.ui.CallLogListingTask;
-
-import java.util.List;
+package com.android.car.dialer.ui.calllog;
+import android.app.Application;
+import android.text.format.DateUtils;
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
+import com.android.car.dialer.livedata.CallHistoryLiveData;
+import com.android.car.dialer.livedata.HeartBeatLiveData;
+import com.android.car.dialer.ui.common.UiCallLogLiveData;
+import com.android.car.dialer.ui.common.entity.UiCallLog;
+import com.android.car.telephony.common.InMemoryPhoneBook;
+
+import java.util.List;
/**
* View model for CallHistoryFragment which provides call history live data.
*/
public class CallHistoryViewModel extends AndroidViewModel {
- private CallHistoryLiveData mCallHistoryLiveData;
- private MissedCallHistoryLiveData mMissedCallHistoryLiveData;
-
- private Context mContext;
+ private UiCallLogLiveData mUiCallLogLiveData;
- public CallHistoryViewModel(
- @NonNull Application application) {
+ public CallHistoryViewModel(@NonNull Application application) {
super(application);
- mContext = application;
+ mUiCallLogLiveData = new UiCallLogLiveData(application.getApplicationContext(),
+ new HeartBeatLiveData(DateUtils.MINUTE_IN_MILLIS),
+ CallHistoryLiveData.newInstance(application.getApplicationContext()),
+ InMemoryPhoneBook.get().getContactsLiveData());
}
- public LiveData<List<CallLogListingTask.CallLogItem>> getCallHistory() {
- if (mCallHistoryLiveData == null) {
- mCallHistoryLiveData = new CallHistoryLiveData(mContext);
- }
- return mCallHistoryLiveData;
- }
-
- public LiveData<List<CallLogListingTask.CallLogItem>> getMissedCallHistory() {
- if (mMissedCallHistoryLiveData == null) {
- mMissedCallHistoryLiveData = new MissedCallHistoryLiveData(mContext);
- }
-
- return mMissedCallHistoryLiveData;
+ /**
+ * Returns the live data for call history list.
+ */
+ public LiveData<List<UiCallLog>> getCallHistory() {
+ return mUiCallLogLiveData;
}
}
diff --git a/src/com/android/car/dialer/ui/calllog/CallLogAdapter.java b/src/com/android/car/dialer/ui/calllog/CallLogAdapter.java
new file mode 100644
index 00000000..41e7e415
--- /dev/null
+++ b/src/com/android/car/dialer/ui/calllog/CallLogAdapter.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.dialer.ui.calllog;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.car.dialer.R;
+import com.android.car.dialer.log.L;
+import com.android.car.dialer.ui.common.entity.UiCallLog;
+import com.android.car.telephony.common.Contact;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** Adapter for call history list. */
+public class CallLogAdapter extends RecyclerView.Adapter<CallLogViewHolder> {
+
+ private static final String TAG = "CD.CallLogAdapter";
+
+ public interface OnShowContactDetailListener {
+ void onShowContactDetail(Contact contact);
+ }
+
+ private List<UiCallLog> mUiCallLogs = new ArrayList<>();
+ private Context mContext;
+ private CallLogAdapter.OnShowContactDetailListener mOnShowContactDetailListener;
+
+ public CallLogAdapter(Context context,
+ CallLogAdapter.OnShowContactDetailListener onShowContactDetailListener) {
+ mContext = context;
+ mOnShowContactDetailListener = onShowContactDetailListener;
+ }
+
+ public void setUiCallLogs(@NonNull List<UiCallLog> uiCallLogs) {
+ L.d(TAG, "setUiCallLogs: %d", uiCallLogs.size());
+ mUiCallLogs.clear();
+ mUiCallLogs.addAll(uiCallLogs);
+ notifyDataSetChanged();
+ }
+
+ @NonNull
+ @Override
+ public CallLogViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ View rootView = LayoutInflater.from(mContext)
+ .inflate(R.layout.call_history_list_item, parent, false);
+ return new CallLogViewHolder(rootView, mOnShowContactDetailListener);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull CallLogViewHolder holder, int position) {
+ holder.onBind(mUiCallLogs.get(position));
+ }
+
+ @Override
+ public void onViewRecycled(@NonNull CallLogViewHolder holder) {
+ holder.onRecycle();
+ }
+
+ @Override
+ public int getItemCount() {
+ return mUiCallLogs.size();
+ }
+}
+
diff --git a/src/com/android/car/dialer/ui/calllog/CallLogViewHolder.java b/src/com/android/car/dialer/ui/calllog/CallLogViewHolder.java
new file mode 100644
index 00000000..fd0a734c
--- /dev/null
+++ b/src/com/android/car/dialer/ui/calllog/CallLogViewHolder.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.calllog;
+
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.car.dialer.R;
+import com.android.car.dialer.livedata.CallHistoryLiveData;
+import com.android.car.dialer.telecom.UiCallManager;
+import com.android.car.dialer.ui.common.entity.UiCallLog;
+import com.android.car.dialer.ui.view.ContactAvatarOutputlineProvider;
+import com.android.car.dialer.widget.CallTypeIconsView;
+import com.android.car.telephony.common.Contact;
+import com.android.car.telephony.common.InMemoryPhoneBook;
+import com.android.car.telephony.common.PhoneCallLog;
+import com.android.car.telephony.common.TelecomUtils;
+
+/**
+ * {@link RecyclerView.ViewHolder} for call history list item, responsible for presenting and
+ * resetting the UI on recycle.
+ */
+public class CallLogViewHolder extends RecyclerView.ViewHolder {
+
+ private CallLogAdapter.OnShowContactDetailListener mOnShowContactDetailListener;
+ private View mPlaceCallView;
+ private ImageView mAvatarView;
+ private TextView mTitleView;
+ private TextView mCallCountTextView;
+ private TextView mTextView;
+ private CallTypeIconsView mCallTypeIconsView;
+ private View mActionButton;
+ private View mDivider;
+
+ public CallLogViewHolder(@NonNull View itemView,
+ CallLogAdapter.OnShowContactDetailListener onShowContactDetailListener) {
+ super(itemView);
+ mOnShowContactDetailListener = onShowContactDetailListener;
+ mPlaceCallView = itemView.findViewById(R.id.call_action_id);
+ mAvatarView = itemView.findViewById(R.id.icon);
+ mAvatarView.setOutlineProvider(ContactAvatarOutputlineProvider.get());
+ mTitleView = itemView.findViewById(R.id.title);
+ mCallCountTextView = itemView.findViewById(R.id.call_count_text);
+ mTextView = itemView.findViewById(R.id.text);
+ mCallTypeIconsView = itemView.findViewById(R.id.call_type_icons);
+ mActionButton = itemView.findViewById(R.id.calllog_action_button);
+ mDivider = itemView.findViewById(R.id.divider);
+ }
+
+ public void onBind(UiCallLog uiCallLog) {
+ TelecomUtils.setContactBitmapAsync(
+ mAvatarView.getContext(),
+ mAvatarView,
+ uiCallLog.getAvatarUri(),
+ uiCallLog.getTitle());
+ mTitleView.setText(uiCallLog.getTitle());
+ if (uiCallLog.getMostRecentCallType() == CallHistoryLiveData.CallType.MISSED_TYPE) {
+ mTitleView.setTextAppearance(R.style.TextAppearance_CallLogTitleMissedCall);
+ } else {
+ mTitleView.setTextAppearance(R.style.TextAppearance_CallLogTitleDefault);
+ }
+
+ for (PhoneCallLog.Record record : uiCallLog.getCallRecords()) {
+ mCallTypeIconsView.add(record.getCallType());
+ }
+
+ mCallCountTextView.setText(mCallTypeIconsView.getCallCountText());
+ mCallCountTextView.setVisibility(
+ mCallTypeIconsView.getCallCountText() == null ? View.GONE : View.VISIBLE);
+ mTextView.setText(uiCallLog.getText());
+
+ mPlaceCallView.setOnClickListener(
+ view -> UiCallManager.get().placeCall(uiCallLog.getNumber()));
+
+ setUpActionButton(uiCallLog);
+ }
+
+ public void onRecycle() {
+ mCallTypeIconsView.clear();
+ }
+
+ private void setUpActionButton(UiCallLog uiCallLog) {
+ if (mActionButton == null) {
+ return;
+ }
+
+ Contact contact = InMemoryPhoneBook.get().lookupContactEntry(uiCallLog.getNumber());
+
+ if (contact == null) {
+ mActionButton.setVisibility(View.GONE);
+ mDivider.setVisibility(View.GONE);
+ return;
+ }
+ mDivider.setVisibility(View.VISIBLE);
+ mActionButton.setVisibility(View.VISIBLE);
+ mActionButton.setOnClickListener(
+ view -> mOnShowContactDetailListener.onShowContactDetail(contact));
+ }
+}
diff --git a/src/com/android/car/dialer/ui/common/DialerBaseFragment.java b/src/com/android/car/dialer/ui/common/DialerBaseFragment.java
new file mode 100644
index 00000000..2eb276f9
--- /dev/null
+++ b/src/com/android/car/dialer/ui/common/DialerBaseFragment.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.common;
+
+import android.app.ActionBar;
+import android.app.Activity;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+
+import com.android.car.apps.common.util.Themes;
+import com.android.car.dialer.R;
+import com.android.car.dialer.ui.TelecomActivity;
+
+/** The base class for top level dialer content {@link Fragment}s. */
+public abstract class DialerBaseFragment extends Fragment {
+
+ /**
+ * Interface for Dialer top level fragment's parent to implement.
+ */
+ public interface DialerFragmentParent {
+
+ /** Sets the background drawable. */
+ void setBackground(Drawable background);
+
+ /** Push a fragment to the back stack. Update action bar accordingly. */
+ void pushContentFragment(Fragment fragment, String fragmentTag);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setHasOptionsMenu(true);
+ }
+
+ @Override
+ public void onResume() {
+ setFullScreenBackground();
+
+ Activity parentActivity = getActivity();
+ ActionBar actionBar = parentActivity.getActionBar();
+ if (actionBar != null) {
+ setupActionBar(actionBar);
+ }
+
+ super.onResume();
+ }
+
+ /**
+ * Sets a fullscreen background to its parent Activity.
+ */
+ protected void setFullScreenBackground() {
+ Activity parentActivity = getActivity();
+ if (parentActivity instanceof DialerFragmentParent) {
+ ((DialerFragmentParent) parentActivity).setBackground(getFullScreenBackgroundColor());
+ }
+ }
+
+ /** Customizes the action bar. Can be overridden in subclasses. */
+ protected void setupActionBar(@NonNull ActionBar actionBar) {
+ actionBar.setTitle(getActionBarTitle());
+ actionBar.setCustomView(null);
+ setActionBarBackground(getContext().getDrawable(R.color.app_bar_background_color));
+ }
+
+ /**
+ * Returns the full screen background for its parent Activity. Override this function to
+ * change the background.
+ */
+ protected Drawable getFullScreenBackgroundColor() {
+ return new ColorDrawable(Themes.getAttrColor(getContext(), android.R.attr.background));
+ }
+
+ /** Push a fragment to the back stack. Update action bar accordingly. */
+ protected void pushContentFragment(@NonNull Fragment fragment, String fragmentTag) {
+ Activity parentActivity = getActivity();
+ if (parentActivity instanceof DialerFragmentParent) {
+ ((DialerFragmentParent) parentActivity).pushContentFragment(fragment, fragmentTag);
+ }
+ }
+
+ /** Return the action bar title. */
+ protected CharSequence getActionBarTitle() {
+ return getString(R.string.default_toolbar_title);
+ }
+
+ protected int getTopBarHeight() {
+ View toolbar = getActivity().findViewById(R.id.car_toolbar);
+
+ int backStackEntryCount =
+ getActivity().getSupportFragmentManager().getBackStackEntryCount();
+ int topBarHeight = Themes.getAttrDimensionPixelSize(getContext(),
+ android.R.attr.actionBarSize);
+ // Tabs are not child of the toolbar and tabs are visible.
+ if (toolbar.findViewById(R.id.tab_layout) == null && backStackEntryCount == 1) {
+ topBarHeight += topBarHeight;
+ }
+ return topBarHeight;
+ }
+
+ protected final void setActionBarBackground(@Nullable Drawable drawable) {
+ Activity activity = getActivity();
+ if (activity instanceof TelecomActivity) {
+ ((TelecomActivity) activity).setActionBarBackground(drawable);
+ }
+ }
+}
diff --git a/src/com/android/car/dialer/ui/common/DialerListBaseFragment.java b/src/com/android/car/dialer/ui/common/DialerListBaseFragment.java
new file mode 100644
index 00000000..49c3782e
--- /dev/null
+++ b/src/com/android/car/dialer/ui/common/DialerListBaseFragment.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.common;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.LayoutRes;
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.car.apps.common.widget.PagedRecyclerView;
+import com.android.car.dialer.R;
+
+/**
+ * Base fragment that inflates a {@link RecyclerView}. It handles the top offset for first row item
+ * so the list can scroll underneath the top bar.
+ */
+public class DialerListBaseFragment extends DialerBaseFragment {
+
+ private PagedRecyclerView mListView;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = inflater.inflate(getLayoutResource(), container, false);
+ mListView = view.findViewById(R.id.list_view);
+ mListView.setLayoutManager(createLayoutManager());
+ mListView.setPaddingRelative(mListView.getPaddingStart(), getTopOffset(),
+ mListView.getPaddingEnd(), mListView.getPaddingBottom());
+ return view;
+ }
+
+ /** Layout resource for this fragment. It must contains a RecyclerView with id list_view. */
+ @LayoutRes
+ protected int getLayoutResource() {
+ return R.layout.list_fragment;
+ }
+
+ /**
+ * Creates the layout manager for the recycler view. Default is a {@link LinearLayoutManager}.
+ * Child inheriting from this fragment can override to create a different layout manager.
+ */
+ @NonNull
+ protected RecyclerView.LayoutManager createLayoutManager() {
+ return new LinearLayoutManager(getContext());
+ }
+
+ /** Returns the {@link RecyclerView} instance. */
+ @NonNull
+ protected PagedRecyclerView getRecyclerView() {
+ return mListView;
+ }
+
+ /** Gets the top padding for the list. By default it includes the action bar's height */
+ protected int getTopOffset() {
+ int listTopPadding = getContext().getResources().getDimensionPixelSize(
+ R.dimen.list_top_padding);
+ return getTopBarHeight() + listTopPadding;
+ }
+}
diff --git a/src/com/android/car/dialer/ui/common/DialerUtils.java b/src/com/android/car/dialer/ui/common/DialerUtils.java
new file mode 100644
index 00000000..c4c191e3
--- /dev/null
+++ b/src/com/android/car/dialer/ui/common/DialerUtils.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.common;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.res.Resources;
+
+import com.android.car.dialer.R;
+import com.android.car.dialer.log.L;
+import com.android.car.telephony.common.Contact;
+import com.android.car.telephony.common.PhoneNumber;
+import com.android.car.telephony.common.TelecomUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** Ui Utilities for dialer */
+public class DialerUtils {
+
+ private static final String TAG = "CD.DialerUtils";
+
+ /**
+ * Callback interface for
+ * {@link DialerUtils#showPhoneNumberSelector(Context, List, PhoneNumberSelectionCallback)} and
+ * {@link DialerUtils#promptForPrimaryNumber(Context, Contact, PhoneNumberSelectionCallback)}
+ */
+ public interface PhoneNumberSelectionCallback {
+ /**
+ * Called when a phone number is chosen.
+ * @param phoneNumber The phone number
+ * @param always Whether the user pressed "aways" or "just once"
+ */
+ void onPhoneNumberSelected(PhoneNumber phoneNumber, boolean always);
+ }
+
+ /**
+ * Shows a dialog asking the user to pick a phone number.
+ * Has buttons for selecting always or just once.
+ */
+ public static void showPhoneNumberSelector(Context context,
+ List<PhoneNumber> numbers,
+ PhoneNumberSelectionCallback callback) {
+ final List<PhoneNumber> selectedPhoneNumber = new ArrayList<>();
+ new AlertDialog.Builder(context)
+ .setTitle(R.string.select_number_dialog_title)
+ .setSingleChoiceItems(
+ new PhoneNumberListAdapter(context, numbers),
+ -1,
+ ((dialog, which) -> {
+ selectedPhoneNumber.clear();
+ selectedPhoneNumber.add(numbers.get(which));
+ }))
+ .setNeutralButton(R.string.select_number_dialog_just_once_button,
+ (dialog, which) -> {
+ if (!selectedPhoneNumber.isEmpty()) {
+ callback.onPhoneNumberSelected(selectedPhoneNumber.get(0), false);
+ }
+ })
+ .setPositiveButton(R.string.select_number_dialog_always_button,
+ (dialog, which) -> {
+ if (!selectedPhoneNumber.isEmpty()) {
+ callback.onPhoneNumberSelected(selectedPhoneNumber.get(0), true);
+ }
+ })
+ .show();
+ }
+
+ /**
+ * Gets the primary phone number from the contact.
+ * If no primary number is set, a dialog will pop up asking the user to select one.
+ * If the user presses the "always" button, the phone number will become their
+ * primary phone number. The "always" parameter of the callback will always be false
+ * using this method.
+ */
+ public static void promptForPrimaryNumber(
+ Context context,
+ Contact contact,
+ PhoneNumberSelectionCallback callback) {
+ if (contact.hasPrimaryPhoneNumber()) {
+ callback.onPhoneNumberSelected(contact.getPrimaryPhoneNumber(), false);
+ } else if (contact.getNumbers().size() == 1) {
+ callback.onPhoneNumberSelected(contact.getNumbers().get(0), false);
+ } else if (contact.getNumbers().size() > 0) {
+ showPhoneNumberSelector(context, contact.getNumbers(), (phoneNumber, always) -> {
+ if (always) {
+ TelecomUtils.setAsPrimaryPhoneNumber(context, phoneNumber);
+ }
+
+ callback.onPhoneNumberSelected(phoneNumber, false);
+ });
+ } else {
+ L.w(TAG, "contact %s doesn't have any phone number", contact.getDisplayName());
+ }
+ }
+
+ /** Returns true if this a short height screen */
+ public static boolean isShortScreen(Context context) {
+ Resources resources = context.getResources();
+ return resources.getBoolean(R.bool.screen_size_short);
+ }
+
+ /** Returns true if this a tall height screen */
+ public static boolean isTallScreen(Context context) {
+ Resources resources = context.getResources();
+ return resources.getBoolean(R.bool.screen_size_tall);
+ }
+}
diff --git a/src/com/android/car/dialer/ui/common/OnItemClickedListener.java b/src/com/android/car/dialer/ui/common/OnItemClickedListener.java
new file mode 100644
index 00000000..bb50b5da
--- /dev/null
+++ b/src/com/android/car/dialer/ui/common/OnItemClickedListener.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.common;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+/**
+ * Called when an item is clicked in a {@link RecyclerView}.
+ */
+public interface OnItemClickedListener<T> {
+ void onItemClicked(T item);
+}
diff --git a/src/com/android/car/dialer/ui/common/PhoneNumberListAdapter.java b/src/com/android/car/dialer/ui/common/PhoneNumberListAdapter.java
new file mode 100644
index 00000000..923fb51e
--- /dev/null
+++ b/src/com/android/car/dialer/ui/common/PhoneNumberListAdapter.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.common;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.car.dialer.R;
+import com.android.car.telephony.common.PhoneNumber;
+
+import java.util.List;
+
+/**
+ * {@link ArrayAdapter} that simply presents the {@link PhoneNumber} and its type as two line list
+ * item.
+ */
+public class PhoneNumberListAdapter extends ArrayAdapter<PhoneNumber> {
+ private final Context mContext;
+
+ public PhoneNumberListAdapter(Context context, List<PhoneNumber> phoneNumbers) {
+ super(context, R.layout.phone_number_list_item, R.id.phone_number, phoneNumbers);
+ mContext = context;
+ }
+
+ @Override
+ public View getView(int position, @Nullable View convertView,
+ @NonNull ViewGroup parent) {
+ View view = super.getView(position, convertView, parent);
+ PhoneNumber phoneNumber = getItem(position);
+ if (phoneNumber == null) {
+ return view;
+ }
+ TextView phoneNumberView = view.findViewById(R.id.phone_number);
+ phoneNumberView.setText(phoneNumber.getRawNumber());
+ TextView phoneNumberDescriptionView = view.findViewById(R.id.phone_number_description);
+ CharSequence readableLabel = phoneNumber.getReadableLabel(mContext.getResources());
+ if (phoneNumber.isPrimary()) {
+ phoneNumberDescriptionView.setText(
+ mContext.getString(R.string.primary_number_description, readableLabel));
+ } else {
+ phoneNumberDescriptionView.setText(readableLabel);
+ }
+ return view;
+ }
+}
diff --git a/src/com/android/car/dialer/ui/common/UiCallLogLiveData.java b/src/com/android/car/dialer/ui/common/UiCallLogLiveData.java
new file mode 100644
index 00000000..ac326dee
--- /dev/null
+++ b/src/com/android/car/dialer/ui/common/UiCallLogLiveData.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.common;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.text.format.DateUtils;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MediatorLiveData;
+import com.android.car.dialer.R;
+import com.android.car.dialer.livedata.CallHistoryLiveData;
+import com.android.car.dialer.livedata.HeartBeatLiveData;
+import com.android.car.dialer.log.L;
+import com.android.car.telephony.common.TelecomUtils;
+import com.android.car.dialer.ui.common.entity.UiCallLog;
+import com.android.car.telephony.common.Contact;
+import com.android.car.telephony.common.InMemoryPhoneBook;
+import com.android.car.telephony.common.PhoneCallLog;
+import com.android.car.telephony.common.PhoneNumber;
+import com.google.common.base.Joiner;
+import com.google.common.base.Splitter;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Represents a list of call logs for UI representation. This live data get data source from both
+ * call log and contact list. It also refresh itself on the relative time in the body text.
+ */
+public class UiCallLogLiveData extends MediatorLiveData<List<UiCallLog>> {
+ private static final String TAG = "CD.UiCallLogLiveData";
+
+ private static final String TYPE_AND_RELATIVE_TIME_JOINER = ", ";
+ private Context mContext;
+
+ public UiCallLogLiveData(Context context,
+ HeartBeatLiveData heartBeatLiveData,
+ CallHistoryLiveData callHistoryLiveData,
+ LiveData<List<Contact>> contactListLiveData) {
+ mContext = context;
+ addSource(callHistoryLiveData, this::onCallHistoryChanged);
+ addSource(contactListLiveData,
+ (contacts) -> onCallHistoryChanged(callHistoryLiveData.getValue()));
+ addSource(heartBeatLiveData, (trigger) -> updateRelativeTime());
+ }
+
+ private void onCallHistoryChanged(List<PhoneCallLog> callLogs) {
+ setValue(convert(callLogs));
+ }
+
+ private void updateRelativeTime() {
+ boolean hasChanged = false;
+ List<UiCallLog> uiCallLogs = getValue();
+ if (uiCallLogs == null) {
+ return;
+ }
+ for (UiCallLog uiCallLog : uiCallLogs) {
+ String secondaryText = uiCallLog.getText();
+ List<String> splittedSecondaryText = Splitter.on(
+ TYPE_AND_RELATIVE_TIME_JOINER).splitToList(secondaryText);
+
+ String oldRelativeTime;
+ String type = "";
+ if (splittedSecondaryText.size() == 1) {
+ oldRelativeTime = splittedSecondaryText.get(0);
+ } else if (splittedSecondaryText.size() == 2) {
+ type = splittedSecondaryText.get(0);
+ oldRelativeTime = splittedSecondaryText.get(1);
+ } else {
+ L.w(TAG, "secondary text format is incorrect: %s", secondaryText);
+ return;
+ }
+
+ String newRelativeTime = getRelativeTime(uiCallLog.getMostRecentCallEndTimestamp());
+ if (!oldRelativeTime.equals(newRelativeTime)) {
+ String newSecondaryText = getSecondaryText(type, newRelativeTime);
+ uiCallLog.setText(newSecondaryText);
+ hasChanged = true;
+ }
+ }
+
+ if (hasChanged) {
+ setValue(getValue());
+ }
+ }
+
+ private List<UiCallLog> convert(List<PhoneCallLog> phoneCallLogs) {
+ if (phoneCallLogs == null) {
+ return Collections.emptyList();
+ }
+ List<UiCallLog> uiCallLogs = new ArrayList<>();
+
+ InMemoryPhoneBook inMemoryPhoneBook = InMemoryPhoneBook.get();
+ for (PhoneCallLog phoneCallLog : phoneCallLogs) {
+ String number = phoneCallLog.getPhoneNumberString();
+ String relativeTime = getRelativeTime(phoneCallLog.getLastCallEndTimestamp());
+ if (TelecomUtils.isVoicemailNumber(mContext, number)) {
+ String title = mContext.getString(R.string.voicemail);
+ UiCallLog uiCallLog = new UiCallLog(title,
+ relativeTime, number, null, phoneCallLog.getAllCallRecords());
+ uiCallLogs.add(uiCallLog);
+ continue;
+ }
+
+ Contact contact = inMemoryPhoneBook.lookupContactEntry(number);
+ String title;
+ if (contact != null && contact.getDisplayName() != null) {
+ title = contact.getDisplayName();
+ } else if (!TextUtils.isEmpty(number)) {
+ title = TelecomUtils.getFormattedNumber(mContext, number);
+ } else {
+ title = mContext.getString(R.string.unknown);
+ }
+ PhoneNumber phoneNumber = contact != null ? contact.getPhoneNumber(number) : null;
+
+ UiCallLog uiCallLog = new UiCallLog(
+ title,
+ getSecondaryText(getType(phoneNumber), relativeTime),
+ number,
+ contact != null ? contact.getAvatarUri() : null,
+ phoneCallLog.getAllCallRecords());
+
+ uiCallLogs.add(uiCallLog);
+ }
+ return uiCallLogs;
+ }
+
+ private String getRelativeTime(long millis) {
+ boolean validTimestamp = millis > 0;
+
+ return validTimestamp ? DateUtils.getRelativeTimeSpanString(
+ millis, System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS,
+ DateUtils.FORMAT_ABBREV_RELATIVE).toString() : "";
+ }
+
+ private String getSecondaryText(@Nullable CharSequence type, String relativeTime) {
+ if (!TextUtils.isEmpty(type)) {
+ return Joiner.on(TYPE_AND_RELATIVE_TIME_JOINER).join(type, relativeTime);
+ } else {
+ return relativeTime;
+ }
+ }
+
+ private CharSequence getType(@Nullable PhoneNumber phoneNumber) {
+ return phoneNumber != null ? phoneNumber.getReadableLabel(mContext.getResources()) : "";
+ }
+}
diff --git a/src/com/android/car/dialer/ui/common/entity/UiCallLog.java b/src/com/android/car/dialer/ui/common/entity/UiCallLog.java
new file mode 100644
index 00000000..999d66e1
--- /dev/null
+++ b/src/com/android/car/dialer/ui/common/entity/UiCallLog.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.common.entity;
+
+import android.net.Uri;
+
+import com.android.car.dialer.livedata.CallHistoryLiveData;
+import com.android.car.telephony.common.PhoneCallLog;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Ui representation of a call log.
+ */
+public class UiCallLog {
+ private final String mTitle;
+ private final String mNumber;
+ private final Uri mAvatarUri;
+ private final List<PhoneCallLog.Record> mCallRecords;
+ private String mText;
+
+ public UiCallLog(String title, String text, String number, Uri avatarUri,
+ List<PhoneCallLog.Record> callRecords) {
+ mTitle = title;
+ mText = text;
+ mNumber = number;
+ mAvatarUri = avatarUri;
+ mCallRecords = new ArrayList<>(callRecords);
+ }
+
+ /**
+ * Returns the title of a call log item.
+ */
+ public String getTitle() {
+ return mTitle;
+ }
+
+ /**
+ * Returns the body text of a call log item.
+ */
+ public String getText() {
+ return mText;
+ }
+
+ /**
+ * Updates the body text.
+ */
+ public void setText(String text) {
+ mText = text;
+ }
+
+ /**
+ * Returns the number of this call log.
+ */
+ public String getNumber() {
+ return mNumber;
+ }
+
+ /** Returns the avatar of the contact associated with the number. */
+ public Uri getAvatarUri() {
+ return mAvatarUri;
+ }
+
+ /**
+ * Returns a copy of combined call log records. See {@link PhoneCallLog.Record} for more details
+ * of each record. Logs are sorted on call end time in decedent order.
+ */
+ public List<PhoneCallLog.Record> getCallRecords() {
+ return getCallRecords(mCallRecords.size());
+ }
+
+ /**
+ * Returns a copy of last N phone call records. Ordered by call end timestamp in descending
+ * order.
+ *
+ * <p>If {@code n} is greater than the number of {@link #getCallRecords() records}, return
+ * the entire record list.
+ */
+ public List<PhoneCallLog.Record> getCallRecords(int n) {
+ int toIndex = Math.max(0, n);
+ toIndex = Math.min(toIndex, mCallRecords.size());
+ if (toIndex == 0) {
+ return Collections.emptyList();
+ } else {
+ return new ArrayList<>(mCallRecords.subList(0, toIndex));
+ }
+ }
+
+ /**
+ * Returns the most recent call end timestamp of this log in milliseconds since the epoch.
+ */
+ public long getMostRecentCallEndTimestamp() {
+ return mCallRecords.isEmpty() ? 0
+ : mCallRecords.get(0).getCallEndTimestamp();
+ }
+
+ /**
+ * Returns the most recent call's call type.
+ */
+ public int getMostRecentCallType() {
+ return mCallRecords.isEmpty() ? CallHistoryLiveData.CallType.CALL_TYPE_ALL
+ : mCallRecords.get(0).getCallType();
+ }
+}
diff --git a/src/com/android/car/dialer/ui/contact/ContactDefaultNumberActionProvider.java b/src/com/android/car/dialer/ui/contact/ContactDefaultNumberActionProvider.java
new file mode 100644
index 00000000..d56c70ca
--- /dev/null
+++ b/src/com/android/car/dialer/ui/contact/ContactDefaultNumberActionProvider.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.contact;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.view.ActionProvider;
+import android.view.View;
+import android.widget.Button;
+
+import com.android.car.dialer.R;
+import com.android.car.dialer.ui.common.PhoneNumberListAdapter;
+import com.android.car.telephony.common.Contact;
+import com.android.car.telephony.common.PhoneNumber;
+import com.android.car.telephony.common.TelecomUtils;
+
+import java.util.List;
+
+/** {@link ActionProvider} for setting contact default number menu in contact details page. */
+public class ContactDefaultNumberActionProvider extends ActionProvider {
+ private final Context mContext;
+ private Contact mContact;
+ private Button mPositiveButton;
+ private PhoneNumber mSelectedPhoneNumber;
+
+ public ContactDefaultNumberActionProvider(Context context) {
+ super(context);
+ mContext = context;
+ }
+
+ public void setContact(Contact contact) {
+ mContact = contact;
+ refreshVisibility();
+ }
+
+ @Override
+ @Deprecated
+ public View onCreateActionView() {
+ return null;
+ }
+
+ @Override
+ public boolean onPerformDefaultAction() {
+ mSelectedPhoneNumber = null;
+
+ List<PhoneNumber> contactPhoneNumbers = mContact.getNumbers();
+ int primaryPhoneNumberIndex =
+ mContact.hasPrimaryPhoneNumber() ? contactPhoneNumbers.indexOf(
+ mContact.getPrimaryPhoneNumber()) : -1;
+ AlertDialog alertDialog = new AlertDialog.Builder(mContext)
+ .setTitle(R.string.set_default_number)
+ .setSingleChoiceItems(
+ new PhoneNumberListAdapter(mContext, contactPhoneNumbers),
+ primaryPhoneNumberIndex,
+ ((dialog, which) -> {
+ mSelectedPhoneNumber = contactPhoneNumbers.get(which);
+ mPositiveButton.setEnabled(
+ which != primaryPhoneNumberIndex);
+ }))
+ .setNegativeButton(android.R.string.cancel, null)
+ .setPositiveButton(android.R.string.ok,
+ (dialog, which) ->
+ TelecomUtils.setAsPrimaryPhoneNumber(mContext,
+ mSelectedPhoneNumber))
+ .show();
+ mPositiveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE);
+ mPositiveButton.setEnabled(false);
+ return true;
+ }
+
+ @Override
+ public boolean overridesItemVisibility() {
+ return true;
+ }
+
+ /** It will be visible when the contact has multiple numbers. */
+ @Override
+ public boolean isVisible() {
+ return mContact != null && mContact.getNumbers().size() > 1;
+ }
+}
diff --git a/src/com/android/car/dialer/ui/contact/ContactDetailsAdapter.java b/src/com/android/car/dialer/ui/contact/ContactDetailsAdapter.java
new file mode 100644
index 00000000..e1b0f6ed
--- /dev/null
+++ b/src/com/android/car/dialer/ui/contact/ContactDetailsAdapter.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.contact;
+
+import android.content.Context;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.car.dialer.R;
+import com.android.car.dialer.log.L;
+import com.android.car.dialer.ui.common.DialerUtils;
+import com.android.car.telephony.common.Contact;
+import com.android.car.telephony.common.PhoneNumber;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+
+class ContactDetailsAdapter extends RecyclerView.Adapter<ContactDetailsViewHolder> {
+
+ private static final String TAG = "CD.ContactDetailsAdapter";
+ @VisibleForTesting
+ static final String TELEPHONE_URI_PREFIX = "tel:";
+
+ private static final int ID_HEADER = 1;
+ private static final int ID_CONTENT = 2;
+
+ private final Context mContext;
+
+ private final ArrayList<Object> mItems = new ArrayList<Object>();
+
+ public ContactDetailsAdapter(@NonNull Context context, @Nullable Contact contact) {
+ super();
+ mContext = context;
+ setContact(contact);
+ }
+
+ void setContact(Contact contact) {
+ L.d(TAG, "setContact %s", contact);
+ mItems.clear();
+ if (shouldShowHeader()) {
+ mItems.add(contact);
+ }
+ if (contact != null) {
+ mItems.addAll(contact.getNumbers());
+ }
+ notifyDataSetChanged();
+ }
+
+ private boolean shouldShowHeader() {
+ return !DialerUtils.isShortScreen(mContext);
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ Object obj = mItems.get(position);
+ if (obj == null || obj instanceof Contact) {
+ return ID_HEADER;
+ } else {
+ return ID_CONTENT;
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ return mItems.size();
+ }
+
+ @Override
+ public ContactDetailsViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ int layoutResId;
+ switch (viewType) {
+ case ID_HEADER:
+ layoutResId = R.layout.contact_details_name_image;
+ break;
+ case ID_CONTENT:
+ layoutResId = R.layout.contact_details_number;
+ break;
+ default:
+ L.e(TAG, "Unknown view type: %d", viewType);
+ return null;
+ }
+
+ View view = LayoutInflater.from(parent.getContext()).inflate(layoutResId, parent,
+ false);
+ return new ContactDetailsViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(ContactDetailsViewHolder viewHolder, int position) {
+ switch (viewHolder.getItemViewType()) {
+ case ID_HEADER:
+ viewHolder.bind(mContext, (Contact) mItems.get(position));
+ break;
+ case ID_CONTENT:
+ viewHolder.bind(mContext, (PhoneNumber) mItems.get(position));
+ break;
+ default:
+ Log.e(TAG, "Unknown view type " + viewHolder.getItemViewType());
+ return;
+ }
+ }
+}
diff --git a/src/com/android/car/dialer/ui/contact/ContactDetailsFragment.java b/src/com/android/car/dialer/ui/contact/ContactDetailsFragment.java
new file mode 100644
index 00000000..2d6f25ac
--- /dev/null
+++ b/src/com/android/car/dialer/ui/contact/ContactDetailsFragment.java
@@ -0,0 +1,173 @@
+/*
+ * 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.car.dialer.ui.contact;
+
+import android.app.ActionBar;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.ViewModelProviders;
+
+import com.android.car.dialer.R;
+import com.android.car.dialer.ui.common.DialerListBaseFragment;
+import com.android.car.dialer.ui.common.DialerUtils;
+import com.android.car.dialer.ui.view.ContactAvatarOutputlineProvider;
+import com.android.car.telephony.common.Contact;
+import com.android.car.telephony.common.TelecomUtils;
+
+/**
+ * A fragment that shows the name of the contact, the photo and all listed phone numbers. It is
+ * primarily used to respond to the results of search queries but supplyig it with the content://
+ * uri of a contact should work too.
+ */
+public class ContactDetailsFragment extends DialerListBaseFragment {
+ private static final String TAG = "CD.ContactDetailsFragment";
+ public static final String FRAGMENT_TAG = "CONTACT_DETAIL_FRAGMENT_TAG";
+
+ // Key to load and save the contact entity instance.
+ private static final String KEY_CONTACT_ENTITY = "ContactEntity";
+
+ // Key to load the contact details by passing in the content provider query uri.
+ private static final String KEY_CONTACT_QUERY_URI = "ContactQueryUri";
+
+ private Contact mContact;
+ private Uri mContactLookupUri;
+ private LiveData<Contact> mContactDetailsLiveData;
+ private ImageView mAvatarView;
+ private TextView mNameView;
+
+ /** Creates a new ContactDetailsFragment using a URI to lookup a {@link Contact} at. */
+ public static ContactDetailsFragment newInstance(Uri uri) {
+ ContactDetailsFragment fragment = new ContactDetailsFragment();
+ Bundle args = new Bundle();
+ args.putParcelable(KEY_CONTACT_QUERY_URI, uri);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ /** Creates a new ContactDetailsFragment using a {@link Contact}. */
+ public static ContactDetailsFragment newInstance(Contact contact) {
+ ContactDetailsFragment fragment = new ContactDetailsFragment();
+ Bundle args = new Bundle();
+ args.putParcelable(KEY_CONTACT_ENTITY, contact);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setHasOptionsMenu(true);
+
+ mContact = getArguments().getParcelable(KEY_CONTACT_ENTITY);
+ mContactLookupUri = getArguments().getParcelable(KEY_CONTACT_QUERY_URI);
+ if (mContact == null && savedInstanceState != null) {
+ mContact = savedInstanceState.getParcelable(KEY_CONTACT_ENTITY);
+ }
+ if (mContact != null) {
+ mContactLookupUri = mContact.getLookupUri();
+ }
+ ContactDetailsViewModel contactDetailsViewModel = ViewModelProviders.of(this).get(
+ ContactDetailsViewModel.class);
+ mContactDetailsLiveData = contactDetailsViewModel.getContactDetails(mContactLookupUri);
+ mContactDetailsLiveData.observe(this, this::onContactChanged);
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) {
+ menuInflater.inflate(R.menu.contact_edit, menu);
+ MenuItem defaultNumberMenuItem = menu.findItem(R.id.menu_contact_default_number);
+ ContactDefaultNumberActionProvider contactDefaultNumberActionProvider =
+ (ContactDefaultNumberActionProvider) defaultNumberMenuItem.getActionProvider();
+ contactDefaultNumberActionProvider.setContact(mContact);
+ mContactDetailsLiveData.observe(this, contactDefaultNumberActionProvider::setContact);
+ }
+
+ @Override
+ public void onPrepareOptionsMenu(Menu menu) {
+ menu.findItem(R.id.menu_contacts_search).setVisible(false);
+ menu.findItem(R.id.menu_dialer_setting).setVisible(false);
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ ContactDetailsAdapter contactDetailsAdapter = new ContactDetailsAdapter(getContext(),
+ mContact);
+ getRecyclerView().setAdapter(contactDetailsAdapter);
+ mContactDetailsLiveData.observe(this, contactDetailsAdapter::setContact);
+ }
+
+ private void onContactChanged(Contact contact) {
+ getArguments().clear();
+
+ if (mAvatarView != null) {
+ mAvatarView.setOutlineProvider(ContactAvatarOutputlineProvider.get());
+ TelecomUtils.setContactBitmapAsync(getContext(), mAvatarView, contact, null);
+ }
+
+ if (mNameView != null) {
+ if (contact != null) {
+ mNameView.setText(contact.getDisplayName());
+ } else {
+ mNameView.setText(R.string.error_contact_deleted);
+ }
+ }
+ }
+
+ @Override
+ protected void setupActionBar(@NonNull ActionBar actionBar) {
+ actionBar.setCustomView(R.layout.contact_details_action_bar);
+ actionBar.setTitle(null);
+
+ // Will set these to null on screen sizes that don't have them in the action bar
+ View customView = actionBar.getCustomView();
+ mAvatarView = customView.findViewById(R.id.contact_details_action_bar_avatar);
+ mNameView = customView.findViewById(R.id.contact_details_action_bar_name);
+
+ // Remove the action bar background on non-short screens
+ // On short screens the avatar and name is in the action bar so we keep it
+ if (mAvatarView == null) {
+ setActionBarBackground(null);
+ getRecyclerView().setScrollBarPadding(actionBar.getHeight(), 0);
+ } else {
+ getRecyclerView().setScrollBarPadding(0, 0);
+ }
+ }
+
+ @Override
+ protected int getTopOffset() {
+ if (DialerUtils.isShortScreen(getContext())) {
+ return super.getTopOffset();
+ } else {
+ return 0;
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putParcelable(KEY_CONTACT_ENTITY, mContactDetailsLiveData.getValue());
+ }
+}
diff --git a/src/com/android/car/dialer/ui/contact/ContactDetailsViewHolder.java b/src/com/android/car/dialer/ui/contact/ContactDetailsViewHolder.java
new file mode 100644
index 00000000..6f1eb35a
--- /dev/null
+++ b/src/com/android/car/dialer/ui/contact/ContactDetailsViewHolder.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.contact;
+
+import android.content.Context;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.car.dialer.R;
+import com.android.car.dialer.telecom.UiCallManager;
+import com.android.car.dialer.ui.view.ContactAvatarOutputlineProvider;
+import com.android.car.telephony.common.Contact;
+import com.android.car.telephony.common.PhoneNumber;
+import com.android.car.telephony.common.TelecomUtils;
+
+/** ViewHolder for {@link ContactDetailsFragment}. */
+class ContactDetailsViewHolder extends RecyclerView.ViewHolder {
+ // Applies to all
+ @NonNull
+ private final TextView mTitle;
+
+ // Applies to header
+ @Nullable
+ private final ImageView mAvatar;
+
+ // Applies to phone number items
+ @Nullable
+ private final TextView mText;
+ @Nullable
+ private final View mCallActionView;
+ @Nullable
+ private final View mFavoriteActionView;
+
+ ContactDetailsViewHolder(View v) {
+ super(v);
+ mCallActionView = v.findViewById(R.id.call_action_id);
+ mFavoriteActionView = v.findViewById(R.id.contact_details_favorite_button);
+ mTitle = v.findViewById(R.id.title);
+ mText = v.findViewById(R.id.text);
+ mAvatar = v.findViewById(R.id.avatar);
+ if (mAvatar != null) {
+ mAvatar.setOutlineProvider(ContactAvatarOutputlineProvider.get());
+ }
+ }
+
+ public void bind(Context context, Contact contact) {
+ TelecomUtils.setContactBitmapAsync(context, mAvatar, contact, null);
+
+ if (contact == null) {
+ mTitle.setText(R.string.error_contact_deleted);
+ return;
+ }
+
+ mTitle.setText(contact.getDisplayName());
+ }
+
+ public void bind(Context context, PhoneNumber phoneNumber) {
+
+ mTitle.setText(phoneNumber.getRawNumber());
+
+ // Present the phone number type.
+ CharSequence readableLabel = phoneNumber.getReadableLabel(context.getResources());
+ if (phoneNumber.isPrimary()) {
+ mText.setText(context.getString(R.string.primary_number_description, readableLabel));
+ } else {
+ mText.setText(readableLabel);
+ }
+
+ mCallActionView.setOnClickListener(v -> placeCall(phoneNumber));
+ mFavoriteActionView.setOnClickListener(v -> {
+ Toast.makeText(context, "Not yet implemented", Toast.LENGTH_LONG).show();
+ mFavoriteActionView.setActivated(!mFavoriteActionView.isActivated());
+ });
+ }
+
+ private void placeCall(PhoneNumber number) {
+ UiCallManager.get().placeCall(number.getRawNumber());
+ }
+}
diff --git a/src/com/android/car/dialer/ui/contact/ContactDetailsViewModel.java b/src/com/android/car/dialer/ui/contact/ContactDetailsViewModel.java
new file mode 100644
index 00000000..c48fda7a
--- /dev/null
+++ b/src/com/android/car/dialer/ui/contact/ContactDetailsViewModel.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.contact;
+
+import android.app.Application;
+import android.net.Uri;
+import android.provider.ContactsContract;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+
+import com.android.car.dialer.livedata.ContactDetailsLiveData;
+import com.android.car.telephony.common.Contact;
+
+/** View model for the contact details page. */
+public class ContactDetailsViewModel extends AndroidViewModel {
+
+ public ContactDetailsViewModel(@NonNull Application application) {
+ super(application);
+ }
+
+ /**
+ * Builds the {@link LiveData} for the contact entity described by the given look up uri.
+ *
+ * @param contactLookupUri An {@link ContactsContract.Contacts#CONTENT_LOOKUP_URI} describing
+ * the contact entry. It might have been out of date and whoever use it
+ * should attempt to refresh first. A null contactLookupUri means the
+ * contact entry has been deleted.
+ */
+ public LiveData<Contact> getContactDetails(@Nullable Uri contactLookupUri) {
+ if (contactLookupUri == null) {
+ MutableLiveData<Contact> deletedContactDetailsLiveData = new MutableLiveData<>();
+ deletedContactDetailsLiveData.setValue(null);
+ return deletedContactDetailsLiveData;
+ }
+
+ return new ContactDetailsLiveData(getApplication(), contactLookupUri);
+ }
+}
diff --git a/src/com/android/car/dialer/ui/contact/ContactListAdapter.java b/src/com/android/car/dialer/ui/contact/ContactListAdapter.java
new file mode 100644
index 00000000..9c9be76e
--- /dev/null
+++ b/src/com/android/car/dialer/ui/contact/ContactListAdapter.java
@@ -0,0 +1,77 @@
+
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.dialer.ui.contact;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.car.dialer.R;
+import com.android.car.telephony.common.Contact;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** Adapter for contact list. */
+public class ContactListAdapter extends RecyclerView.Adapter<ContactListViewHolder> {
+ private static final String TAG = "CD.ContactListAdapter";
+
+ interface OnShowContactDetailListener {
+ void onShowContactDetail(Contact contact);
+ }
+
+ private final Context mContext;
+ private final List<Contact> mContactList = new ArrayList<>();
+ private final OnShowContactDetailListener mOnShowContactDetailListener;
+
+ public ContactListAdapter(Context context,
+ OnShowContactDetailListener onShowContactDetailListener) {
+ mContext = context;
+ mOnShowContactDetailListener = onShowContactDetailListener;
+ }
+
+ public void setContactList(List<Contact> contactList) {
+ mContactList.clear();
+ if (contactList != null) {
+ mContactList.addAll(contactList);
+ }
+ notifyDataSetChanged();
+ }
+
+ @NonNull
+ @Override
+ public ContactListViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ View itemView = LayoutInflater.from(mContext).inflate(R.layout.contact_list_item, parent,
+ false);
+ return new ContactListViewHolder(itemView, mOnShowContactDetailListener);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull ContactListViewHolder holder, int position) {
+ Contact contact = mContactList.get(position);
+ holder.onBind(contact);
+ }
+
+ @Override
+ public int getItemCount() {
+ return mContactList.size();
+ }
+}
diff --git a/src/com/android/car/dialer/ui/contact/ContactListFragment.java b/src/com/android/car/dialer/ui/contact/ContactListFragment.java
new file mode 100644
index 00000000..05155bb2
--- /dev/null
+++ b/src/com/android/car/dialer/ui/contact/ContactListFragment.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.contact;
+
+import android.os.Bundle;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.ViewModelProviders;
+
+import com.android.car.dialer.ui.common.DialerListBaseFragment;
+import com.android.car.telephony.common.Contact;
+
+/**
+ * Contact Fragment.
+ */
+public class ContactListFragment extends DialerListBaseFragment implements
+ ContactListAdapter.OnShowContactDetailListener {
+ private ContactListAdapter mContactListAdapter;
+
+ public static ContactListFragment newInstance() {
+ return new ContactListFragment();
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ mContactListAdapter = new ContactListAdapter(
+ getContext(), /* onShowContactDetailListener= */this);
+ getRecyclerView().setAdapter(mContactListAdapter);
+
+ ContactListViewModel contactListViewModel = ViewModelProviders.of(this).get(
+ ContactListViewModel.class);
+ contactListViewModel.getAllContacts().observe(this, mContactListAdapter::setContactList);
+ }
+
+ @Override
+ public void onShowContactDetail(Contact contact) {
+ Fragment contactDetailsFragment = ContactDetailsFragment.newInstance(contact);
+ pushContentFragment(contactDetailsFragment, ContactDetailsFragment.FRAGMENT_TAG);
+ }
+}
diff --git a/src/com/android/car/dialer/ui/contact/ContactListViewHolder.java b/src/com/android/car/dialer/ui/contact/ContactListViewHolder.java
new file mode 100644
index 00000000..701f5ec1
--- /dev/null
+++ b/src/com/android/car/dialer/ui/contact/ContactListViewHolder.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.contact;
+
+import android.content.Context;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.car.dialer.R;
+import com.android.car.dialer.telecom.UiCallManager;
+import com.android.car.dialer.ui.common.DialerUtils;
+import com.android.car.dialer.ui.view.ContactAvatarOutputlineProvider;
+import com.android.car.telephony.common.Contact;
+import com.android.car.telephony.common.PhoneNumber;
+import com.android.car.telephony.common.TelecomUtils;
+
+import java.util.List;
+
+/**
+ * {@link RecyclerView.ViewHolder} for contact list item, responsible for presenting and resetting
+ * the UI on recycle.
+ */
+public class ContactListViewHolder extends RecyclerView.ViewHolder {
+ private final ContactListAdapter.OnShowContactDetailListener mOnShowContactDetailListener;
+ private final ImageView mAvatarView;
+ private final TextView mTitleView;
+ private final TextView mTextView;
+ private final View mShowContactDetailView;
+ private final View mCallActionView;
+
+ public ContactListViewHolder(@NonNull View itemView,
+ ContactListAdapter.OnShowContactDetailListener onShowContactDetailListener) {
+ super(itemView);
+ mOnShowContactDetailListener = onShowContactDetailListener;
+ mAvatarView = itemView.findViewById(R.id.icon);
+ mAvatarView.setOutlineProvider(ContactAvatarOutputlineProvider.get());
+ mTitleView = itemView.findViewById(R.id.title);
+ mTextView = itemView.findViewById(R.id.text);
+ mShowContactDetailView = itemView.findViewById(R.id.show_contact_detail_id);
+ mCallActionView = itemView.findViewById(R.id.call_action_id);
+ }
+
+ public void onBind(Contact contact) {
+ TelecomUtils.setContactBitmapAsync(mAvatarView.getContext(), mAvatarView, contact, null);
+ mTitleView.setText(contact.getDisplayName());
+ setLabelText(contact);
+ mShowContactDetailView.setOnClickListener(
+ view -> mOnShowContactDetailListener.onShowContactDetail(contact));
+ mCallActionView.setOnClickListener(view -> {
+ DialerUtils.promptForPrimaryNumber(itemView.getContext(), contact,
+ (phoneNumber, always) -> UiCallManager.get().placeCall(
+ phoneNumber.getRawNumber()));
+ });
+ }
+
+ private void setLabelText(Contact contact) {
+ if (mTextView == null) {
+ return;
+ }
+
+ Context context = itemView.getContext();
+ CharSequence readableLabel = "";
+ List<PhoneNumber> numberList = contact.getNumbers();
+
+ if (numberList.size() == 1) {
+ readableLabel = numberList.get(0).getReadableLabel(context.getResources());
+ } else if (numberList.size() > 1) {
+ readableLabel = contact.hasPrimaryPhoneNumber()
+ ? context.getString(R.string.primary_number_description,
+ contact.getPrimaryPhoneNumber().getReadableLabel(context.getResources()))
+ : context.getString(R.string.type_multiple);
+ }
+
+ mTextView.setText(readableLabel);
+ }
+}
diff --git a/src/com/android/car/dialer/ui/contact/ContactListViewModel.java b/src/com/android/car/dialer/ui/contact/ContactListViewModel.java
new file mode 100644
index 00000000..d1938313
--- /dev/null
+++ b/src/com/android/car/dialer/ui/contact/ContactListViewModel.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.contact;
+
+import android.app.Application;
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MediatorLiveData;
+
+import com.android.car.dialer.R;
+import com.android.car.dialer.livedata.SharedPreferencesLiveData;
+import com.android.car.dialer.widget.WorkerExecutor;
+import com.android.car.telephony.common.Contact;
+import com.android.car.telephony.common.InMemoryPhoneBook;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.concurrent.Future;
+
+/**
+ * View model for {@link ContactListFragment}.
+ */
+public class ContactListViewModel extends AndroidViewModel {
+
+ private final Context mContext;
+ private final LiveData<List<Contact>> mSortedContactListLiveData;
+
+ public ContactListViewModel(@NonNull Application application) {
+ super(application);
+ mContext = application.getApplicationContext();
+
+ String key = mContext.getString(R.string.sort_order_key);
+ SharedPreferencesLiveData preferencesLiveData =
+ new SharedPreferencesLiveData(mContext, key);
+ LiveData<List<Contact>> contactListLiveData = InMemoryPhoneBook.get().getContactsLiveData();
+ mSortedContactListLiveData = new SortedContactListLiveData(
+ mContext, contactListLiveData, preferencesLiveData);
+ }
+
+ /**
+ * Returns a live data which represents a list of all contacts.
+ */
+ public LiveData<List<Contact>> getAllContacts() {
+ return mSortedContactListLiveData;
+ }
+
+ private static class SortedContactListLiveData extends MediatorLiveData<List<Contact>> {
+
+ private final LiveData<List<Contact>> mContactListLiveData;
+ private final SharedPreferencesLiveData mPreferencesLiveData;
+ private final Context mContext;
+
+ private Future<?> mRunnableFuture;
+
+ /**
+ * Sort by the default display order of a name. For western names it will be "Given Family".
+ * For unstructured names like east asian this will be the only order.
+ * Phone Dialer uses the same method for sorting given names.
+ *
+ * @see android.provider.ContactsContract.Contacts#DISPLAY_NAME_PRIMARY
+ */
+ private final Comparator<Contact> mFirstNameComparator =
+ (o1, o2) -> o1.compareByDisplayName(o2);
+
+ /**
+ * Sort by the alternative display order of a name. For western names it will be "Family,
+ * Given". For unstructured names like east asian this order will be ignored and treated as
+ * primary.
+ * Phone Dialer uses the same method for sorting family names.
+ *
+ * @see android.provider.ContactsContract.Contacts#DISPLAY_NAME_ALTERNATIVE
+ */
+ private final Comparator<Contact> mLastNameComparator =
+ (o1, o2) -> o1.compareByAltDisplayName(o2);
+
+ private SortedContactListLiveData(Context context,
+ @NonNull LiveData<List<Contact>> contactListLiveData,
+ @NonNull SharedPreferencesLiveData sharedPreferencesLiveData) {
+ mContext = context;
+ mContactListLiveData = contactListLiveData;
+ mPreferencesLiveData = sharedPreferencesLiveData;
+
+ addSource(mPreferencesLiveData, (trigger) -> updateSortedContactList());
+ addSource(mContactListLiveData, (trigger) -> updateSortedContactList());
+ }
+
+ private void updateSortedContactList() {
+ if (mContactListLiveData.getValue() == null) {
+ setValue(null);
+ return;
+ }
+
+ String key = mPreferencesLiveData.getKey();
+ String defaultValue = mContext.getResources().getStringArray(
+ R.array.contact_order_entry_values)[0];
+
+ List<Contact> contactList = mContactListLiveData.getValue();
+ Comparator<Contact> comparator;
+ if (mPreferencesLiveData.getValue() == null
+ || mPreferencesLiveData.getValue().getString(key, defaultValue)
+ .equals(defaultValue)) {
+ comparator = mFirstNameComparator;
+ } else {
+ comparator = mLastNameComparator;
+ }
+
+ // SingleThreadPoolExecutor is used here to avoid multiple threads sorting the list
+ // at the same time.
+ if (mRunnableFuture != null) {
+ mRunnableFuture.cancel(true);
+ }
+
+ Runnable runnable = () -> {
+ Collections.sort(contactList, comparator);
+ postValue(contactList);
+ };
+ mRunnableFuture = WorkerExecutor.getInstance().getSingleThreadExecutor().submit(
+ runnable);
+ }
+
+ @Override
+ protected void onInactive() {
+ super.onInactive();
+ if (mRunnableFuture != null) {
+ mRunnableFuture.cancel(true);
+ }
+ }
+ }
+}
diff --git a/src/com/android/car/dialer/ui/dialpad/AbstractDialpadFragment.java b/src/com/android/car/dialer/ui/dialpad/AbstractDialpadFragment.java
new file mode 100644
index 00000000..2333414b
--- /dev/null
+++ b/src/com/android/car/dialer/ui/dialpad/AbstractDialpadFragment.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.dialpad;
+
+import android.animation.AnimatorInflater;
+import android.animation.ValueAnimator;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.text.SpannableString;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.util.SparseArray;
+import android.view.KeyEvent;
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.annotation.CallSuper;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.car.dialer.R;
+import com.android.car.dialer.log.L;
+import com.android.car.dialer.ui.common.DialerBaseFragment;
+import com.android.car.dialer.ui.view.ScaleSpan;
+
+/** Fragment that controls the dialpad. */
+public abstract class AbstractDialpadFragment extends DialerBaseFragment implements
+ KeypadFragment.KeypadCallback {
+ private static final String TAG = "CD.AbsDialpadFragment";
+ private static final String DIAL_NUMBER_KEY = "DIAL_NUMBER_KEY";
+ private static final int PLAY_DTMF_TONE = 1;
+
+ static final SparseArray<Character> sDialValueMap = new SparseArray<>();
+
+ static {
+ sDialValueMap.put(KeyEvent.KEYCODE_1, '1');
+ sDialValueMap.put(KeyEvent.KEYCODE_2, '2');
+ sDialValueMap.put(KeyEvent.KEYCODE_3, '3');
+ sDialValueMap.put(KeyEvent.KEYCODE_4, '4');
+ sDialValueMap.put(KeyEvent.KEYCODE_5, '5');
+ sDialValueMap.put(KeyEvent.KEYCODE_6, '6');
+ sDialValueMap.put(KeyEvent.KEYCODE_7, '7');
+ sDialValueMap.put(KeyEvent.KEYCODE_8, '8');
+ sDialValueMap.put(KeyEvent.KEYCODE_9, '9');
+ sDialValueMap.put(KeyEvent.KEYCODE_0, '0');
+ sDialValueMap.put(KeyEvent.KEYCODE_STAR, '*');
+ sDialValueMap.put(KeyEvent.KEYCODE_POUND, '#');
+ }
+
+ private boolean mDTMFToneEnabled;
+ private final StringBuffer mNumber = new StringBuffer();
+ private ValueAnimator mInputMotionAnimator;
+ private ScaleSpan mScaleSpan;
+ private TextView mTitleView;
+ private int mCurrentlyPlayingTone = KeyEvent.KEYCODE_UNKNOWN;
+
+ /** Defines how the dialed number should be presented. */
+ abstract void presentDialedNumber(@NonNull StringBuffer number);
+
+ /** Plays the tone for the pressed keycode when "play DTMF tone" is enabled in settings. */
+ abstract void playTone(int keycode);
+
+ /** Stops playing all tones when "play DTMF tone" is enabled in settings. */
+ abstract void stopAllTones();
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (savedInstanceState != null) {
+ mNumber.append(savedInstanceState.getCharSequence(DIAL_NUMBER_KEY));
+ }
+ L.d(TAG, "onCreate, number: %s", mNumber);
+ }
+
+ @CallSuper
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ mTitleView = view.findViewById(R.id.title);
+ if (mTitleView != null && getResources().getBoolean(R.bool.config_enable_dial_motion)) {
+ mInputMotionAnimator = (ValueAnimator) AnimatorInflater.loadAnimator(getContext(),
+ R.animator.scale_down);
+ float startTextSize = mTitleView.getTextSize() * getResources().getFloat(
+ R.integer.config_dial_motion_scale_start);
+ mScaleSpan = new ScaleSpan(startTextSize);
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mDTMFToneEnabled = Settings.System.getInt(getContext().getContentResolver(),
+ Settings.System.DTMF_TONE_WHEN_DIALING, 1) == PLAY_DTMF_TONE;
+ L.d(TAG, "DTMF tone enabled = %s", String.valueOf(mDTMFToneEnabled));
+
+ presentDialedNumber();
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ stopAllTones();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putCharSequence(DIAL_NUMBER_KEY, mNumber);
+ }
+
+ @Override
+ public void onKeypadKeyDown(@KeypadFragment.DialKeyCode int keycode) {
+ String digit = sDialValueMap.get(keycode).toString();
+ appendDialedNumber(digit);
+
+ if (mDTMFToneEnabled) {
+ mCurrentlyPlayingTone = keycode;
+ playTone(keycode);
+ }
+ }
+
+ @Override
+ public void onKeypadKeyUp(@KeypadFragment.DialKeyCode int keycode) {
+ if (mDTMFToneEnabled && keycode == mCurrentlyPlayingTone) {
+ mCurrentlyPlayingTone = KeyEvent.KEYCODE_UNKNOWN;
+ stopAllTones();
+ }
+ }
+
+ /** Set the dialed number to the given number. Must be called after the fragment is added. */
+ public void setDialedNumber(String number) {
+ mNumber.setLength(0);
+ if (!TextUtils.isEmpty(number)) {
+ mNumber.append(number);
+ }
+ presentDialedNumber();
+ }
+
+ void clearDialedNumber() {
+ mNumber.setLength(0);
+ presentDialedNumber();
+ }
+
+ void removeLastDigit() {
+ if (mNumber.length() != 0) {
+ mNumber.deleteCharAt(mNumber.length() - 1);
+ }
+ presentDialedNumber();
+ }
+
+ void appendDialedNumber(String number) {
+ mNumber.append(number);
+ presentDialedNumber();
+
+ if (TextUtils.isEmpty(number)) {
+ return;
+ }
+
+ if (mInputMotionAnimator != null) {
+ final String currentText = mTitleView.getText().toString();
+ final SpannableString spannableString = new SpannableString(currentText);
+ mInputMotionAnimator.addUpdateListener(valueAnimator -> {
+ float textSize =
+ (float) valueAnimator.getAnimatedValue() * mTitleView.getTextSize();
+ mScaleSpan.setTextSize(textSize);
+ spannableString.setSpan(mScaleSpan, currentText.length() - number.length(),
+ currentText.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+ mTitleView.setText(spannableString, TextView.BufferType.SPANNABLE);
+ });
+ mInputMotionAnimator.start();
+ }
+ }
+
+ private void presentDialedNumber() {
+ if (mInputMotionAnimator != null) {
+ mInputMotionAnimator.cancel();
+ mInputMotionAnimator.removeAllUpdateListeners();
+ }
+
+ presentDialedNumber(mNumber);
+ }
+
+ @NonNull
+ StringBuffer getNumber() {
+ return mNumber;
+ }
+}
diff --git a/src/com/android/car/dialer/ui/dialpad/DialpadFragment.java b/src/com/android/car/dialer/ui/dialpad/DialpadFragment.java
new file mode 100644
index 00000000..1adcc5ee
--- /dev/null
+++ b/src/com/android/car/dialer/ui/dialpad/DialpadFragment.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.dialpad;
+
+import android.app.ActionBar;
+import android.media.AudioManager;
+import android.media.ToneGenerator;
+import android.os.Bundle;
+import android.provider.CallLog;
+import android.text.TextUtils;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageButton;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.car.apps.common.util.ViewUtils;
+import com.android.car.dialer.R;
+import com.android.car.dialer.log.L;
+import com.android.car.dialer.telecom.UiCallManager;
+import com.android.car.telephony.common.Contact;
+import com.android.car.telephony.common.InMemoryPhoneBook;
+import com.android.car.telephony.common.TelecomUtils;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableMap;
+
+/** Fragment that controls the dialpad. */
+public class DialpadFragment extends AbstractDialpadFragment {
+ private static final String TAG = "CD.DialpadFragment";
+
+ private static final String DIALPAD_MODE_KEY = "DIALPAD_MODE_KEY";
+ private static final int MODE_DIAL = 1;
+ private static final int MODE_EMERGENCY = 2;
+
+ @VisibleForTesting
+ static final int MAX_DIAL_NUMBER = 20;
+
+ private static final int TONE_RELATIVE_VOLUME = 80;
+ private static final int TONE_LENGTH_INFINITE = -1;
+ private final ImmutableMap<Integer, Integer> mToneMap =
+ ImmutableMap.<Integer, Integer>builder()
+ .put(KeyEvent.KEYCODE_1, ToneGenerator.TONE_DTMF_1)
+ .put(KeyEvent.KEYCODE_2, ToneGenerator.TONE_DTMF_2)
+ .put(KeyEvent.KEYCODE_3, ToneGenerator.TONE_DTMF_3)
+ .put(KeyEvent.KEYCODE_4, ToneGenerator.TONE_DTMF_4)
+ .put(KeyEvent.KEYCODE_5, ToneGenerator.TONE_DTMF_5)
+ .put(KeyEvent.KEYCODE_6, ToneGenerator.TONE_DTMF_6)
+ .put(KeyEvent.KEYCODE_7, ToneGenerator.TONE_DTMF_7)
+ .put(KeyEvent.KEYCODE_8, ToneGenerator.TONE_DTMF_8)
+ .put(KeyEvent.KEYCODE_9, ToneGenerator.TONE_DTMF_9)
+ .put(KeyEvent.KEYCODE_0, ToneGenerator.TONE_DTMF_0)
+ .put(KeyEvent.KEYCODE_STAR, ToneGenerator.TONE_DTMF_S)
+ .put(KeyEvent.KEYCODE_POUND, ToneGenerator.TONE_DTMF_P)
+ .build();
+
+ private TextView mTitleView;
+ private TextView mDisplayName;
+ private ImageButton mDeleteButton;
+ private int mMode;
+
+ private ToneGenerator mToneGenerator;
+
+ /**
+ * Creates a new instance of the {@link DialpadFragment} which is used for dialing a number.
+ */
+ public static DialpadFragment newPlaceCallDialpad() {
+ DialpadFragment fragment = newDialpad(MODE_DIAL);
+ return fragment;
+ }
+
+ /** Creates a new instance used for emergency dialing. */
+ public static DialpadFragment newEmergencyDialpad() {
+ return newDialpad(MODE_EMERGENCY);
+ }
+
+ private static DialpadFragment newDialpad(int mode) {
+ DialpadFragment fragment = new DialpadFragment();
+
+ Bundle args = new Bundle();
+ args.putInt(DIALPAD_MODE_KEY, mode);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mToneGenerator = new ToneGenerator(AudioManager.STREAM_MUSIC, TONE_RELATIVE_VOLUME);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ mMode = getArguments().getInt(DIALPAD_MODE_KEY);
+ L.d(TAG, "onCreateView mode: %s", mMode);
+
+ View rootView = inflater.inflate(R.layout.dialpad_fragment, container, false);
+ // Offset the dialpad to under the tabs in normal dial mode.
+ rootView.setPadding(0, getTopOffset(), 0, 0);
+
+ mTitleView = rootView.findViewById(R.id.title);
+ mTitleView.setTextAppearance(
+ mMode == MODE_EMERGENCY ? R.style.TextAppearance_EmergencyDialNumber
+ : R.style.TextAppearance_DialNumber);
+ mDisplayName = rootView.findViewById(R.id.display_name);
+
+ View callButton = rootView.findViewById(R.id.call_button);
+ callButton.setOnClickListener(v -> {
+ if (!TextUtils.isEmpty(getNumber().toString())) {
+ UiCallManager.get().placeCall(getNumber().toString());
+ // Update dialed number UI later in onResume() when in call intent is handled.
+ getNumber().setLength(0);
+ } else {
+ setDialedNumber(CallLog.Calls.getLastOutgoingCall(getContext()));
+ }
+ });
+
+ callButton.addOnUnhandledKeyEventListener((v, event) -> {
+ if (event.getKeyCode() == KeyEvent.KEYCODE_CALL) {
+ // Use onKeyDown/Up instead of performClick() because it animates the ripple
+ if (event.getAction() == KeyEvent.ACTION_DOWN) {
+ callButton.onKeyDown(KeyEvent.KEYCODE_ENTER, event);
+ } else if (event.getAction() == KeyEvent.ACTION_UP) {
+ callButton.onKeyUp(KeyEvent.KEYCODE_ENTER, event);
+ }
+ return true;
+ } else {
+ return false;
+ }
+ });
+
+ mDeleteButton = rootView.findViewById(R.id.delete_button);
+ mDeleteButton.setOnClickListener(v -> removeLastDigit());
+ mDeleteButton.setOnLongClickListener(v -> {
+ clearDialedNumber();
+ return true;
+ });
+
+ return rootView;
+ }
+
+ @Override
+ protected void setupActionBar(ActionBar actionBar) {
+ // Only setup the actionbar if we're in dial mode.
+ // In all the other modes, there will be another fragment in the activity
+ // at the same time, and we don't want to mess up it's action bar.
+ if (mMode == MODE_DIAL) {
+ super.setupActionBar(actionBar);
+ }
+ }
+
+ @Override
+ public void onKeypadKeyLongPressed(@KeypadFragment.DialKeyCode int keycode) {
+ switch (keycode) {
+ case KeyEvent.KEYCODE_0:
+ removeLastDigit();
+ appendDialedNumber("+");
+ break;
+ case KeyEvent.KEYCODE_STAR:
+ removeLastDigit();
+ appendDialedNumber(",");
+ break;
+ case KeyEvent.KEYCODE_1:
+ UiCallManager.get().callVoicemail();
+ break;
+ default:
+ break;
+ }
+ }
+
+ @Override
+ void playTone(int keycode) {
+ L.d(TAG, "start key pressed tone for %s", keycode);
+ mToneGenerator.startTone(mToneMap.get(keycode), TONE_LENGTH_INFINITE);
+ }
+
+ @Override
+ void stopAllTones() {
+ L.d(TAG, "stop key pressed tone");
+ mToneGenerator.stopTone();
+ }
+
+ @Override
+ void presentDialedNumber(@NonNull StringBuffer number) {
+ if (getActivity() == null) {
+ return;
+ }
+
+ if (number.length() == 0) {
+ mTitleView.setGravity(Gravity.CENTER);
+ mTitleView.setText(
+ mMode == MODE_DIAL ? R.string.dial_a_number
+ : R.string.emergency_call_description);
+ ViewUtils.setVisible(mDeleteButton, false);
+ } else {
+ mTitleView.setGravity(Gravity.END | Gravity.CENTER_VERTICAL);
+ if (number.length() <= MAX_DIAL_NUMBER) {
+ mTitleView.setText(
+ TelecomUtils.getFormattedNumber(getContext(), number.toString()));
+ } else {
+ mTitleView.setText(number.substring(number.length() - MAX_DIAL_NUMBER));
+ }
+ ViewUtils.setVisible(mDeleteButton, true);
+ }
+
+ presentContactName(number);
+ }
+
+ private void presentContactName(@NonNull StringBuffer number) {
+ Contact contact = InMemoryPhoneBook.get().lookupContactEntry(number.toString());
+ // OEM might remove the display name view.
+ ViewUtils.setText(mDisplayName, contact == null ? "" : contact.getDisplayName());
+ }
+
+ private int getTopOffset() {
+ if (mMode == MODE_DIAL) {
+ return getTopBarHeight();
+ }
+ return 0;
+ }
+}
diff --git a/src/com/android/car/dialer/ui/dialpad/InCallDialpadFragment.java b/src/com/android/car/dialer/ui/dialpad/InCallDialpadFragment.java
new file mode 100644
index 00000000..37868069
--- /dev/null
+++ b/src/com/android/car/dialer/ui/dialpad/InCallDialpadFragment.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.dialpad;
+
+import android.app.ActionBar;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.telecom.Call;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Chronometer;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.ViewModelProviders;
+
+import com.android.car.dialer.R;
+import com.android.car.dialer.log.L;
+import com.android.car.dialer.ui.activecall.InCallViewModel;
+import com.android.car.telephony.common.TelecomUtils;
+
+/** Dialpad fragment used in the ongoing call page. */
+public class InCallDialpadFragment extends AbstractDialpadFragment {
+ private static final String TAG = "CD.InCallDialpadFragment";
+
+ private TextView mTitleView;
+ private Chronometer mCallStateView;
+
+ /** An active call which this fragment is serving for. */
+ private LiveData<Call> mActiveCall;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+
+ ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.incall_dialpad_fragment,
+ container,
+ false);
+
+ mTitleView = rootView.findViewById(R.id.title);
+ mCallStateView = rootView.findViewById(R.id.call_state);
+
+ InCallViewModel viewModel = ViewModelProviders.of(getActivity()).get(InCallViewModel.class);
+ viewModel.getCallStateAndConnectTime().observe(this, (pair) -> {
+ if (pair == null) {
+ mCallStateView.stop();
+ mCallStateView.setText("");
+ return;
+ }
+ if (pair.first == Call.STATE_ACTIVE) {
+ mCallStateView.setBase(pair.second
+ - System.currentTimeMillis() + SystemClock.elapsedRealtime());
+ mCallStateView.start();
+ } else {
+ mCallStateView.stop();
+ mCallStateView.setText(TelecomUtils.callStateToUiString(
+ getContext(), pair.first));
+ }
+ });
+ mActiveCall = viewModel.getPrimaryCall();
+
+ return rootView;
+ }
+
+ @Override
+ void presentDialedNumber(@NonNull StringBuffer number) {
+ if (getActivity() == null) {
+ return;
+ }
+
+ if (number.length() == 0) {
+ mTitleView.setText("");
+ } else {
+ mTitleView.setText(number);
+ }
+ }
+
+ @Override
+ void playTone(int keycode) {
+ L.d(TAG, "start DTMF tone for %s", keycode);
+ if (mActiveCall.getValue() != null) {
+ mActiveCall.getValue().playDtmfTone(sDialValueMap.get(keycode));
+ }
+ }
+
+ @Override
+ void stopAllTones() {
+ if (mActiveCall.getValue() != null) {
+ L.d(TAG, "stop DTMF tone");
+ mActiveCall.getValue().stopDtmfTone();
+ }
+ }
+
+ @Override
+ protected void setupActionBar(ActionBar actionBar) {
+ // No-op
+ }
+
+ @Override
+ public void onKeypadKeyLongPressed(int keycode) {
+ // No-op
+ }
+}
diff --git a/src/com/android/car/dialer/DialpadButton.java b/src/com/android/car/dialer/ui/dialpad/KeypadButton.java
index 7e058c0b..686a6d62 100644
--- a/src/com/android/car/dialer/DialpadButton.java
+++ b/src/com/android/car/dialer/ui/dialpad/KeypadButton.java
@@ -13,7 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.car.dialer;
+
+package com.android.car.dialer.ui.dialpad;
import android.content.Context;
import android.content.res.TypedArray;
@@ -22,46 +23,48 @@ import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
+import com.android.car.dialer.R;
+
/**
- * A View that represents a single button on the dialpad. This View display a number above letters
+ * A View that represents a single button on the keypad. This View display a number above letters
* or an image.
*/
-public class DialpadButton extends FrameLayout {
+public class KeypadButton extends FrameLayout {
private static final int INVALID_IMAGE_RES = -1;
private String mNumberText;
private String mLetterText;
private int mImageRes = INVALID_IMAGE_RES;
- public DialpadButton(Context context) {
+ public KeypadButton(Context context) {
super(context);
init(context, null);
}
- public DialpadButton(Context context, AttributeSet attrs) {
+ public KeypadButton(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
- public DialpadButton(Context context, AttributeSet attrs, int defStyleAttrs) {
+ public KeypadButton(Context context, AttributeSet attrs, int defStyleAttrs) {
super(context, attrs, defStyleAttrs);
init(context, attrs);
}
- public DialpadButton(Context context, AttributeSet attrs, int defStyleAttrs, int defStyleRes) {
+ public KeypadButton(Context context, AttributeSet attrs, int defStyleAttrs, int defStyleRes) {
super(context, attrs, defStyleAttrs, defStyleRes);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
- inflate(context, R.layout.dialpad_button, this /* root */);
+ inflate(context, R.layout.keypad_button, this /* root */);
- TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.DialpadButton);
+ TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.KeypadButton);
try {
- mNumberText = ta.getString(R.styleable.DialpadButton_numberText);
- mLetterText = ta.getString(R.styleable.DialpadButton_letterText);
- mImageRes = ta.getResourceId(R.styleable.DialpadButton_image, INVALID_IMAGE_RES);
+ mNumberText = ta.getString(R.styleable.KeypadButton_numberText);
+ mLetterText = ta.getString(R.styleable.KeypadButton_letterText);
+ mImageRes = ta.getResourceId(R.styleable.KeypadButton_image, INVALID_IMAGE_RES);
} finally {
if (ta != null) {
ta.recycle();
@@ -76,19 +79,19 @@ public class DialpadButton extends FrameLayout {
// Using null check instead of a TextUtils.isEmpty() check so that an empty number/letter
// can be used to keep the positioning of non-empty numbers/letters consistent.
if (mNumberText != null) {
- TextView numberTextView = (TextView) findViewById(R.id.dialpad_number);
+ TextView numberTextView = (TextView) findViewById(R.id.keypad_number);
numberTextView.setText(mNumberText);
numberTextView.setVisibility(VISIBLE);
}
if (mLetterText != null) {
- TextView letterTextView = (TextView) findViewById(R.id.dialpad_letters);
+ TextView letterTextView = (TextView) findViewById(R.id.keypad_letters);
letterTextView.setText(mLetterText);
letterTextView.setVisibility(VISIBLE);
}
if (mImageRes != INVALID_IMAGE_RES) {
- ImageView imageView = (ImageView) findViewById(R.id.dialpad_image);
+ ImageView imageView = (ImageView) findViewById(R.id.keypad_image);
imageView.setImageResource(mImageRes);
imageView.setVisibility(VISIBLE);
}
diff --git a/src/com/android/car/dialer/ui/dialpad/KeypadFragment.java b/src/com/android/car/dialer/ui/dialpad/KeypadFragment.java
new file mode 100644
index 00000000..d21566b5
--- /dev/null
+++ b/src/com/android/car/dialer/ui/dialpad/KeypadFragment.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.dialpad;
+
+import android.os.Bundle;
+import android.util.SparseArray;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+
+import com.android.car.dialer.R;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Fragment which displays a pad of keys.
+ */
+public class KeypadFragment extends Fragment {
+ private static final SparseArray<Integer> sRIdMap = new SparseArray<>();
+
+ static {
+ sRIdMap.put(KeyEvent.KEYCODE_1, R.id.one);
+ sRIdMap.put(KeyEvent.KEYCODE_2, R.id.two);
+ sRIdMap.put(KeyEvent.KEYCODE_3, R.id.three);
+ sRIdMap.put(KeyEvent.KEYCODE_4, R.id.four);
+ sRIdMap.put(KeyEvent.KEYCODE_5, R.id.five);
+ sRIdMap.put(KeyEvent.KEYCODE_6, R.id.six);
+ sRIdMap.put(KeyEvent.KEYCODE_7, R.id.seven);
+ sRIdMap.put(KeyEvent.KEYCODE_8, R.id.eight);
+ sRIdMap.put(KeyEvent.KEYCODE_9, R.id.nine);
+ sRIdMap.put(KeyEvent.KEYCODE_0, R.id.zero);
+ sRIdMap.put(KeyEvent.KEYCODE_STAR, R.id.star);
+ sRIdMap.put(KeyEvent.KEYCODE_POUND, R.id.pound);
+ }
+
+ /** Valid keycodes that can be sent to the callback. **/
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({KeyEvent.KEYCODE_0, KeyEvent.KEYCODE_1, KeyEvent.KEYCODE_2, KeyEvent.KEYCODE_3,
+ KeyEvent.KEYCODE_4, KeyEvent.KEYCODE_5, KeyEvent.KEYCODE_6, KeyEvent.KEYCODE_7,
+ KeyEvent.KEYCODE_8, KeyEvent.KEYCODE_9, KeyEvent.KEYCODE_STAR, KeyEvent.KEYCODE_POUND})
+ @interface DialKeyCode {
+ }
+
+ /** Callback for keypad to interact with its host. */
+ public interface KeypadCallback {
+
+ /** Called when a key is long pressed. */
+ void onKeypadKeyLongPressed(@DialKeyCode int keycode);
+
+ /** Called when a key is pressed down. */
+ void onKeypadKeyDown(@DialKeyCode int keycode);
+
+ /** Called when a key is released. */
+ void onKeypadKeyUp(@DialKeyCode int keycode);
+ }
+
+
+ private KeypadCallback mKeypadCallback;
+
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ if (getParentFragment() instanceof KeypadCallback) {
+ mKeypadCallback = (KeypadCallback) getParentFragment();
+ } else if (getHost() instanceof KeypadCallback) {
+ mKeypadCallback = (KeypadCallback) getHost();
+ }
+
+ View keypadView = inflater.inflate(R.layout.keypad, container, false);
+ setupKeypadClickListeners(keypadView);
+ return keypadView;
+ }
+
+ /**
+ * The click listener for all keypad buttons. Reacts to touch-down and touch-up events, as
+ * well as long-press for certain keys. Mimics the behavior of the phone dialer app.
+ */
+ private class KeypadClickListener implements View.OnTouchListener,
+ View.OnLongClickListener, View.OnKeyListener, View.OnFocusChangeListener {
+ private final int mKeycode;
+ private boolean mIsKeyDown = false;
+
+ KeypadClickListener(@DialKeyCode int keyCode) {
+ mKeycode = keyCode;
+ }
+
+ @Override
+ public boolean onLongClick(View v) {
+ mKeypadCallback.onKeypadKeyLongPressed(mKeycode);
+ return true;
+ }
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ if (mKeypadCallback != null) {
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ mKeypadCallback.onKeypadKeyDown(mKeycode);
+ } else if (event.getAction() == MotionEvent.ACTION_UP) {
+ mKeypadCallback.onKeypadKeyUp(mKeycode);
+ }
+ }
+
+ // Continue propagating the event
+ return false;
+ }
+
+ @Override
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ if (mKeypadCallback != null && KeyEvent.isConfirmKey(keyCode)) {
+ if (event.getAction() == KeyEvent.ACTION_DOWN && !mIsKeyDown) {
+ mIsKeyDown = true;
+ mKeypadCallback.onKeypadKeyDown(mKeycode);
+ } else if (event.getAction() == KeyEvent.ACTION_UP && mIsKeyDown) {
+ mIsKeyDown = false;
+ mKeypadCallback.onKeypadKeyUp(mKeycode);
+ }
+ }
+
+ // Continue propagating the event
+ return false;
+ }
+
+ @Override
+ public void onFocusChange(View v, boolean hasFocus) {
+ if (!hasFocus && mIsKeyDown) {
+ mIsKeyDown = false;
+ mKeypadCallback.onKeypadKeyUp(mKeycode);
+ }
+ }
+ }
+
+ private void setupKeypadClickListeners(View parent) {
+ for (int i = 0; i < sRIdMap.size(); i++) {
+ int key = sRIdMap.keyAt(i);
+ KeypadClickListener clickListener = new KeypadClickListener(key);
+ View v = parent.findViewById(sRIdMap.get(key));
+ v.setOnTouchListener(clickListener);
+ v.setOnLongClickListener(clickListener);
+ v.setOnKeyListener(clickListener);
+ v.setOnFocusChangeListener(clickListener);
+ }
+ }
+}
diff --git a/src/com/android/car/dialer/ui/favorite/FavoriteAdapter.java b/src/com/android/car/dialer/ui/favorite/FavoriteAdapter.java
new file mode 100644
index 00000000..ae051217
--- /dev/null
+++ b/src/com/android/car/dialer/ui/favorite/FavoriteAdapter.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.favorite;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Toast;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.car.dialer.R;
+import com.android.car.dialer.log.L;
+import com.android.car.dialer.ui.common.OnItemClickedListener;
+import com.android.car.telephony.common.Contact;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Adapter class for binding favorite contacts.
+ */
+public class FavoriteAdapter extends RecyclerView.Adapter<FavoriteContactViewHolder> {
+ private static final String TAG = "CD.FavoriteAdapter";
+
+ private List<Contact> mFavoriteContacts = Collections.emptyList();
+ private OnItemClickedListener<Contact> mListener;
+
+ /** Sets the favorite contact list. */
+ public void setFavoriteContacts(List<Contact> favoriteContacts) {
+ L.d(TAG, "setFavoriteContacts %s", favoriteContacts);
+ mFavoriteContacts = (favoriteContacts != null) ? favoriteContacts : Collections.emptyList();
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public int getItemCount() {
+ return mFavoriteContacts.size() + 1; // +1 for the "Add a favorite" button
+ }
+
+ @Override
+ public FavoriteContactViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View view = LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.favorite_contact_list_item, parent, false);
+
+ return new FavoriteContactViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(FavoriteContactViewHolder viewHolder, int position) {
+ Context context = viewHolder.itemView.getContext();
+
+ if (position >= mFavoriteContacts.size()) {
+ viewHolder.onBindAddFavorite(context);
+ viewHolder.itemView.setOnClickListener((v) ->
+ Toast.makeText(context, "Not yet implemented", Toast.LENGTH_LONG).show());
+ } else {
+ Contact contact = mFavoriteContacts.get(position);
+ viewHolder.onBind(context, contact);
+ viewHolder.itemView.setOnClickListener((v) -> onItemViewClicked(contact));
+ }
+ }
+
+ private void onItemViewClicked(Contact contact) {
+ if (mListener != null) {
+ mListener.onItemClicked(contact);
+ }
+ }
+
+ /**
+ * Sets a {@link OnItemClickedListener listener} which will be called when an item is clicked.
+ */
+ public void setOnListItemClickedListener(OnItemClickedListener<Contact> listener) {
+ mListener = listener;
+ }
+}
diff --git a/src/com/android/car/dialer/ui/favorite/FavoriteContactViewHolder.java b/src/com/android/car/dialer/ui/favorite/FavoriteContactViewHolder.java
new file mode 100644
index 00000000..1853cc43
--- /dev/null
+++ b/src/com/android/car/dialer/ui/favorite/FavoriteContactViewHolder.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.favorite;
+
+import android.content.Context;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.car.dialer.R;
+import com.android.car.dialer.log.L;
+import com.android.car.dialer.ui.view.ContactAvatarOutputlineProvider;
+import com.android.car.telephony.common.Contact;
+import com.android.car.telephony.common.PhoneNumber;
+import com.android.car.telephony.common.TelecomUtils;
+
+import java.util.List;
+
+import javax.annotation.Nonnull;
+
+/**
+ * A {@link RecyclerView.ViewHolder ViewHolder} that will hold layouts for favorite contacts list
+ * items.
+ */
+class FavoriteContactViewHolder extends RecyclerView.ViewHolder {
+ private static final String TAG = "CD.FavoriteContactVH";
+
+ private final ImageView mIcon;
+ private final TextView mTitle;
+ private final TextView mText;
+
+ FavoriteContactViewHolder(View v) {
+ super(v);
+ mIcon = v.findViewById(R.id.icon);
+ mIcon.setOutlineProvider(ContactAvatarOutputlineProvider.get());
+ mTitle = v.findViewById(R.id.title);
+ mText = v.findViewById(R.id.text);
+ }
+
+ /**
+ * Binds view with favorite contact.
+ */
+ public void onBind(Context context, @Nonnull Contact contact) {
+ String displayName = contact.getDisplayName();
+ mTitle.setText(displayName);
+
+ List<PhoneNumber> contactPhoneNumbers = contact.getNumbers();
+ if (contactPhoneNumbers.isEmpty()) {
+ L.w(TAG, "contact %s doesn't have any phone number", contact.getDisplayName());
+ return;
+ }
+
+ String secondaryText;
+ if (!contact.isVoicemail() && contactPhoneNumbers.size() > 1) {
+ if (contact.hasPrimaryPhoneNumber()) {
+ secondaryText = context.getString(R.string.primary_number_description,
+ contact.getPrimaryPhoneNumber().getReadableLabel(context.getResources()));
+ } else {
+ secondaryText = context.getString(R.string.type_multiple);
+ }
+ } else {
+ secondaryText = String.valueOf(
+ contactPhoneNumbers.get(0).getReadableLabel(context.getResources()));
+ }
+ mText.setText(secondaryText);
+
+ TelecomUtils.setContactBitmapAsync(context, mIcon, contact, null);
+ }
+
+ /**
+ * Binds view as the "Add a favorite" button
+ */
+ public void onBindAddFavorite(Context context) {
+ mTitle.setText(R.string.add_favorite_button);
+ mText.setText(null);
+ mIcon.setImageDrawable(context.getDrawable(R.drawable.ic_add_favorite));
+ }
+}
diff --git a/src/com/android/car/dialer/ui/favorite/FavoriteFragment.java b/src/com/android/car/dialer/ui/favorite/FavoriteFragment.java
new file mode 100644
index 00000000..7e3573de
--- /dev/null
+++ b/src/com/android/car/dialer/ui/favorite/FavoriteFragment.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.favorite;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Toast;
+
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.ViewModelProviders;
+
+import com.android.car.dialer.R;
+import com.android.car.dialer.ui.common.DialerBaseFragment;
+
+/** Contains either the "You haven't added any favorites yet" screen, or FavoriteListFragment */
+public class FavoriteFragment extends DialerBaseFragment {
+
+ public static FavoriteFragment newInstance() {
+ return new FavoriteFragment();
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.favorite_fragment, container, false);
+ View emptyPage = view.findViewById(R.id.empty_page_container);
+ Fragment listFragment =
+ getChildFragmentManager().findFragmentById(R.id.favorite_list_fragment);
+
+ FavoriteViewModel favoriteViewModel = ViewModelProviders.of(getActivity()).get(
+ FavoriteViewModel.class);
+ favoriteViewModel.getFavoriteContacts().observe(this, contacts -> {
+ if (contacts == null || contacts.isEmpty()) {
+ emptyPage.setVisibility(View.VISIBLE);
+ getChildFragmentManager().beginTransaction()
+ .hide(listFragment)
+ .commit();
+ } else {
+ emptyPage.setVisibility(View.GONE);
+ getChildFragmentManager().beginTransaction()
+ .show(listFragment)
+ .commit();
+ }
+ });
+
+ emptyPage.findViewById(R.id.add_favorite_button).setOnClickListener(v ->
+ Toast.makeText(getContext(), "Not yet implemented", Toast.LENGTH_LONG).show());
+
+ return view;
+ }
+}
diff --git a/src/com/android/car/dialer/ui/favorite/FavoriteListFragment.java b/src/com/android/car/dialer/ui/favorite/FavoriteListFragment.java
new file mode 100644
index 00000000..41583727
--- /dev/null
+++ b/src/com/android/car/dialer/ui/favorite/FavoriteListFragment.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.favorite;
+
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.ViewModelProviders;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.car.dialer.R;
+import com.android.car.dialer.telecom.UiCallManager;
+import com.android.car.dialer.ui.common.DialerListBaseFragment;
+import com.android.car.dialer.ui.common.DialerUtils;
+import com.android.car.telephony.common.Contact;
+
+import java.util.List;
+
+/** Contains a list of favorite contacts. */
+public class FavoriteListFragment extends DialerListBaseFragment {
+
+ /** Constructs a new FavoriteListFragment */
+ public static FavoriteListFragment newInstance() {
+ return new FavoriteListFragment();
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ getRecyclerView().addItemDecoration(new ItemSpacingDecoration());
+ getRecyclerView().setItemAnimator(null);
+
+ FavoriteAdapter adapter = new FavoriteAdapter();
+
+ FavoriteViewModel favoriteViewModel = ViewModelProviders.of(getActivity()).get(
+ FavoriteViewModel.class);
+ LiveData<List<Contact>> favoriteContacts = favoriteViewModel.getFavoriteContacts();
+ adapter.setOnListItemClickedListener(this::onItemClicked);
+ favoriteContacts.observe(this, adapter::setFavoriteContacts);
+
+ getRecyclerView().setAdapter(adapter);
+ }
+
+ @NonNull
+ @Override
+ protected RecyclerView.LayoutManager createLayoutManager() {
+ int numOfColumn = getContext().getResources().getInteger(
+ R.integer.favorite_fragment_grid_column);
+ return new GridLayoutManager(getContext(), numOfColumn);
+ }
+
+ private void onItemClicked(Contact contact) {
+ DialerUtils.promptForPrimaryNumber(getContext(), contact, (phoneNumber, always) ->
+ UiCallManager.get().placeCall(phoneNumber.getRawNumber()));
+ }
+
+ private class ItemSpacingDecoration extends RecyclerView.ItemDecoration {
+
+ @Override
+ public void getItemOffsets(@NonNull Rect outRect, @NonNull View view,
+ @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
+ super.getItemOffsets(outRect, view, parent, state);
+ Resources resources = FavoriteListFragment.this.getContext().getResources();
+ int numColumns = resources.getInteger(R.integer.favorite_fragment_grid_column);
+ int leftPadding =
+ resources.getDimensionPixelOffset(R.dimen.favorite_card_space_horizontal);
+ int topPadding =
+ resources.getDimensionPixelOffset(R.dimen.favorite_card_space_vertical);
+
+ if (parent.getChildAdapterPosition(view) % numColumns == 0) {
+ leftPadding = 0;
+ }
+ if (parent.getChildAdapterPosition(view) < numColumns) {
+ topPadding = 0;
+ }
+
+ outRect.set(leftPadding, topPadding, 0, 0);
+ }
+ }
+}
diff --git a/src/com/android/car/dialer/ui/favorite/FavoriteViewModel.java b/src/com/android/car/dialer/ui/favorite/FavoriteViewModel.java
new file mode 100644
index 00000000..f58f56ff
--- /dev/null
+++ b/src/com/android/car/dialer/ui/favorite/FavoriteViewModel.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.favorite;
+
+import android.app.Application;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.LiveData;
+import com.android.car.dialer.livedata.FavoriteContactLiveData;
+import com.android.car.telephony.common.Contact;
+
+import java.util.List;
+
+/**
+ * View model for {@link FavoriteFragment}.
+ */
+public class FavoriteViewModel extends AndroidViewModel {
+ private LiveData<List<Contact>> mFavoriteContactsLiveData;
+
+ public FavoriteViewModel(Application application) {
+ super(application);
+ mFavoriteContactsLiveData = FavoriteContactLiveData.newInstance(application);
+ }
+
+ /** Returns favorite contact list live data. */
+ public LiveData<List<Contact>> getFavoriteContacts() {
+ return mFavoriteContactsLiveData;
+ }
+}
diff --git a/src/com/android/car/dialer/ui/listitem/CallLogListItem.java b/src/com/android/car/dialer/ui/listitem/CallLogListItem.java
deleted file mode 100644
index af8081b8..00000000
--- a/src/com/android/car/dialer/ui/listitem/CallLogListItem.java
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.car.dialer.ui.listitem;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
-import android.widget.ImageView;
-
-import com.android.car.apps.common.LetterTileDrawable;
-import com.android.car.dialer.R;
-import com.android.car.dialer.telecom.ContactBitmapWorker;
-import com.android.car.dialer.ui.CallHistoryListItemProvider;
-import com.android.car.dialer.ui.CallLogListingTask;
-import com.android.car.dialer.ui.CircleBitmapDrawable;
-
-import androidx.car.widget.TextListItem;
-
-/**
- * List item which is created by {@link CallHistoryListItemProvider} binds a call list item to a
- * list view item.
- */
-public class CallLogListItem extends TextListItem {
- private final CallLogListingTask.CallLogItem mCallLogItem;
- private final Context mContext;
-
- public CallLogListItem(Context context, CallLogListingTask.CallLogItem callLog) {
- super(context);
- mCallLogItem = callLog;
- mContext = context;
- }
-
- @Override
- public void onBind(ViewHolder viewHolder) {
- super.onBind(viewHolder);
- ContactBitmapWorker.loadBitmap(mContext.getContentResolver(), viewHolder.getPrimaryIcon(),
- mCallLogItem.mNumber,
- bitmap -> {
- Resources r = mContext.getResources();
- viewHolder.getPrimaryIcon().setScaleType(ImageView.ScaleType.CENTER);
- Drawable avatarDrawable;
- if (bitmap != null) {
- avatarDrawable = new CircleBitmapDrawable(r, bitmap);
- setPrimaryActionIcon(new CircleBitmapDrawable(r, bitmap), true);
- } else {
- LetterTileDrawable letterTileDrawable = new LetterTileDrawable(r);
- letterTileDrawable.setContactDetails(mCallLogItem.mTitle,
- mCallLogItem.mNumber);
- letterTileDrawable.setIsCircular(true);
- avatarDrawable = letterTileDrawable;
- }
-
- int iconSize = mContext.getResources().getDimensionPixelSize(
- R.dimen.avatar_icon_size);
- setPrimaryActionIcon(scaleDrawable(avatarDrawable, iconSize), true);
- super.onBind(viewHolder);
- });
-
- viewHolder.getContainerLayout().setBackgroundColor(
- mContext.getColor(R.color.call_history_list_item_color));
- }
-
- private Drawable scaleDrawable(Drawable targetDrawable, int sizeInPixel) {
- Bitmap bitmap = null;
- if (targetDrawable instanceof CircleBitmapDrawable) {
- bitmap = ((CircleBitmapDrawable) targetDrawable).toBitmap(sizeInPixel);
- } else if (targetDrawable instanceof LetterTileDrawable){
- bitmap = ((LetterTileDrawable) targetDrawable).toBitmap(sizeInPixel);
- }
- return new BitmapDrawable(mContext.getResources(), bitmap);
- }
-}
diff --git a/src/com/android/car/dialer/ui/listitem/ContactListItem.java b/src/com/android/car/dialer/ui/listitem/ContactListItem.java
deleted file mode 100644
index 7e5792f9..00000000
--- a/src/com/android/car/dialer/ui/listitem/ContactListItem.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.car.dialer.ui.listitem;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
-import android.widget.ImageView;
-
-import com.android.car.apps.common.LetterTileDrawable;
-import com.android.car.dialer.R;
-import com.android.car.dialer.telecom.ContactBitmapWorker;
-import com.android.car.dialer.ui.CircleBitmapDrawable;
-import com.android.car.dialer.ui.ContactListFragment;
-
-import androidx.car.widget.TextListItem;
-
-/**
- * ListItem for contact.
- */
-public class ContactListItem extends TextListItem {
- private Context mContext;
- private ContactListFragment.ContactItem mContactItem;
-
- public ContactListItem(Context context, ContactListFragment.ContactItem contactItem) {
- super(context);
- mContext = context;
- mContactItem = contactItem;
- }
-
- @Override
- public void onBind(ViewHolder viewHolder) {
- super.onBind(viewHolder);
- ContactBitmapWorker.loadBitmap(mContext.getContentResolver(), viewHolder.getPrimaryIcon(),
- mContactItem.mNumber,
- bitmap -> {
- Resources r = mContext.getResources();
- viewHolder.getPrimaryIcon().setScaleType(ImageView.ScaleType.CENTER);
- Drawable avatarDrawable;
- if (bitmap != null) {
- avatarDrawable = new CircleBitmapDrawable(r, bitmap);
- } else {
- LetterTileDrawable letterTileDrawable = new LetterTileDrawable(r);
- letterTileDrawable.setContactDetails(mContactItem.mDisplayName,
- mContactItem.mNumber);
- letterTileDrawable.setIsCircular(true);
- avatarDrawable = letterTileDrawable;
- }
-
- int iconSize = mContext.getResources().getDimensionPixelSize(
- R.dimen.avatar_icon_size);
- setPrimaryActionIcon(scaleDrawable(avatarDrawable, iconSize), true);
- super.onBind(viewHolder);
-
- // force rebind the view.
- super.onBind(viewHolder);
- });
- viewHolder.getContainerLayout().setBackgroundColor(
- mContext.getColor(R.color.contact_list_item_color));
- }
-
- private Drawable scaleDrawable(Drawable targetDrawable, int sizeInPixel) {
- Bitmap bitmap = null;
- if (targetDrawable instanceof CircleBitmapDrawable) {
- bitmap = ((CircleBitmapDrawable) targetDrawable).toBitmap(sizeInPixel);
- } else if (targetDrawable instanceof LetterTileDrawable){
- bitmap = ((LetterTileDrawable) targetDrawable).toBitmap(sizeInPixel);
- }
- return new BitmapDrawable(mContext.getResources(), bitmap);
- }
-}
diff --git a/src/com/android/car/dialer/ui/menu/MenuActionProvider.java b/src/com/android/car/dialer/ui/menu/MenuActionProvider.java
new file mode 100644
index 00000000..108d4277
--- /dev/null
+++ b/src/com/android/car/dialer/ui/menu/MenuActionProvider.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.menu;
+
+import android.content.Context;
+import android.view.ActionProvider;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.ImageView;
+
+import com.android.car.dialer.R;
+
+/**
+ * Dialer's {@link ActionProvider} which provides a custom action view. Right now, it requires an
+ * intent set in {@link com.android.car.dialer.ui.TelecomActivity#onCreateOptionsMenu(Menu)} and on
+ * click will launch the intent.
+ */
+public class MenuActionProvider extends ActionProvider {
+ private final Context mContext;
+
+ public MenuActionProvider(Context context) {
+ super(context);
+ mContext = context;
+ }
+
+ /** @deprecated in parent, see {@link ActionProvider#onCreateActionView()} */
+ @Deprecated
+ @Override
+ public View onCreateActionView() {
+ return null;
+ }
+
+ @Override
+ public View onCreateActionView(MenuItem forItem) {
+ View actionView = LayoutInflater.from(mContext).inflate(R.layout.menu_action_view, null);
+ actionView.setTooltip(forItem.getTitle());
+ ImageView icon = actionView.findViewById(R.id.menu_icon);
+ icon.setImageDrawable(forItem.getIcon());
+ if (forItem.getIconTintMode() != null) {
+ icon.setImageTintMode(forItem.getIconTintMode());
+ }
+ if (forItem.getIconTintList() != null) {
+ icon.setImageTintList(forItem.getIconTintList());
+ }
+
+ actionView.setOnClickListener(v -> {
+ if (forItem.getIntent() != null) {
+ mContext.startActivity(forItem.getIntent());
+ }
+ });
+ return actionView;
+ }
+}
diff --git a/src/com/android/car/dialer/ui/search/ContactDetails.java b/src/com/android/car/dialer/ui/search/ContactDetails.java
new file mode 100644
index 00000000..ccec3a20
--- /dev/null
+++ b/src/com/android/car/dialer/ui/search/ContactDetails.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.search;
+
+import android.net.Uri;
+
+/**
+ * A struct that holds the details for a contact search result.
+ */
+class ContactDetails {
+ final String displayName;
+ final Uri photoUri;
+ final Uri lookupUri;
+
+ ContactDetails(String displayName, String photoUri, Uri lookupUri) {
+ this.displayName = displayName;
+ this.photoUri = photoUri == null ? null : Uri.parse(photoUri);
+ this.lookupUri = lookupUri;
+ }
+}
diff --git a/src/com/android/car/dialer/ui/search/ContactResultViewHolder.java b/src/com/android/car/dialer/ui/search/ContactResultViewHolder.java
new file mode 100644
index 00000000..a7071000
--- /dev/null
+++ b/src/com/android/car/dialer/ui/search/ContactResultViewHolder.java
@@ -0,0 +1,65 @@
+/*
+ * 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.car.dialer.ui.search;
+
+import android.content.Context;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.car.dialer.R;
+import com.android.car.dialer.ui.view.ContactAvatarOutputlineProvider;
+import com.android.car.telephony.common.TelecomUtils;
+
+/**
+ * A {@link androidx.recyclerview.widget.RecyclerView.ViewHolder} that will parse relevant
+ * views out of a {@code contact_result} layout.
+ */
+public class ContactResultViewHolder extends RecyclerView.ViewHolder {
+ private final Context mContext;
+ private final View mContactCard;
+ private final TextView mContactName;
+ private final ImageView mContactPicture;
+ private final ContactResultsAdapter.OnShowContactDetailListener mOnShowContactDetailListener;
+
+ public ContactResultViewHolder(View view,
+ ContactResultsAdapter.OnShowContactDetailListener onShowContactDetailListener) {
+ super(view);
+ mContext = view.getContext();
+ mContactCard = view.findViewById(R.id.contact_result);
+ mContactName = view.findViewById(R.id.contact_name);
+ mContactPicture = view.findViewById(R.id.contact_picture);
+ mContactPicture.setOutlineProvider(ContactAvatarOutputlineProvider.get());
+ mOnShowContactDetailListener = onShowContactDetailListener;
+ }
+
+ /**
+ * Populates the view that is represented by this ViewHolder with the information in the
+ * provided {@link ContactDetails}.
+ */
+ public void bind(ContactDetails details) {
+ mContactCard.setOnClickListener(v -> {
+ mOnShowContactDetailListener.onShowContactDetail(details.lookupUri);
+ });
+
+ mContactName.setText(details.displayName);
+ TelecomUtils.setContactBitmapAsync(mContext, mContactPicture, details.photoUri,
+ details.displayName);
+ }
+}
diff --git a/src/com/android/car/dialer/ContactResultsAdapter.java b/src/com/android/car/dialer/ui/search/ContactResultsAdapter.java
index 20fa2c86..639ca9f8 100644
--- a/src/com/android/car/dialer/ContactResultsAdapter.java
+++ b/src/com/android/car/dialer/ui/search/ContactResultsAdapter.java
@@ -14,28 +14,37 @@
* limitations under the License.
*/
-package com.android.car.dialer;
+package com.android.car.dialer.ui.search;
import android.database.Cursor;
import android.net.Uri;
-import android.provider.ContactsContract.Contacts;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.car.dialer.R;
+
import java.util.ArrayList;
import java.util.List;
-import androidx.car.widget.PagedListView;
-import androidx.recyclerview.widget.RecyclerView;
-
/**
- * An adapter that will parse a list of contacts given by a {@link Cursor} that display the
- * results as a list.
+ * An adapter that will parse a list of contacts given by a {@link Cursor} that display the
+ * results as a list.
*/
-public class ContactResultsAdapter extends RecyclerView.Adapter<ContactResultViewHolder>
- implements PagedListView.ItemCap {
- private final List<ContactResultViewHolder.ContactDetails> mContacts = new ArrayList<>();
+public class ContactResultsAdapter extends RecyclerView.Adapter<ContactResultViewHolder> {
+
+ interface OnShowContactDetailListener {
+ void onShowContactDetail(Uri contactLookupUri);
+ }
+
+ private final List<ContactDetails> mContacts = new ArrayList<>();
+ private final OnShowContactDetailListener mOnShowContactDetailListener;
+
+ public ContactResultsAdapter(OnShowContactDetailListener onShowContactDetailListener) {
+ mOnShowContactDetailListener = onShowContactDetailListener;
+ }
/**
* Clears all contact results from this adapter.
@@ -49,24 +58,9 @@ public class ContactResultsAdapter extends RecyclerView.Adapter<ContactResultVie
* Sets the list of contacts that should be displayed. The given {@link Cursor} can be safely
* closed after this call.
*/
- public void setData(Cursor data) {
+ public void setData(List<ContactDetails> data) {
mContacts.clear();
-
- while (data.moveToNext()) {
- int idColIdx = data.getColumnIndex(Contacts._ID);
- int lookupColIdx = data.getColumnIndex(Contacts.LOOKUP_KEY);
- int nameColIdx = data.getColumnIndex(Contacts.DISPLAY_NAME);
- int photoUriColIdx = data.getColumnIndex(Contacts.PHOTO_URI);
-
- Uri lookupUri = Contacts.getLookupUri(
- data.getLong(idColIdx), data.getString(lookupColIdx));
-
- mContacts.add(new ContactResultViewHolder.ContactDetails(
- data.getString(nameColIdx),
- data.getString(photoUriColIdx),
- lookupUri));
- }
-
+ mContacts.addAll(data);
notifyDataSetChanged();
}
@@ -74,12 +68,12 @@ public class ContactResultsAdapter extends RecyclerView.Adapter<ContactResultVie
public ContactResultViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.contact_result, parent, false);
- return new ContactResultViewHolder(view);
+ return new ContactResultViewHolder(view, mOnShowContactDetailListener);
}
@Override
public void onBindViewHolder(ContactResultViewHolder holder, int position) {
- holder.bind(mContacts.get(position), getItemCount());
+ holder.bind(mContacts.get(position));
}
@Override
@@ -92,10 +86,4 @@ public class ContactResultsAdapter extends RecyclerView.Adapter<ContactResultVie
public int getItemCount() {
return mContacts.size();
}
-
- @Override
- public void setMaxItems(int max) {
- // No-op. A PagedListView needs the ItemCap interface to be implemented. However, the
- // list of contacts not be limited.
- }
}
diff --git a/src/com/android/car/dialer/ui/search/ContactResultsFragment.java b/src/com/android/car/dialer/ui/search/ContactResultsFragment.java
new file mode 100644
index 00000000..56b72930
--- /dev/null
+++ b/src/com/android/car/dialer/ui/search/ContactResultsFragment.java
@@ -0,0 +1,213 @@
+/*
+ * 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.car.dialer.ui.search;
+
+import android.app.ActionBar;
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.Menu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.SearchView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.ViewModelProviders;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.car.dialer.R;
+import com.android.car.dialer.log.L;
+import com.android.car.dialer.ui.common.DialerListBaseFragment;
+import com.android.car.dialer.ui.contact.ContactDetailsFragment;
+
+/**
+ * A fragment that will take a search query, look up contacts that match and display those
+ * results as a list.
+ */
+public class ContactResultsFragment extends DialerListBaseFragment implements
+ ContactResultsAdapter.OnShowContactDetailListener {
+
+ /**
+ * Creates a new instance of the {@link ContactResultsFragment}.
+ *
+ * @param initialSearchQuery An optional search query that will be inputted when the fragment
+ * starts up.
+ */
+ public static ContactResultsFragment newInstance(@Nullable String initialSearchQuery) {
+ ContactResultsFragment fragment = new ContactResultsFragment();
+ Bundle args = new Bundle();
+ args.putString(SEARCH_QUERY, initialSearchQuery);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ public static final String FRAGMENT_TAG = "ContactResultsFragment";
+
+ private static final String TAG = "CD.ContactResultsFragment";
+ private static final String SEARCH_QUERY = "SearchQuery";
+ private static final String KEY_KEYBOARD_SHOWN = "KeyboardShown";
+
+ private ContactResultsViewModel mContactResultsViewModel;
+ private final ContactResultsAdapter mAdapter = new ContactResultsAdapter(this);
+
+ private RecyclerView.OnScrollListener mOnScrollChangeListener;
+ private SearchView mSearchView;
+
+ private boolean mKeyboardShown = false;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setHasOptionsMenu(true);
+
+ mContactResultsViewModel = ViewModelProviders.of(this).get(
+ ContactResultsViewModel.class);
+ mContactResultsViewModel.getContactSearchResults().observe(this,
+ contactResults -> mAdapter.setData(contactResults));
+
+ // Set the initial search query, if one was provided from a Intent.ACTION_SEARCH
+ if (getArguments() != null) {
+ String initialQuery = getArguments().getString(SEARCH_QUERY);
+ if (!TextUtils.isEmpty(initialQuery)) {
+ setSearchQuery(initialQuery);
+ }
+ getArguments().clear();
+ }
+
+ if (savedInstanceState != null) {
+ mKeyboardShown = savedInstanceState.getBoolean(KEY_KEYBOARD_SHOWN, false);
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle savedInstanceState) {
+ savedInstanceState.putBoolean(KEY_KEYBOARD_SHOWN, mKeyboardShown);
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ getRecyclerView().setAdapter(mAdapter);
+
+ mOnScrollChangeListener = new RecyclerView.OnScrollListener() {
+ @Override
+ public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
+ }
+
+ @Override
+ public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
+ if (dy != 0) {
+ mSearchView.clearFocus();
+ }
+ }
+ };
+ getRecyclerView().addOnScrollListener(mOnScrollChangeListener);
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ getRecyclerView().removeOnScrollListener(mOnScrollChangeListener);
+ }
+
+ @Override
+ public void onPrepareOptionsMenu(Menu menu) {
+ menu.findItem(R.id.menu_contacts_search).setVisible(false);
+ }
+
+ @Override
+ protected void setupActionBar(@NonNull ActionBar actionBar) {
+ super.setupActionBar(actionBar);
+
+ // We have to use the setCustomView that accepts a LayoutParams to get the SearchView
+ // to take up the full height and width of the action bar
+ View v = getLayoutInflater().inflate(R.layout.search_view, null);
+ actionBar.setCustomView(v, new ActionBar.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT));
+
+ SearchView searchView = actionBar.getCustomView().findViewById(R.id.search_view);
+
+ // We need to call setIconified(false) so the SearchView is a text box instead of just
+ // an icon, but doing so also focuses on it and shows the keyboard. The first time we
+ // enter the fragment that's fine, but every time after we have to clearFocus() so the
+ // keyboard isn't shown.
+ searchView.setIconified(false);
+ if (mKeyboardShown) {
+ searchView.clearFocus();
+ } else {
+ mKeyboardShown = true;
+ }
+
+ searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
+ @Override
+ public boolean onQueryTextSubmit(String query) {
+ L.d(TAG, "onQueryTextSubmit: %s", query);
+ return false;
+ }
+
+ @Override
+ public boolean onQueryTextChange(String newText) {
+ L.d(TAG, "onQueryTextChange: %s", newText);
+ onNewQuery(newText);
+ return false;
+ }
+ });
+
+ // Don't collapse the search view by clicking the clear button on an empty input
+ searchView.setOnCloseListener(() -> true);
+
+ mSearchView = searchView;
+ setSearchQuery(mContactResultsViewModel.getSearchQuery());
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mSearchView.clearFocus();
+ }
+
+ /**
+ * Sets the search query that should be used to filter contacts.
+ */
+ public void setSearchQuery(String query) {
+ if (mSearchView != null) {
+ // This will update the search field and trigger the onNewQuery.
+ // "submit" flag is false so it won't send search intent and ending in infinite loop.
+ mSearchView.setQuery(query, /* submit= */false);
+ } else {
+ onNewQuery(query);
+ }
+ }
+
+ /** Triggered by search view text change. */
+ private void onNewQuery(String newQuery) {
+ mContactResultsViewModel.setSearchQuery(newQuery);
+ }
+
+ @Override
+ protected CharSequence getActionBarTitle() {
+ return null;
+ }
+
+ @Override
+ public void onShowContactDetail(Uri contactLookupUri) {
+ Fragment contactDetailsFragment = ContactDetailsFragment.newInstance(contactLookupUri);
+ pushContentFragment(contactDetailsFragment, ContactDetailsFragment.FRAGMENT_TAG);
+ }
+}
diff --git a/src/com/android/car/dialer/ui/search/ContactResultsViewModel.java b/src/com/android/car/dialer/ui/search/ContactResultsViewModel.java
new file mode 100644
index 00000000..8dc979fb
--- /dev/null
+++ b/src/com/android/car/dialer/ui/search/ContactResultsViewModel.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.search;
+
+import android.app.Application;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+
+import com.android.car.telephony.common.ObservableAsyncQuery;
+import com.android.car.telephony.common.QueryParam;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/** {link AndroidViewModel} used for search functionality. */
+public class ContactResultsViewModel extends AndroidViewModel {
+ private static final String[] CONTACT_DETAILS_PROJECTION = {
+ ContactsContract.Contacts._ID,
+ ContactsContract.Contacts.LOOKUP_KEY,
+ ContactsContract.Contacts.DISPLAY_NAME,
+ ContactsContract.Contacts.PHOTO_URI
+ };
+
+ private final SearchQueryParamProvider mSearchQueryParamProvider;
+ private final ObservableAsyncQuery mObservableAsyncQuery;
+ private final MutableLiveData<List<ContactDetails>> mContactSearchResultsLiveData;
+ private String mSearchQuery;
+
+ public ContactResultsViewModel(@NonNull Application application) {
+ super(application);
+ mSearchQueryParamProvider = new SearchQueryParamProvider();
+ mContactSearchResultsLiveData = new MutableLiveData<>();
+ mObservableAsyncQuery = new ObservableAsyncQuery(mSearchQueryParamProvider,
+ application.getContentResolver(), this::onQueryFinished);
+ }
+
+ void setSearchQuery(String searchQuery) {
+ if (TextUtils.equals(mSearchQuery, searchQuery)) {
+ return;
+ }
+
+ mSearchQuery = searchQuery;
+ if (TextUtils.isEmpty(searchQuery)) {
+ mContactSearchResultsLiveData.setValue(Collections.emptyList());
+ } else {
+ mObservableAsyncQuery.startQuery();
+ }
+ }
+
+ LiveData<List<ContactDetails>> getContactSearchResults() {
+ return mContactSearchResultsLiveData;
+ }
+
+ String getSearchQuery() {
+ return mSearchQuery;
+ }
+
+ private void onQueryFinished(@Nullable Cursor cursor) {
+ if (cursor == null) {
+ mContactSearchResultsLiveData.setValue(Collections.emptyList());
+ return;
+ }
+
+ List<ContactDetails> contactDetails = new ArrayList<>();
+ while (cursor.moveToNext()) {
+ int idColIdx = cursor.getColumnIndex(ContactsContract.Contacts._ID);
+ int lookupColIdx = cursor.getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY);
+ int nameColIdx = cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME);
+ int photoUriColIdx = cursor.getColumnIndex(ContactsContract.Contacts.PHOTO_URI);
+
+ Uri lookupUri = ContactsContract.Contacts.getLookupUri(
+ cursor.getLong(idColIdx), cursor.getString(lookupColIdx));
+
+ contactDetails.add(new ContactDetails(
+ cursor.getString(nameColIdx),
+ cursor.getString(photoUriColIdx),
+ lookupUri));
+ }
+ mContactSearchResultsLiveData.setValue(contactDetails);
+ cursor.close();
+ }
+
+ private class SearchQueryParamProvider implements QueryParam.Provider {
+
+ @Nullable
+ @Override
+ public QueryParam getQueryParam() {
+ Uri lookupUri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_FILTER_URI,
+ Uri.encode(mSearchQuery));
+ return new QueryParam(lookupUri, CONTACT_DETAILS_PROJECTION,
+ ContactsContract.Contacts.HAS_PHONE_NUMBER + "!=0",
+ /* selectionArgs= */null, /* orderBy= */null);
+ }
+ }
+}
diff --git a/src/com/android/car/dialer/ui/settings/DialerSettingsActivity.java b/src/com/android/car/dialer/ui/settings/DialerSettingsActivity.java
new file mode 100644
index 00000000..c90f9a97
--- /dev/null
+++ b/src/com/android/car/dialer/ui/settings/DialerSettingsActivity.java
@@ -0,0 +1,50 @@
+/**
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.settings;
+
+import android.os.Bundle;
+import android.widget.Toolbar;
+
+import androidx.fragment.app.FragmentActivity;
+
+import com.android.car.dialer.R;
+import com.android.car.dialer.log.L;
+
+/**
+ * Activity for settings in Dialer
+ */
+public class DialerSettingsActivity extends FragmentActivity {
+ private static final String TAG = "CD.SettingsActivity";
+
+ private Toolbar mToolbar;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ L.d(TAG, "onCreate");
+
+ setContentView(R.layout.dialer_settings_activity);
+ mToolbar = findViewById(R.id.settings_toolbar);
+ setActionBar(mToolbar);
+
+ getSupportFragmentManager().beginTransaction()
+ .replace(R.id.settings_fragment_container, new DialerSettingsFragment())
+ .commit();
+
+ getActionBar().setDisplayHomeAsUpEnabled(true);
+ }
+}
diff --git a/src/com/android/car/dialer/ui/settings/DialerSettingsFragment.java b/src/com/android/car/dialer/ui/settings/DialerSettingsFragment.java
new file mode 100644
index 00000000..8615629c
--- /dev/null
+++ b/src/com/android/car/dialer/ui/settings/DialerSettingsFragment.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.settings;
+
+import android.os.Bundle;
+
+import androidx.annotation.Nullable;
+import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.Fragment;
+import androidx.preference.ListPreference;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceFragmentCompat;
+
+import com.android.car.dialer.R;
+import com.android.car.dialer.ui.settings.common.SettingsListPreferenceDialogFragment;
+
+/**
+ * A fragment that displays the settings page
+ */
+public class DialerSettingsFragment extends PreferenceFragmentCompat {
+ private static final String TAG = "CD.SettingsFragment";
+ private static final String DIALOG_FRAGMENT_TAG = "DIALOG";
+
+ @Override
+ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
+ setPreferencesFromResource(R.xml.settings_page, rootKey);
+ }
+
+ /**
+ * Settings needs to launch custom dialog types in order to extend the Device Default theme.
+ *
+ * @param preference The Preference object requesting the dialog.
+ */
+ @Override
+ public void onDisplayPreferenceDialog(Preference preference) {
+ // check if dialog is already showing
+ if (findDialogByTag(DIALOG_FRAGMENT_TAG) != null) {
+ return;
+ }
+
+ DialogFragment dialogFragment;
+ if (preference instanceof ListPreference) {
+ dialogFragment = SettingsListPreferenceDialogFragment.newInstance(preference.getKey());
+ } else {
+ throw new IllegalArgumentException(
+ "Tried to display dialog for unknown preference type. Did you forget to "
+ + "override onDisplayPreferenceDialog()?");
+ }
+
+ dialogFragment.setTargetFragment(/* fragment= */ this, /* requestCode= */ 0);
+ showDialog(dialogFragment, DIALOG_FRAGMENT_TAG);
+ }
+
+ @Nullable
+ private DialogFragment findDialogByTag(String tag) {
+ Fragment fragment = getFragmentManager().findFragmentByTag(tag);
+ if (fragment instanceof DialogFragment) {
+ return (DialogFragment) fragment;
+ }
+ return null;
+ }
+
+ private void showDialog(DialogFragment dialogFragment, @Nullable String tag) {
+ dialogFragment.show(getFragmentManager(), tag);
+ }
+}
diff --git a/src/com/android/car/dialer/ui/settings/common/SettingsListPreferenceDialogFragment.java b/src/com/android/car/dialer/ui/settings/common/SettingsListPreferenceDialogFragment.java
new file mode 100644
index 00000000..f544704e
--- /dev/null
+++ b/src/com/android/car/dialer/ui/settings/common/SettingsListPreferenceDialogFragment.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.settings.common;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.preference.ListPreference;
+
+/**
+ * Presents a dialog with a list of options associated with a {@link ListPreference}.
+ *
+ * <p>Note: this is borrowed as-is from androidx.preference.EditTextPreferenceDialogFragmentCompat
+ * with updates to formatting to match the project style. DialerSettings needs to use custom dialog
+ * implementations in order to launch the platform {@link AlertDialog} instead of the one in the
+ * support library.
+ */
+public class SettingsListPreferenceDialogFragment extends SettingsPreferenceDialogFragment {
+
+ private static final String SAVE_STATE_INDEX = "SettingsListPreferenceDialogFragment.index";
+ private static final String SAVE_STATE_ENTRIES = "SettingsListPreferenceDialogFragment.entries";
+ private static final String SAVE_STATE_ENTRY_VALUES =
+ "SettingsListPreferenceDialogFragment.entryValues";
+
+ private int mClickedDialogEntryIndex;
+ private CharSequence[] mEntries;
+ private CharSequence[] mEntryValues;
+
+ /**
+ * Returns a new instance of {@link SettingsListPreferenceDialogFragment} for the {@link
+ * ListPreference} with the given {@code key}.
+ */
+ public static SettingsListPreferenceDialogFragment newInstance(String key) {
+ SettingsListPreferenceDialogFragment fragment = new SettingsListPreferenceDialogFragment();
+ Bundle b = new Bundle(/* capacity= */ 1);
+ b.putString(ARG_KEY, key);
+ fragment.setArguments(b);
+ return fragment;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (savedInstanceState == null) {
+ ListPreference preference = getListPreference();
+
+ if (preference.getEntries() == null || preference.getEntryValues() == null) {
+ throw new IllegalStateException(
+ "ListPreference requires an entries array and an entryValues array.");
+ }
+
+ mClickedDialogEntryIndex = preference.findIndexOfValue(preference.getValue());
+ mEntries = preference.getEntries();
+ mEntryValues = preference.getEntryValues();
+ } else {
+ mClickedDialogEntryIndex = savedInstanceState.getInt(SAVE_STATE_INDEX, 0);
+ mEntries = savedInstanceState.getCharSequenceArray(SAVE_STATE_ENTRIES);
+ mEntryValues = savedInstanceState.getCharSequenceArray(SAVE_STATE_ENTRY_VALUES);
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(@NonNull Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putInt(SAVE_STATE_INDEX, mClickedDialogEntryIndex);
+ outState.putCharSequenceArray(SAVE_STATE_ENTRIES, mEntries);
+ outState.putCharSequenceArray(SAVE_STATE_ENTRY_VALUES, mEntryValues);
+ }
+
+ private ListPreference getListPreference() {
+ return (ListPreference) getPreference();
+ }
+
+ @Override
+ protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
+ super.onPrepareDialogBuilder(builder);
+
+ builder.setSingleChoiceItems(mEntries, mClickedDialogEntryIndex,
+ (dialog, which) -> {
+ mClickedDialogEntryIndex = which;
+
+ // Clicking on an item simulates the positive button click, and dismisses the
+ // dialog.
+ SettingsListPreferenceDialogFragment.this.onClick(dialog,
+ DialogInterface.BUTTON_POSITIVE);
+ dialog.dismiss();
+ });
+
+ // The typical interaction for list-based dialogs is to have click-on-an-item dismiss the
+ // dialog instead of the user having to press 'Ok'.
+ builder.setPositiveButton(null, null);
+ }
+
+ @Override
+ public void onDialogClosed(boolean positiveResult) {
+ if (positiveResult && mClickedDialogEntryIndex >= 0) {
+ String value = mEntryValues[mClickedDialogEntryIndex].toString();
+ ListPreference preference = getListPreference();
+ if (preference.callChangeListener(value)) {
+ preference.setValue(value);
+ }
+ }
+ }
+
+}
diff --git a/src/com/android/car/dialer/ui/settings/common/SettingsPreferenceDialogFragment.java b/src/com/android/car/dialer/ui/settings/common/SettingsPreferenceDialogFragment.java
new file mode 100644
index 00000000..c885f6a2
--- /dev/null
+++ b/src/com/android/car/dialer/ui/settings/common/SettingsPreferenceDialogFragment.java
@@ -0,0 +1,286 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.settings.common;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.TextView;
+
+import androidx.annotation.CallSuper;
+import androidx.annotation.LayoutRes;
+import androidx.annotation.NonNull;
+import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.Fragment;
+import androidx.preference.DialogPreference;
+import androidx.preference.PreferenceFragmentCompat;
+
+/**
+ * Abstract base class which presents a dialog associated with a {@link DialogPreference}.
+ * Since the preference object may not be available during fragment re-creation, the
+ * necessary information for displaying the dialog is read once during the initial call
+ * to {@link #onCreate(Bundle)} and saved/restored in the saved instance state.
+ * Custom subclasses should also follow this pattern.
+ *
+ * <p>Note: this is borrowed as-is from androidx.preference.PreferenceDialogFragmentCompat with
+ * updates to formatting to match the project style. Settings needs to use custom dialog
+ * implementations in order to launch the platform {@link AlertDialog} instead of the one in the
+ * support library.
+ */
+public abstract class SettingsPreferenceDialogFragment extends DialogFragment implements
+ DialogInterface.OnClickListener {
+
+ protected static final String ARG_KEY = "key";
+
+ private static final String SAVE_STATE_TITLE = "SettingsPreferenceDialogFragment.title";
+ private static final String SAVE_STATE_POSITIVE_TEXT =
+ "SettingsPreferenceDialogFragment.positiveText";
+ private static final String SAVE_STATE_NEGATIVE_TEXT =
+ "SettingsPreferenceDialogFragment.negativeText";
+ private static final String SAVE_STATE_MESSAGE = "SettingsPreferenceDialogFragment.message";
+ private static final String SAVE_STATE_LAYOUT = "SettingsPreferenceDialogFragment.layout";
+ private static final String SAVE_STATE_ICON = "SettingsPreferenceDialogFragment.icon";
+
+ private DialogPreference mPreference;
+
+ private CharSequence mDialogTitle;
+ private CharSequence mPositiveButtonText;
+ private CharSequence mNegativeButtonText;
+ private CharSequence mDialogMessage;
+ @LayoutRes
+ private int mDialogLayoutRes;
+
+ private BitmapDrawable mDialogIcon;
+
+ /** Which button was clicked. */
+ private int mWhichButtonClicked;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ Fragment rawFragment = getTargetFragment();
+ if (!(rawFragment instanceof DialogPreference.TargetFragment)) {
+ throw new IllegalStateException(
+ "Target fragment must implement TargetFragment interface");
+ }
+
+ DialogPreference.TargetFragment fragment =
+ (DialogPreference.TargetFragment) rawFragment;
+
+ String key = getArguments().getString(ARG_KEY);
+ if (savedInstanceState == null) {
+ mPreference = (DialogPreference) fragment.findPreference(key);
+ mDialogTitle = mPreference.getDialogTitle();
+ mPositiveButtonText = mPreference.getPositiveButtonText();
+ mNegativeButtonText = mPreference.getNegativeButtonText();
+ mDialogMessage = mPreference.getDialogMessage();
+ mDialogLayoutRes = mPreference.getDialogLayoutResource();
+
+ Drawable icon = mPreference.getDialogIcon();
+ if (icon == null || icon instanceof BitmapDrawable) {
+ mDialogIcon = (BitmapDrawable) icon;
+ } else {
+ Bitmap bitmap = Bitmap.createBitmap(icon.getIntrinsicWidth(),
+ icon.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ icon.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+ icon.draw(canvas);
+ mDialogIcon = new BitmapDrawable(getResources(), bitmap);
+ }
+ } else {
+ mDialogTitle = savedInstanceState.getCharSequence(SAVE_STATE_TITLE);
+ mPositiveButtonText = savedInstanceState.getCharSequence(SAVE_STATE_POSITIVE_TEXT);
+ mNegativeButtonText = savedInstanceState.getCharSequence(SAVE_STATE_NEGATIVE_TEXT);
+ mDialogMessage = savedInstanceState.getCharSequence(SAVE_STATE_MESSAGE);
+ mDialogLayoutRes = savedInstanceState.getInt(SAVE_STATE_LAYOUT, 0);
+ Bitmap bitmap = savedInstanceState.getParcelable(SAVE_STATE_ICON);
+ if (bitmap != null) {
+ mDialogIcon = new BitmapDrawable(getResources(), bitmap);
+ }
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(@NonNull Bundle outState) {
+ super.onSaveInstanceState(outState);
+
+ outState.putCharSequence(SAVE_STATE_TITLE, mDialogTitle);
+ outState.putCharSequence(SAVE_STATE_POSITIVE_TEXT, mPositiveButtonText);
+ outState.putCharSequence(SAVE_STATE_NEGATIVE_TEXT, mNegativeButtonText);
+ outState.putCharSequence(SAVE_STATE_MESSAGE, mDialogMessage);
+ outState.putInt(SAVE_STATE_LAYOUT, mDialogLayoutRes);
+ if (mDialogIcon != null) {
+ outState.putParcelable(SAVE_STATE_ICON, mDialogIcon.getBitmap());
+ }
+ }
+
+ @Override
+ @NonNull
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ Context context = getActivity();
+ mWhichButtonClicked = DialogInterface.BUTTON_NEGATIVE;
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context)
+ .setTitle(mDialogTitle)
+ .setIcon(mDialogIcon)
+ .setPositiveButton(mPositiveButtonText, this)
+ .setNegativeButton(mNegativeButtonText, this);
+
+ View contentView = onCreateDialogView(context);
+ if (contentView != null) {
+ onBindDialogView(contentView);
+ builder.setView(contentView);
+ } else {
+ builder.setMessage(mDialogMessage);
+ }
+
+ onPrepareDialogBuilder(builder);
+
+ // Create the dialog
+ Dialog dialog = builder.create();
+ if (needInputMethod()) {
+ // Request input only after the dialog is shown. This is to prevent an issue where the
+ // dialog view collapsed the content on small displays.
+ dialog.setOnShowListener(d -> requestInputMethod(dialog));
+ }
+
+ return dialog;
+ }
+
+ /**
+ * Get the preference that requested this dialog. Available after {@link #onCreate(Bundle)} has
+ * been called on the {@link PreferenceFragmentCompat} which launched this dialog.
+ *
+ * @return the {@link DialogPreference} associated with this dialog.
+ */
+ public DialogPreference getPreference() {
+ if (mPreference == null) {
+ String key = getArguments().getString(ARG_KEY);
+ DialogPreference.TargetFragment fragment =
+ (DialogPreference.TargetFragment) getTargetFragment();
+ mPreference = (DialogPreference) fragment.findPreference(key);
+ }
+ return mPreference;
+ }
+
+ /**
+ * Prepares the dialog builder to be shown when the preference is clicked. Use this to set
+ * custom properties on the dialog.
+ *
+ * <p>Do not {@link AlertDialog.Builder#create()} or {@link AlertDialog.Builder#show()}.
+ */
+ protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
+ }
+
+ /**
+ * Returns whether the preference needs to display a soft input method when the dialog is
+ * displayed. Default is false. Subclasses should override this method if they need the soft
+ * input method brought up automatically.
+ *
+ * <p>Note: Ensure your subclass manually requests focus (ideally in {@link
+ * #onBindDialogView(View)}) for the input field in order to
+ * correctly attach the input method to the field.
+ */
+ protected boolean needInputMethod() {
+ return false;
+ }
+
+ /**
+ * Sets the required flags on the dialog window to enable input method window to show up.
+ */
+ private void requestInputMethod(Dialog dialog) {
+ Window window = dialog.getWindow();
+ window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
+ }
+
+ /**
+ * Creates the content view for the dialog (if a custom content view is required). By default,
+ * it inflates the dialog layout resource if it is set.
+ *
+ * @return the content View for the dialog.
+ * @see DialogPreference#setLayoutResource(int)
+ */
+ protected View onCreateDialogView(Context context) {
+ int resId = mDialogLayoutRes;
+ if (resId == 0) {
+ return null;
+ }
+
+ LayoutInflater inflater = LayoutInflater.from(context);
+ return inflater.inflate(resId, null);
+ }
+
+ /**
+ * Binds views in the content View of the dialog to data.
+ *
+ * <p>Make sure to call through to the superclass implementation.
+ *
+ * @param view the content View of the dialog, if it is custom.
+ */
+ @CallSuper
+ protected void onBindDialogView(View view) {
+ View dialogMessageView = view.findViewById(android.R.id.message);
+
+ if (dialogMessageView != null) {
+ CharSequence message = mDialogMessage;
+ int newVisibility = View.GONE;
+
+ if (!TextUtils.isEmpty(message)) {
+ if (dialogMessageView instanceof TextView) {
+ ((TextView) dialogMessageView).setText(message);
+ }
+
+ newVisibility = View.VISIBLE;
+ }
+
+ if (dialogMessageView.getVisibility() != newVisibility) {
+ dialogMessageView.setVisibility(newVisibility);
+ }
+ }
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ mWhichButtonClicked = which;
+ }
+
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ super.onDismiss(dialog);
+ onDialogClosed(mWhichButtonClicked == DialogInterface.BUTTON_POSITIVE);
+ }
+
+ /**
+ * Called when the dialog is dismissed.
+ *
+ * @param positiveResult {@code true} if the dialog was dismissed with {@link
+ * DialogInterface#BUTTON_POSITIVE}.
+ */
+ protected abstract void onDialogClosed(boolean positiveResult);
+}
diff --git a/src/com/android/car/dialer/ui/CircleBitmapDrawable.java b/src/com/android/car/dialer/ui/view/CircleBitmapDrawable.java
index 857c9427..0be5a9c4 100644
--- a/src/com/android/car/dialer/ui/CircleBitmapDrawable.java
+++ b/src/com/android/car/dialer/ui/view/CircleBitmapDrawable.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.car.dialer.ui;
+package com.android.car.dialer.ui.view;
import android.content.res.Resources;
import android.graphics.Bitmap;
diff --git a/src/com/android/car/dialer/ui/view/ContactAvatarOutputlineProvider.java b/src/com/android/car/dialer/ui/view/ContactAvatarOutputlineProvider.java
new file mode 100644
index 00000000..67b1bd8c
--- /dev/null
+++ b/src/com/android/car/dialer/ui/view/ContactAvatarOutputlineProvider.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.view;
+
+import android.graphics.Outline;
+import android.view.View;
+import android.view.ViewOutlineProvider;
+
+import com.android.car.dialer.R;
+
+/** OutlineProvider that changes the shape of ImageViews used to display contact avatars */
+public final class ContactAvatarOutputlineProvider extends ViewOutlineProvider {
+
+ private ContactAvatarOutputlineProvider() {}
+
+ private static final ContactAvatarOutputlineProvider INSTANCE =
+ new ContactAvatarOutputlineProvider();
+
+ /** Gets the singleton instance */
+ public static ContactAvatarOutputlineProvider get() {
+ return INSTANCE;
+ }
+
+ @Override
+ public void getOutline(View view, Outline outline) {
+ float radiusPercent = view.getContext().getResources()
+ .getFloat(R.dimen.contact_avatar_corner_radius_percent);
+ float radius = Math.min(view.getWidth(), view.getHeight()) * radiusPercent;
+ outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), radius);
+ view.setClipToOutline(true);
+ }
+}
diff --git a/src/com/android/car/dialer/ui/view/ListItemOutlineProvider.java b/src/com/android/car/dialer/ui/view/ListItemOutlineProvider.java
new file mode 100644
index 00000000..7e47ed2c
--- /dev/null
+++ b/src/com/android/car/dialer/ui/view/ListItemOutlineProvider.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.view;
+
+import android.graphics.Outline;
+import android.view.View;
+import android.view.ViewOutlineProvider;
+
+/**
+ * Outline provider that allows to only have a rounded top or a rounded bottom. Used for list items
+ * so entire list looks like a card.
+ */
+public class ListItemOutlineProvider extends ViewOutlineProvider {
+
+ private boolean mHasRoundedTop;
+ private boolean mHasRoundedBottom;
+ private final float mRadius;
+
+ public ListItemOutlineProvider(float radius) {
+ mRadius = radius;
+ }
+
+ public void setCorners(boolean hasRoundedTop, boolean hasRoundedBottom) {
+ mHasRoundedTop = hasRoundedTop;
+ mHasRoundedBottom = hasRoundedBottom;
+ }
+
+ @Override
+ public void getOutline(View view, Outline outline) {
+ int left = 0;
+ int right = view.getWidth();
+ int top = 0;
+ int bottom = view.getHeight();
+
+ // If we don't want top rounded corner, the top outline should clip the top above the view
+ // from -radius to 0.
+ if (!mHasRoundedTop) {
+ top -= mRadius;
+ }
+ // If we don't want bottom round corner, the bottom outline should clip the bottom below the
+ // view starting from view's height to view's height + radius.
+ if (!mHasRoundedBottom) {
+ bottom += mRadius;
+ }
+ outline.setRoundRect(left, top, right, bottom, mRadius);
+ }
+}
diff --git a/src/com/android/car/dialer/ui/view/ScaleSpan.java b/src/com/android/car/dialer/ui/view/ScaleSpan.java
new file mode 100644
index 00000000..ef8b6ed6
--- /dev/null
+++ b/src/com/android/car/dialer/ui/view/ScaleSpan.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.view;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.text.StaticLayout;
+import android.text.TextPaint;
+import android.text.TextUtils;
+import android.text.style.ReplacementSpan;
+
+/** {@link ReplacementSpan} that redraw the text with a scaled text size. */
+public class ScaleSpan extends ReplacementSpan {
+ private final float mStartTextSize;
+ private float mTextSize;
+
+ /**
+ * This span is used in animation. To make sure the calculated width does not change during the
+ * animation, we pass in the {@code startTextSize} to get the max width it will use.
+ */
+ public ScaleSpan(float startTextSize) {
+ mStartTextSize = startTextSize;
+ }
+
+ /** Updates the current text size of this span. */
+ public void setTextSize(float textSize) {
+ mTextSize = textSize;
+ }
+
+ @Override
+ public int getSize(Paint paint, CharSequence text, int start, int end,
+ Paint.FontMetricsInt fm) {
+ if (TextUtils.isEmpty(text)) {
+ return 0;
+ }
+ TextPaint textPaint = new TextPaint(paint);
+ textPaint.setTextSize(Math.max(mStartTextSize, paint.getTextSize()));
+ // Remove span and measure, otherwise it will crash due to infinite loop
+ float desiredWidth = StaticLayout.getDesiredWidth(text.toString(), start, end, textPaint);
+ return (int) desiredWidth;
+ }
+
+ @Override
+ public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y,
+ int bottom, Paint paint) {
+ Rect targetRect = new Rect();
+ paint.getTextBounds(text, start, end, targetRect);
+
+ paint.setTextSize(mTextSize);
+ Rect currentRect = new Rect();
+ paint.getTextBounds(text, start, end, currentRect);
+
+ int yShift = (currentRect.height() - targetRect.height()) / 2;
+ canvas.drawText(text.subSequence(start, end).toString(), x, y + yShift, paint);
+ }
+}
diff --git a/src/com/android/car/dialer/ui/view/VerticalListDividerDecoration.java b/src/com/android/car/dialer/ui/view/VerticalListDividerDecoration.java
new file mode 100644
index 00000000..7ba413ce
--- /dev/null
+++ b/src/com/android/car/dialer/ui/view/VerticalListDividerDecoration.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.view;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.car.apps.common.util.Themes;
+import com.android.car.dialer.log.L;
+
+/**
+ * Branched from {@link androidx.recyclerview.widget.DividerItemDecoration} but can hide the last
+ * divider.
+ */
+public class VerticalListDividerDecoration extends RecyclerView.ItemDecoration {
+
+ private static final String TAG = "CD.VerticalListDividerDecoration";
+ private static final int SCROLLING_DOWN = 1;
+ private static final int LIST_DIVIDER_ATTR = android.R.attr.listDivider;
+ private final boolean mHideLastDivider;
+ private final Rect mBounds = new Rect();
+
+ private Drawable mDivider;
+
+ /**
+ * Creates a divider {@link RecyclerView.ItemDecoration} that can be used with a vertical
+ * {@link LinearLayoutManager}.
+ *
+ * @param context Current context, it will be used to access resources.
+ */
+ public VerticalListDividerDecoration(Context context, boolean hideLastDivider) {
+ mDivider = Themes.getAttrDrawable(context, LIST_DIVIDER_ATTR);
+ if (mDivider == null) {
+ L.w(TAG, "@android:attr/listDivider was not set."
+ + " Set divider drawable by calling setDrawable().");
+ }
+
+ mHideLastDivider = hideLastDivider;
+ }
+
+ /**
+ * Sets the {@link Drawable} for this divider.
+ *
+ * @param drawable Drawable that should be used as a divider.
+ */
+ public void setDrawable(@NonNull Drawable drawable) {
+ if (drawable == null) {
+ throw new IllegalArgumentException("Drawable cannot be null.");
+ }
+ mDivider = drawable;
+ }
+
+ @Override
+ public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
+ if (parent.getLayoutManager() == null || mDivider == null) {
+ return;
+ }
+ drawVertical(c, parent);
+ }
+
+ private void drawVertical(Canvas canvas, RecyclerView parent) {
+ canvas.save();
+ final int left;
+ final int right;
+ if (parent.getClipToPadding()) {
+ left = parent.getPaddingLeft();
+ right = parent.getWidth() - parent.getPaddingRight();
+ canvas.clipRect(left, parent.getPaddingTop(), right,
+ parent.getHeight() - parent.getPaddingBottom());
+ } else {
+ left = 0;
+ right = parent.getWidth();
+ }
+
+ final int childCount = parent.getChildCount();
+ final int dividerCount = !parent.canScrollVertically(SCROLLING_DOWN) && mHideLastDivider ?
+ childCount - 1 : childCount;
+ for (int i = 0; i < dividerCount; i++) {
+ final View child = parent.getChildAt(i);
+ parent.getDecoratedBoundsWithMargins(child, mBounds);
+ final int bottom = mBounds.bottom + Math.round(child.getTranslationY());
+ final int top = bottom - mDivider.getIntrinsicHeight();
+ mDivider.setBounds(left, top, right, bottom);
+ mDivider.draw(canvas);
+ }
+ canvas.restore();
+ }
+}
diff --git a/src/com/android/car/dialer/NoHfpFragment.java b/src/com/android/car/dialer/ui/warning/NoHfpFragment.java
index 6bf197b6..6774ee18 100644
--- a/src/com/android/car/dialer/NoHfpFragment.java
+++ b/src/com/android/car/dialer/ui/warning/NoHfpFragment.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -13,8 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.car.dialer;
+package com.android.car.dialer.ui.warning;
+
+import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
@@ -23,6 +25,13 @@ import android.view.ViewGroup;
import android.widget.TextView;
import androidx.fragment.app.Fragment;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModelProviders;
+
+import com.android.car.apps.common.util.ViewUtils;
+import com.android.car.dialer.R;
+import com.android.car.dialer.telecom.UiCallManager;
+import com.android.car.dialer.ui.TelecomActivityViewModel;
/**
* A fragment that informs the user that there is no bluetooth device attached that can make
@@ -30,6 +39,8 @@ import androidx.fragment.app.Fragment;
*/
public class NoHfpFragment extends Fragment {
private static final String ERROR_MESSAGE_KEY = "ERROR_MESSAGE_KEY";
+ private static final String Bluetooth_Setting_ACTION = "android.settings.BLUETOOTH_SETTINGS";
+ private static final String Bluetooth_Setting_CATEGORY = "android.intent.category.DEFAULT";
private TextView mErrorMessageView;
private String mErrorMessage;
@@ -38,7 +49,7 @@ public class NoHfpFragment extends Fragment {
* Returns an instance of the {@link NoHfpFragment} with the given error message as the one to
* display.
*/
- static NoHfpFragment newInstance(String errorMessage) {
+ public static NoHfpFragment newInstance(String errorMessage) {
NoHfpFragment fragment = new NoHfpFragment();
Bundle args = new Bundle();
@@ -60,7 +71,7 @@ public class NoHfpFragment extends Fragment {
/**
* Sets the given error message to be displayed.
*/
- void setErrorMessage(String errorMessage) {
+ public void setErrorMessage(String errorMessage) {
mErrorMessage = errorMessage;
// If this method is called before the error message view is available, then no need to
@@ -73,14 +84,29 @@ public class NoHfpFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
- View v = inflater.inflate(R.layout.no_hfp, container, false);
- mErrorMessageView = v.findViewById(R.id.error_string);
+ View view = inflater.inflate(R.layout.no_hfp, container, false);
+ mErrorMessageView = view.findViewById(R.id.error_string);
// If no error message is set, the default string from the layout will be used.
if (!TextUtils.isEmpty(mErrorMessage)) {
mErrorMessageView.setText(mErrorMessage);
}
- return v;
+ TelecomActivityViewModel viewModel = ViewModelProviders.of(getActivity()).get(
+ TelecomActivityViewModel.class);
+ MutableLiveData<Integer> dialerAppStateLiveData = viewModel.getDialerAppState();
+ View emergencyButton = view.findViewById(R.id.emergency_call_button);
+ ViewUtils.setVisible(emergencyButton, UiCallManager.get().isEmergencyCallSupported());
+ emergencyButton.setOnClickListener(v -> dialerAppStateLiveData.setValue(
+ TelecomActivityViewModel.DialerAppState.EMERGENCY_DIALPAD));
+
+ view.findViewById(R.id.connect_bluetooth_button).setOnClickListener(v -> {
+ Intent launchIntent = new Intent();
+ launchIntent.setAction(Bluetooth_Setting_ACTION);
+ launchIntent.addCategory(Bluetooth_Setting_CATEGORY);
+ startActivity(launchIntent);
+ });
+
+ return view;
}
}
diff --git a/src/com/android/car/dialer/widget/CallTypeIconsView.java b/src/com/android/car/dialer/widget/CallTypeIconsView.java
new file mode 100644
index 00000000..b59ee68d
--- /dev/null
+++ b/src/com/android/car/dialer/widget/CallTypeIconsView.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.dialer.widget;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.View;
+
+import com.android.car.dialer.R;
+import com.android.car.dialer.livedata.CallHistoryLiveData;
+
+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 {
+ // Limit the icons up to 3 and if there are more than 3 calls, append the call count at the end.
+ private static final int MAX_CALL_TYPE_ICONS = 3;
+ private static final String CALL_COUNT_FORMAT = "(%d)";
+
+ private final List<Integer> mCallTypes = new ArrayList<>();
+ private final IconResources mIconResources;
+ private final int mSingleIconSize;
+ private int mIconWidth;
+ private int mIconHeight;
+ private String mCallCountText;
+
+ public CallTypeIconsView(Context context) {
+ this(context, null);
+ }
+
+ public CallTypeIconsView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public CallTypeIconsView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public CallTypeIconsView(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ mIconResources = new IconResources(context);
+ mIconResources.voicemail.setColorFilter(context.getColor(R.color.primary_icon_color),
+ PorterDuff.Mode.SRC_IN);
+ mSingleIconSize = getResources().getDimensionPixelSize(R.dimen.inline_icon_size);
+ }
+
+ public void clear() {
+ setText(null);
+ mCallTypes.clear();
+ mIconWidth = 0;
+ mIconHeight = 0;
+ requestLayout();
+ }
+
+ /**
+ * Call type icons are added in reverse chronological order that the most recent call will be
+ * the first icon.
+ */
+ public void add(int callType) {
+ mCallTypes.add(callType);
+ if (mCallTypes.size() > MAX_CALL_TYPE_ICONS) {
+ setText(String.format(CALL_COUNT_FORMAT, mCallTypes.size()));
+ return;
+ }
+
+ setText(null);
+ mIconWidth += mSingleIconSize + mIconResources.iconMargin;
+ mIconHeight = Math.max(mIconHeight, mSingleIconSize + mIconResources.iconMargin);
+ requestLayout();
+ }
+
+ public String getCallCountText() {
+ return mCallCountText;
+ }
+
+ public int getCallType(int index) {
+ return mCallTypes.get(index);
+ }
+
+ private Drawable getCallTypeDrawable(int callType) {
+ switch (callType) {
+ case CallHistoryLiveData.CallType.INCOMING_TYPE:
+ return mIconResources.incoming;
+ case CallHistoryLiveData.CallType.OUTGOING_TYPE:
+ return mIconResources.outgoing;
+ case CallHistoryLiveData.CallType.MISSED_TYPE:
+ return mIconResources.missed;
+ case CallHistoryLiveData.CallType.VOICEMAIL_TYPE:
+ return mIconResources.voicemail;
+ 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 mIconResources.missed;
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int mWidth = mIconWidth;
+ int mHeight = Math.max(getMeasuredHeight(), mIconHeight);
+ // Add extra end margin if show the count text.
+ if (mCallTypes.size() > MAX_CALL_TYPE_ICONS) {
+ mWidth += mIconResources.iconMargin;
+ }
+ setMeasuredDimension(mWidth, mHeight);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ // Draw up to 3 icons.
+ int left = 0;
+ int iconCount = Math.min(MAX_CALL_TYPE_ICONS, mCallTypes.size());
+ for (int i = 0; i < iconCount; i++) {
+ final Drawable drawable = getCallTypeDrawable(mCallTypes.get(i));
+ final int right = left + mSingleIconSize;
+ drawable.setBounds(left, mIconResources.iconMargin, right,
+ mSingleIconSize + mIconResources.iconMargin);
+ drawable.draw(canvas);
+ left = right + mIconResources.iconMargin;
+ }
+ }
+
+ private void setText(String text) {
+ mCallCountText = text;
+ }
+
+ private static class IconResources {
+ public final Drawable incoming;
+ public final Drawable outgoing;
+ public final Drawable missed;
+ public final Drawable voicemail;
+ public final int iconMargin;
+
+ public IconResources(Context context) {
+ incoming = context.getDrawable(R.drawable.ic_call_received);
+ outgoing = context.getDrawable(R.drawable.ic_call_made);
+ missed = context.getDrawable(R.drawable.ic_call_missed);
+ voicemail = context.getDrawable(R.drawable.ic_voicemail);
+ iconMargin = context.getResources().getDimensionPixelSize(R.dimen.call_log_icon_margin);
+ }
+ }
+}
diff --git a/src/com/android/car/dialer/widget/CheckableRelativeLayout.java b/src/com/android/car/dialer/widget/CheckableRelativeLayout.java
new file mode 100644
index 00000000..73a040c2
--- /dev/null
+++ b/src/com/android/car/dialer/widget/CheckableRelativeLayout.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.dialer.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.Checkable;
+import android.widget.RelativeLayout;
+
+/**
+ * A {@link RelativeLayout} that blocks click/focus event from its checkable child view and sync
+ * the checked state with it. Easy to use with
+ * {@link android.app.AlertDialog.Builder#setSingleChoiceItems}.
+ */
+public class CheckableRelativeLayout extends RelativeLayout implements Checkable {
+ private boolean mChecked;
+ private Checkable mCheckableChildView;
+
+ public CheckableRelativeLayout(Context context) {
+ super(context);
+ }
+
+ public CheckableRelativeLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public CheckableRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public CheckableRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ public void onFinishInflate() {
+ for (int i = 0; i < getChildCount(); i++) {
+ View childView = getChildAt(i);
+ if (childView instanceof Checkable) {
+ childView.setClickable(false);
+ childView.setFocusable(false);
+ childView.setBackground(null);
+ childView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
+ mCheckableChildView = (Checkable) childView;
+ mCheckableChildView.setChecked(isChecked());
+ return;
+ }
+ }
+ super.onFinishInflate();
+ }
+
+ @Override
+ public void setChecked(boolean checked) {
+ if (mChecked != checked) {
+ mChecked = checked;
+ if (mCheckableChildView != null) {
+ mCheckableChildView.setChecked(checked);
+ }
+ }
+ }
+
+ @Override
+ public boolean isChecked() {
+ return mChecked;
+ }
+
+ @Override
+ public void toggle() {
+ setChecked(!mChecked);
+ }
+}
diff --git a/src/com/android/car/dialer/widget/WorkerExecutor.java b/src/com/android/car/dialer/widget/WorkerExecutor.java
new file mode 100644
index 00000000..59b3bf16
--- /dev/null
+++ b/src/com/android/car/dialer/widget/WorkerExecutor.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.widget;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * WorkerExecutor is a singleton tied to the application to provide {@link ExecutorService} for
+ * Dialer to run tasks in background.
+ */
+public class WorkerExecutor {
+ private static WorkerExecutor sWorkerExecutor;
+
+ private ExecutorService mSingleThreadExecutor;
+
+ /** Returns the singleton WorkerExecutor for the application. */
+ public static WorkerExecutor getInstance() {
+ if (sWorkerExecutor == null) {
+ sWorkerExecutor = new WorkerExecutor();
+ }
+ return sWorkerExecutor;
+ }
+
+ private WorkerExecutor() {
+ mSingleThreadExecutor = Executors.newSingleThreadExecutor();
+ }
+
+ /** Returns the single thread executor. */
+ public ExecutorService getSingleThreadExecutor() {
+ return mSingleThreadExecutor;
+ }
+
+ /** Tears down the singleton WorkerExecutor for the application */
+ public void tearDown() {
+ mSingleThreadExecutor.shutdown();
+ sWorkerExecutor = null;
+ }
+}
diff --git a/tests/Android.mk b/tests/Android.mk
new file mode 100644
index 00000000..0903c90c
--- /dev/null
+++ b/tests/Android.mk
@@ -0,0 +1,19 @@
+# Copyright (C) 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+# Include all makefiles in subdirectories
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/tests/checkresources.py b/tests/checkresources.py
new file mode 100755
index 00000000..8469bcac
--- /dev/null
+++ b/tests/checkresources.py
@@ -0,0 +1,22 @@
+#!/usr/bin/python3
+import subprocess
+import sys
+
+process = subprocess.Popen(['lint', '--check', 'UnusedResources', sys.argv[1]],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+
+stdout, stderr = process.communicate()
+
+lines = stdout.decode('utf-8').split('\n')
+results = []
+for i in range(len(lines)-1):
+ if '[UnusedResources]' in lines[i] and 'msgid=' not in lines[i+1]:
+ results.append(lines[i])
+
+if len(results) > 0:
+ print('\n'.join(results))
+ sys.exit(1)
+else:
+ sys.exit(0)
+
diff --git a/tests/robotests/Android.mk b/tests/robotests/Android.mk
new file mode 100644
index 00000000..26134454
--- /dev/null
+++ b/tests/robotests/Android.mk
@@ -0,0 +1,49 @@
+LOCAL_PATH := $(call my-dir)
+#############################################################
+# Car Dialer Robolectric test target. #
+#############################################################
+include $(CLEAR_VARS)
+
+LOCAL_MODULE := CarDialerRoboTests
+LOCAL_MODULE_CLASS := JAVA_LIBRARIES
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_JAVA_RESOURCE_DIRS := config
+
+# Include the testing libraries
+LOCAL_JAVA_LIBRARIES := \
+ android.car \
+ robolectric_android-all-stub \
+ Robolectric_all-target \
+ mockito-robolectric-prebuilt \
+ truth-prebuilt
+
+LOCAL_INSTRUMENTATION_FOR := CarDialerAppForTesting
+
+LOCAL_MODULE_TAGS := optional
+
+# Generate test_config.properties
+include external/robolectric-shadows/gen_test_config.mk
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
+#############################################################
+# Car Dialer runner target to run the previous target. #
+#############################################################
+include $(CLEAR_VARS)
+
+LOCAL_MODULE := RunCarDialerRoboTests
+
+LOCAL_JAVA_LIBRARIES := \
+ android.car \
+ CarDialerRoboTests \
+ robolectric_android-all-stub \
+ Robolectric_all-target \
+ mockito-robolectric-prebuilt \
+ truth-prebuilt
+
+LOCAL_TEST_PACKAGE := CarDialerAppForTesting
+
+LOCAL_INSTRUMENT_SOURCE_DIRS := $(dir $(LOCAL_PATH))../src
+
+include external/robolectric-shadows/run_robotests.mk
diff --git a/tests/robotests/config/robolectric.properties b/tests/robotests/config/robolectric.properties
new file mode 100644
index 00000000..fc4f8ca0
--- /dev/null
+++ b/tests/robotests/config/robolectric.properties
@@ -0,0 +1,14 @@
+# Copyright (C) 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+sdk=NEWEST_SDK
diff --git a/tests/robotests/readme.md b/tests/robotests/readme.md
new file mode 100644
index 00000000..022bcb4b
--- /dev/null
+++ b/tests/robotests/readme.md
@@ -0,0 +1,6 @@
+Unit test suite for CarDialerApp using Robolectric.
+
+```
+$ croot
+$ make RunCarDialerRoboTests -j96
+``` \ No newline at end of file
diff --git a/res/drawable/button_active_state_ring.xml b/tests/robotests/res/layout/test_activity.xml
index 8f717177..d2cd9998 100644
--- a/res/drawable/button_active_state_ring.xml
+++ b/tests/robotests/res/layout/test_activity.xml
@@ -13,10 +13,13 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<shape
+<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
- android:shape="ring"
- android:thickness="4dp"
- android:innerRadius="34dp"
- android:useLevel="false">
-</shape> \ No newline at end of file
+ android:id="@+id/test_fragment_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <Toolbar
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/car_toolbar"/>
+</FrameLayout>
diff --git a/tests/robotests/res/values/arrays.xml b/tests/robotests/res/values/arrays.xml
new file mode 100644
index 00000000..0b925303
--- /dev/null
+++ b/tests/robotests/res/values/arrays.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+ <string-array name="entries" translatable="false">
+ <item>Choose me!</item>
+ <item>No, me!</item>
+ <item>What about me?!</item>
+ </string-array>
+
+ <string-array name="entry_values" translatable="false">
+ <item>alpha</item>
+ <item>beta</item>
+ <item>charlie</item>
+ </string-array>
+</resources>
diff --git a/tests/robotests/res/values/bools.xml b/tests/robotests/res/values/bools.xml
new file mode 100644
index 00000000..6df5100f
--- /dev/null
+++ b/tests/robotests/res/values/bools.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+ <item name="config_enable_dial_motion" type="bool">false</item>
+</resources>
diff --git a/src/com/android/car/dialer/livedata/MissedCallHistoryLiveData.java b/tests/robotests/src/com/android/car/dialer/CarDialerRobolectricTestRunner.java
index 26a7f2ec..92703766 100644
--- a/src/com/android/car/dialer/livedata/MissedCallHistoryLiveData.java
+++ b/tests/robotests/src/com/android/car/dialer/CarDialerRobolectricTestRunner.java
@@ -13,23 +13,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.car.dialer.livedata;
-import android.content.Context;
+package com.android.car.dialer;
-import com.android.car.dialer.telecom.PhoneLoader;
+import org.junit.runners.model.InitializationError;
+import org.robolectric.RobolectricTestRunner;
/**
- * Live data which loads missed call history.
+ * This is kept to avoid too much diverge.
+ * <p>TODO: remove and use RobolectricTestRunner directly.
*/
-public class MissedCallHistoryLiveData extends CallHistoryLiveData {
-
- public MissedCallHistoryLiveData(Context context) {
- super(context);
- }
-
- @Override
- protected int getCallHistoryFilterType() {
- return PhoneLoader.MISSED_TYPE;
+public class CarDialerRobolectricTestRunner extends RobolectricTestRunner {
+ public CarDialerRobolectricTestRunner(Class<?> testClass) throws InitializationError {
+ super(testClass);
}
}
diff --git a/tests/robotests/src/com/android/car/dialer/FragmentTestActivity.java b/tests/robotests/src/com/android/car/dialer/FragmentTestActivity.java
new file mode 100644
index 00000000..fd54794f
--- /dev/null
+++ b/tests/robotests/src/com/android/car/dialer/FragmentTestActivity.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.dialer;
+
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+
+import androidx.annotation.Nullable;
+import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+
+import com.android.car.dialer.ui.common.DialerBaseFragment;
+
+/**
+ * An activity that is used for testing fragments. A unit test starts this activity, adds a fragment
+ * and then tests the fragment.
+ */
+public class FragmentTestActivity extends FragmentActivity implements
+ DialerBaseFragment.DialerFragmentParent {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setTheme(R.style.Theme_Dialer);
+ setContentView(R.layout.test_activity);
+ }
+
+ public void setFragment(Fragment fragment) {
+ getSupportFragmentManager()
+ .beginTransaction()
+ .replace(R.id.test_fragment_container, fragment)
+ .commit();
+ }
+
+ @Override
+ public void setBackground(Drawable background) {
+ // Do nothing
+ }
+
+ @Override
+ public void pushContentFragment(Fragment fragment, String fragmentTag) {
+ getSupportFragmentManager()
+ .beginTransaction()
+ .add(fragment, fragmentTag)
+ .addToBackStack(fragmentTag)
+ .commit();
+ }
+
+ public void showDialog(DialogFragment dialogFragment, @Nullable String tag) {
+ dialogFragment.show(getSupportFragmentManager(), tag);
+ }
+}
diff --git a/tests/robotests/src/com/android/car/dialer/LiveDataObserver.java b/tests/robotests/src/com/android/car/dialer/LiveDataObserver.java
new file mode 100644
index 00000000..b352659b
--- /dev/null
+++ b/tests/robotests/src/com/android/car/dialer/LiveDataObserver.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer;
+
+import androidx.annotation.Nullable;
+import androidx.lifecycle.Observer;
+
+/** Instrumental test observer for live data behavior. */
+public class LiveDataObserver<T> implements Observer {
+ @Override
+ public void onChanged(@Nullable Object value) {
+ // no ops
+ }
+}
diff --git a/tests/robotests/src/com/android/car/dialer/TestDialerApplication.java b/tests/robotests/src/com/android/car/dialer/TestDialerApplication.java
new file mode 100644
index 00000000..21d5406c
--- /dev/null
+++ b/tests/robotests/src/com/android/car/dialer/TestDialerApplication.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.app.Application;
+import android.app.NotificationManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.telecom.CallAudioState;
+
+import com.android.car.dialer.notification.InCallNotificationController;
+import com.android.car.dialer.notification.MissedCallNotificationController;
+import com.android.car.dialer.telecom.InCallServiceImpl;
+import com.android.car.dialer.telecom.UiCallManager;
+
+/** Robolectric runtime application for Dialer. Must be Test + application class name. */
+public class TestDialerApplication extends Application {
+
+ private InCallServiceImpl.LocalBinder mLocalBinder;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ shadowOf(this).setSystemService(
+ Context.NOTIFICATION_SERVICE, mock(NotificationManager.class));
+ InCallNotificationController.init(this);
+ MissedCallNotificationController.init(this);
+
+ mLocalBinder = mock(InCallServiceImpl.LocalBinder.class);
+ shadowOf(this).setComponentNameAndServiceForBindService(
+ new ComponentName(this, InCallServiceImpl.class), mLocalBinder);
+ }
+
+ public void initUiCallManager() {
+ UiCallManager.init(this);
+ }
+
+ public void setupInCallServiceImpl() {
+ InCallServiceImpl inCallService = mock(InCallServiceImpl.class);
+ CallAudioState callAudioState = mock(CallAudioState.class);
+ when(callAudioState.getRoute()).thenReturn(CallAudioState.ROUTE_BLUETOOTH);
+ when(inCallService.getCallAudioState()).thenReturn(callAudioState);
+ when(mLocalBinder.getService()).thenReturn(inCallService);
+ }
+
+ public void setupInCallServiceImpl(InCallServiceImpl inCallServiceImpl) {
+ when(mLocalBinder.getService()).thenReturn(inCallServiceImpl);
+ }
+
+ @Override
+ public void onTerminate() {
+ super.onTerminate();
+ InCallNotificationController.tearDown();
+ MissedCallNotificationController.get().tearDown();
+ }
+
+}
diff --git a/tests/robotests/src/com/android/car/dialer/TestFragment.java b/tests/robotests/src/com/android/car/dialer/TestFragment.java
new file mode 100644
index 00000000..2406a2e8
--- /dev/null
+++ b/tests/robotests/src/com/android/car/dialer/TestFragment.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+
+/** {@link Fragment} used for unit tests. */
+public class TestFragment extends Fragment {
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ View fragmentView = inflater.inflate(R.layout.test_activity, container, false);
+ return fragmentView;
+ }
+
+ public void addChildFragment(Fragment fragment) {
+ getChildFragmentManager().beginTransaction()
+ .replace(R.id.test_fragment_container, fragment)
+ .commitNow();
+ }
+}
diff --git a/tests/robotests/src/com/android/car/dialer/livedata/AudioRouteLiveDataTest.java b/tests/robotests/src/com/android/car/dialer/livedata/AudioRouteLiveDataTest.java
new file mode 100644
index 00000000..7e776d0c
--- /dev/null
+++ b/tests/robotests/src/com/android/car/dialer/livedata/AudioRouteLiveDataTest.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.livedata;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.bluetooth.BluetoothHeadsetClient;
+import android.content.Context;
+import android.content.Intent;
+import android.telecom.CallAudioState;
+
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LifecycleRegistry;
+
+import com.android.car.dialer.CarDialerRobolectricTestRunner;
+import com.android.car.dialer.LiveDataObserver;
+import com.android.car.dialer.telecom.UiCallManager;
+import com.android.car.dialer.testutils.BroadcastReceiverVerifier;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(CarDialerRobolectricTestRunner.class)
+public class AudioRouteLiveDataTest {
+ private static final String INTENT_ACTION = BluetoothHeadsetClient.ACTION_AUDIO_STATE_CHANGED;
+
+ private AudioRouteLiveData mAudioRouteLiveData;
+ private LifecycleRegistry mLifecycleRegistry;
+ private BroadcastReceiverVerifier mReceiverVerifier;
+ @Mock
+ private LifecycleOwner mMockLifecycleOwner;
+ @Mock
+ private LiveDataObserver<Integer> mMockObserver;
+ @Mock
+ private UiCallManager mMockUiCallManager;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ mAudioRouteLiveData = new AudioRouteLiveData(RuntimeEnvironment.application);
+ mLifecycleRegistry = new LifecycleRegistry(mMockLifecycleOwner);
+ when(mMockLifecycleOwner.getLifecycle()).thenReturn(mLifecycleRegistry);
+
+ when(mMockUiCallManager.getAudioRoute()).thenReturn(CallAudioState.ROUTE_EARPIECE);
+ UiCallManager.set(mMockUiCallManager);
+ mReceiverVerifier = new BroadcastReceiverVerifier(RuntimeEnvironment.application);
+ }
+
+ @After
+ public void tearDown() {
+ UiCallManager.set(null);
+ }
+
+ @Test
+ public void testOnActive() {
+ mAudioRouteLiveData.observe(mMockLifecycleOwner, (value) -> mMockObserver.onChanged(value));
+ verify(mMockObserver, never()).onChanged(any());
+
+ mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
+ mReceiverVerifier.verifyReceiverRegistered(INTENT_ACTION);
+ verify(mMockObserver).onChanged(any());
+ }
+
+ @Test
+ public void testOnBluetoothHfpStateChange() {
+ ArgumentCaptor<Integer> valueCaptor = ArgumentCaptor.forClass(Integer.class);
+ doNothing().when(mMockObserver).onChanged(valueCaptor.capture());
+
+ mAudioRouteLiveData.observe(mMockLifecycleOwner, (value) -> mMockObserver.onChanged(value));
+ mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
+ assertThat(mAudioRouteLiveData.getValue()).isEqualTo(CallAudioState.ROUTE_EARPIECE);
+ assertThat(valueCaptor.getValue()).isEqualTo(CallAudioState.ROUTE_EARPIECE);
+
+ when(mMockUiCallManager.getAudioRoute()).thenReturn(CallAudioState.ROUTE_BLUETOOTH);
+ mReceiverVerifier.getBroadcastReceiverFor(INTENT_ACTION)
+ .onReceive(mock(Context.class), mock(Intent.class));
+ assertThat(mAudioRouteLiveData.getValue()).isEqualTo(CallAudioState.ROUTE_BLUETOOTH);
+ assertThat(valueCaptor.getValue()).isEqualTo(CallAudioState.ROUTE_BLUETOOTH);
+ }
+
+ @Test
+ public void testOnInactiveUnregister() {
+ mAudioRouteLiveData.observe(mMockLifecycleOwner, (value) -> mMockObserver.onChanged(value));
+ mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
+ int preNumber = mReceiverVerifier.getReceiverNumber();
+
+ mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY);
+ mReceiverVerifier.verifyReceiverUnregistered(INTENT_ACTION, preNumber);
+ }
+}
diff --git a/tests/robotests/src/com/android/car/dialer/livedata/BluetoothHfpStateLiveDataTest.java b/tests/robotests/src/com/android/car/dialer/livedata/BluetoothHfpStateLiveDataTest.java
new file mode 100644
index 00000000..4648b28e
--- /dev/null
+++ b/tests/robotests/src/com/android/car/dialer/livedata/BluetoothHfpStateLiveDataTest.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.livedata;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothHeadsetClient;
+import android.bluetooth.BluetoothProfile;
+import android.content.Context;
+import android.content.Intent;
+
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LifecycleRegistry;
+
+import com.android.car.dialer.CarDialerRobolectricTestRunner;
+import com.android.car.dialer.LiveDataObserver;
+import com.android.car.dialer.testutils.BroadcastReceiverVerifier;
+import com.android.car.dialer.testutils.ShadowBluetoothAdapterForDialer;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
+
+@RunWith(CarDialerRobolectricTestRunner.class)
+@Config(shadows = ShadowBluetoothAdapterForDialer.class)
+public class BluetoothHfpStateLiveDataTest {
+ private static final String INTENT_ACTION =
+ BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED;
+
+ private BluetoothHfpStateLiveData mBluetoothHfpStateLiveData;
+ private LifecycleRegistry mLifecycleRegistry;
+ private BroadcastReceiverVerifier mReceiverVerifier;
+ @Mock
+ private LifecycleOwner mMockLifecycleOwner;
+ @Mock
+ private LiveDataObserver<Integer> mMockObserver;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+
+ mBluetoothHfpStateLiveData = new BluetoothHfpStateLiveData(RuntimeEnvironment.application);
+ mLifecycleRegistry = new LifecycleRegistry(mMockLifecycleOwner);
+ when(mMockLifecycleOwner.getLifecycle()).thenReturn(mLifecycleRegistry);
+
+ mReceiverVerifier = new BroadcastReceiverVerifier(RuntimeEnvironment.application);
+ }
+
+ @Test
+ public void testOnActive() {
+ mBluetoothHfpStateLiveData.observe(mMockLifecycleOwner,
+ (value) -> mMockObserver.onChanged(value));
+ verify(mMockObserver, never()).onChanged(any());
+
+ mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
+ mReceiverVerifier.verifyReceiverRegistered(INTENT_ACTION);
+ verify(mMockObserver).onChanged(any());
+ }
+
+ @Test
+ public void testOnBluetoothHfpStateChange() {
+ ArgumentCaptor<Integer> valueCaptor = ArgumentCaptor.forClass(Integer.class);
+ doNothing().when(mMockObserver).onChanged(valueCaptor.capture());
+
+ ShadowBluetoothAdapterForDialer shadowBluetoothAdapter =
+ (ShadowBluetoothAdapterForDialer) Shadow.extract(
+ BluetoothAdapter.getDefaultAdapter());
+ shadowBluetoothAdapter.setState(BluetoothAdapter.STATE_ON);
+ shadowBluetoothAdapter.setProfileConnectionState(BluetoothProfile.HEADSET_CLIENT,
+ BluetoothProfile.STATE_CONNECTED);
+
+ mBluetoothHfpStateLiveData.observe(mMockLifecycleOwner,
+ (value) -> mMockObserver.onChanged(value));
+ mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
+ assertThat(BluetoothAdapter.getDefaultAdapter().getProfileConnectionState(
+ BluetoothProfile.HEADSET_CLIENT)).isEqualTo(BluetoothProfile.STATE_CONNECTED);
+ assertThat(mBluetoothHfpStateLiveData.getValue()).isEqualTo(
+ BluetoothProfile.STATE_CONNECTED);
+ assertThat(valueCaptor.getValue()).isEqualTo(BluetoothProfile.STATE_CONNECTED);
+
+ shadowBluetoothAdapter.setProfileConnectionState(BluetoothProfile.HEADSET_CLIENT,
+ BluetoothProfile.STATE_DISCONNECTED);
+ mReceiverVerifier.getBroadcastReceiverFor(INTENT_ACTION)
+ .onReceive(mock(Context.class), mock(Intent.class));
+ assertThat(BluetoothAdapter.getDefaultAdapter().getProfileConnectionState(
+ BluetoothProfile.HEADSET_CLIENT)).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+ assertThat(mBluetoothHfpStateLiveData.getValue()).isEqualTo(
+ BluetoothProfile.STATE_DISCONNECTED);
+ assertThat(valueCaptor.getValue()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+ }
+
+ @Test
+ public void testOnInactiveUnregister() {
+ mBluetoothHfpStateLiveData.observe(mMockLifecycleOwner,
+ (value) -> mMockObserver.onChanged(value));
+ mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
+ int preNumber = mReceiverVerifier.getReceiverNumber();
+
+ mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY);
+ mReceiverVerifier.verifyReceiverUnregistered(INTENT_ACTION, preNumber);
+ }
+}
diff --git a/tests/robotests/src/com/android/car/dialer/livedata/BluetoothPairListLiveDataTest.java b/tests/robotests/src/com/android/car/dialer/livedata/BluetoothPairListLiveDataTest.java
new file mode 100644
index 00000000..a91bff04
--- /dev/null
+++ b/tests/robotests/src/com/android/car/dialer/livedata/BluetoothPairListLiveDataTest.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.livedata;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.content.Context;
+import android.content.Intent;
+
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LifecycleRegistry;
+
+import com.android.car.dialer.CarDialerRobolectricTestRunner;
+import com.android.car.dialer.LiveDataObserver;
+import com.android.car.dialer.testutils.BroadcastReceiverVerifier;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.shadows.ShadowBluetoothAdapter;
+
+import java.util.HashSet;
+import java.util.Set;
+
+@RunWith(CarDialerRobolectricTestRunner.class)
+public class BluetoothPairListLiveDataTest {
+ private static final String INTENT_ACTION = BluetoothDevice.ACTION_BOND_STATE_CHANGED;
+ private static final String BLUETOOTH_DEVICE_ALIAS_1 = "BluetoothDevice 1";
+ private static final String BLUETOOTH_DEVICE_ALIAS_2 = "BluetoothDevice 2";
+
+ private BluetoothPairListLiveData mBluetoothPairListLiveData;
+ private LifecycleRegistry mLifecycleRegistry;
+ private BroadcastReceiverVerifier mReceiverVerifier;
+ @Mock
+ private LifecycleOwner mMockLifecycleOwner;
+ @Mock
+ private LiveDataObserver<Set<BluetoothDevice>> mMockObserver;
+ @Captor
+ private ArgumentCaptor<Set<BluetoothDevice>> mValueCaptor;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+
+ mBluetoothPairListLiveData = new BluetoothPairListLiveData(RuntimeEnvironment.application);
+ mLifecycleRegistry = new LifecycleRegistry(mMockLifecycleOwner);
+ when(mMockLifecycleOwner.getLifecycle()).thenReturn(mLifecycleRegistry);
+
+ mReceiverVerifier = new BroadcastReceiverVerifier(RuntimeEnvironment.application);
+ }
+
+ @Test
+ public void testOnActive() {
+ mBluetoothPairListLiveData.observe(mMockLifecycleOwner,
+ (value) -> mMockObserver.onChanged(value));
+ verify(mMockObserver, never()).onChanged(any());
+
+ mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
+ mReceiverVerifier.verifyReceiverRegistered(INTENT_ACTION);
+ verify(mMockObserver).onChanged(any());
+ }
+
+ @Test
+ public void testOnBluetoothConnected() {
+ // Set up Bluetooth devices
+ BluetoothDevice bluetoothDevice1 = mock(BluetoothDevice.class);
+ bluetoothDevice1.setAlias(BLUETOOTH_DEVICE_ALIAS_1);
+ Set<BluetoothDevice> bondedDevices = new HashSet<BluetoothDevice>();
+ bondedDevices.add(bluetoothDevice1);
+ ShadowBluetoothAdapter shadowBluetoothAdapter = shadowOf(
+ BluetoothAdapter.getDefaultAdapter());
+ shadowBluetoothAdapter.setBondedDevices(bondedDevices);
+
+ doNothing().when(mMockObserver).onChanged(mValueCaptor.capture());
+ mBluetoothPairListLiveData.observe(mMockLifecycleOwner,
+ (value) -> mMockObserver.onChanged(value));
+ mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
+ verifyBondedDevices(bondedDevices);
+
+ // Update Bluetooth devices
+ BluetoothDevice bluetoothDevice2 = mock(BluetoothDevice.class);
+ bluetoothDevice2.setAlias(BLUETOOTH_DEVICE_ALIAS_2);
+ bondedDevices.add(bluetoothDevice2);
+ shadowBluetoothAdapter.setBondedDevices(bondedDevices);
+
+ mReceiverVerifier.getBroadcastReceiverFor(INTENT_ACTION)
+ .onReceive(mock(Context.class), mock(Intent.class));
+ verifyBondedDevices(bondedDevices);
+ }
+
+ @Test
+ public void testOnInactiveUnregister() {
+ mBluetoothPairListLiveData.observe(mMockLifecycleOwner,
+ (value) -> mMockObserver.onChanged(value));
+ mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
+ int preNumber = mReceiverVerifier.getReceiverNumber();
+
+ mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY);
+ mReceiverVerifier.verifyReceiverUnregistered(INTENT_ACTION, preNumber);
+ }
+
+ private void verifyBondedDevices(Set bondedDevices) {
+ // Verify Bonded Devices for BluetoothAdapter
+ assertThat(BluetoothAdapter.getDefaultAdapter().getBondedDevices().containsAll(
+ bondedDevices)).isTrue();
+ assertThat(BluetoothAdapter.getDefaultAdapter().getBondedDevices().size()).isEqualTo(
+ bondedDevices.size());
+ // Verify Bonded Devices for LiveData
+ assertThat(mBluetoothPairListLiveData.getValue().containsAll(bondedDevices)).isTrue();
+ assertThat(mBluetoothPairListLiveData.getValue().size()).isEqualTo(bondedDevices.size());
+ // Verify Bonded Devices for Observer
+ assertThat(mValueCaptor.getValue().containsAll(bondedDevices)).isTrue();
+ assertThat(mValueCaptor.getValue().size()).isEqualTo(bondedDevices.size());
+ }
+}
diff --git a/tests/robotests/src/com/android/car/dialer/livedata/BluetoothStateLiveDataTest.java b/tests/robotests/src/com/android/car/dialer/livedata/BluetoothStateLiveDataTest.java
new file mode 100644
index 00000000..28d4c79b
--- /dev/null
+++ b/tests/robotests/src/com/android/car/dialer/livedata/BluetoothStateLiveDataTest.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.livedata;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.bluetooth.BluetoothAdapter;
+import android.content.Context;
+import android.content.Intent;
+
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LifecycleRegistry;
+
+import com.android.car.dialer.CarDialerRobolectricTestRunner;
+import com.android.car.dialer.LiveDataObserver;
+import com.android.car.dialer.testutils.BroadcastReceiverVerifier;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.shadows.ShadowBluetoothAdapter;
+
+@RunWith(CarDialerRobolectricTestRunner.class)
+public class BluetoothStateLiveDataTest {
+ private static final String INTENT_ACTION = BluetoothAdapter.ACTION_STATE_CHANGED;
+
+ private BluetoothStateLiveData mBluetoothStateLiveData;
+ private LifecycleRegistry mLifecycleRegistry;
+ private BroadcastReceiverVerifier mReceiverVerifier;
+ @Mock
+ private LifecycleOwner mMockLifecycleOwner;
+ @Mock
+ private LiveDataObserver<Integer> mMockObserver;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+
+ mBluetoothStateLiveData = new BluetoothStateLiveData(RuntimeEnvironment.application);
+ mLifecycleRegistry = new LifecycleRegistry(mMockLifecycleOwner);
+ when(mMockLifecycleOwner.getLifecycle()).thenReturn(mLifecycleRegistry);
+
+ mReceiverVerifier = new BroadcastReceiverVerifier(RuntimeEnvironment.application);
+ }
+
+ @Test
+ public void testOnActive() {
+ mBluetoothStateLiveData.observe(mMockLifecycleOwner,
+ (value) -> mMockObserver.onChanged(value));
+ verify(mMockObserver, never()).onChanged(any());
+
+ mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
+ mReceiverVerifier.verifyReceiverRegistered(INTENT_ACTION);
+ verify(mMockObserver).onChanged(any());
+ }
+
+ @Test
+ public void testOnBluetoothAdapterStateChange() {
+ ArgumentCaptor<Integer> valueCaptor = ArgumentCaptor.forClass(Integer.class);
+ doNothing().when(mMockObserver).onChanged(valueCaptor.capture());
+
+ ShadowBluetoothAdapter shadowBluetoothAdapter = shadowOf(
+ BluetoothAdapter.getDefaultAdapter());
+ shadowBluetoothAdapter.setEnabled(false);
+
+ mBluetoothStateLiveData.observe(mMockLifecycleOwner,
+ (value) -> mMockObserver.onChanged(value));
+ mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
+ assertThat(valueCaptor.getValue()).isEqualTo(
+ BluetoothStateLiveData.BluetoothState.DISABLED);
+
+ shadowBluetoothAdapter.setEnabled(true);
+ mReceiverVerifier.getBroadcastReceiverFor(INTENT_ACTION)
+ .onReceive(mock(Context.class), mock(Intent.class));
+ assertThat(mBluetoothStateLiveData.getValue()).isEqualTo(
+ BluetoothStateLiveData.BluetoothState.ENABLED);
+ assertThat(valueCaptor.getValue()).isEqualTo(BluetoothStateLiveData.BluetoothState.ENABLED);
+ }
+
+ @Test
+ public void testOnInactiveUnregister() {
+ mBluetoothStateLiveData.observe(mMockLifecycleOwner,
+ (value) -> mMockObserver.onChanged(value));
+ mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
+ int preNumber = mReceiverVerifier.getReceiverNumber();
+
+ mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY);
+ mReceiverVerifier.verifyReceiverUnregistered(INTENT_ACTION, preNumber);
+ }
+}
diff --git a/tests/robotests/src/com/android/car/dialer/livedata/CallDetailLiveDataTest.java b/tests/robotests/src/com/android/car/dialer/livedata/CallDetailLiveDataTest.java
new file mode 100644
index 00000000..3e3ad7d0
--- /dev/null
+++ b/tests/robotests/src/com/android/car/dialer/livedata/CallDetailLiveDataTest.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.livedata;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.net.Uri;
+import android.telecom.Call;
+import android.telecom.DisconnectCause;
+import android.telecom.GatewayInfo;
+
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LifecycleRegistry;
+
+import com.android.car.dialer.CarDialerRobolectricTestRunner;
+import com.android.car.dialer.LiveDataObserver;
+import com.android.car.telephony.common.CallDetail;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(CarDialerRobolectricTestRunner.class)
+public class CallDetailLiveDataTest {
+
+ private CallDetailLiveData mCallDetailLiveData;
+ private LifecycleRegistry mLifecycleRegistry;
+ @Mock
+ private Call mMockCall;
+ @Mock
+ private LifecycleOwner mMockLifecycleOwner;
+ @Mock
+ private LiveDataObserver<Integer> mMockObserver;
+ @Captor
+ private ArgumentCaptor<Call.Callback> mCallbackCaptor;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+
+ doNothing().when(mMockCall).registerCallback(mCallbackCaptor.capture());
+
+ mCallDetailLiveData = new CallDetailLiveData(mMockCall);
+ mLifecycleRegistry = new LifecycleRegistry(mMockLifecycleOwner);
+ when(mMockLifecycleOwner.getLifecycle()).thenReturn(mLifecycleRegistry);
+ }
+
+ @Test
+ public void testOnActiveRegistry() {
+ mCallDetailLiveData.onActive();
+
+ verify(mMockCall).registerCallback(any());
+ }
+
+ @Test
+ public void testOnLifecycleStart() {
+ mCallDetailLiveData.observe(mMockLifecycleOwner, (value) -> mMockObserver.onChanged(value));
+ verify(mMockObserver, never()).onChanged(any());
+ assertThat(mCallDetailLiveData.hasObservers()).isTrue();
+ assertThat(mCallDetailLiveData.hasActiveObservers()).isFalse();
+
+ mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
+ assertThat(mCallDetailLiveData.hasActiveObservers()).isTrue();
+ verify(mMockObserver).onChanged(any());
+ verify(mMockCall).registerCallback(any());
+ }
+
+ @Test
+ public void testOnDetailsChanged() {
+ ArgumentCaptor<CallDetail> valueCaptor = ArgumentCaptor.forClass(CallDetail.class);
+ doNothing().when(mMockObserver).onChanged(valueCaptor.capture());
+ startObserving();
+
+ // Set up updated details
+ String number = "6505551234";
+ Uri uri = Uri.fromParts("tel", number, null);
+ GatewayInfo gatewayInfo = new GatewayInfo("", uri, uri);
+ CharSequence label = "DisconnectCause";
+ DisconnectCause disconnectCause = new DisconnectCause(1, label, null, "");
+ long connectTimeMillis = 5000;
+ Call.Details updatedDetails = mock(Call.Details.class);
+ when(updatedDetails.getHandle()).thenReturn(uri);
+ when(updatedDetails.getDisconnectCause()).thenReturn(disconnectCause);
+ when(updatedDetails.getGatewayInfo()).thenReturn(gatewayInfo);
+ when(updatedDetails.getConnectTimeMillis()).thenReturn(connectTimeMillis);
+
+ when(mMockCall.getDetails()).thenReturn(updatedDetails);
+ mCallbackCaptor.getValue().onDetailsChanged(mMockCall, updatedDetails);
+
+ CallDetail observedValue = valueCaptor.getValue();
+ assertThat(observedValue.getNumber()).isEqualTo(number);
+ assertThat(observedValue.getConnectTimeMillis()).isEqualTo(connectTimeMillis);
+ assertThat(observedValue.getDisconnectCause()).isEqualTo(label);
+ assertThat(observedValue.getGatewayInfoOriginalAddress()).isEqualTo(uri);
+ }
+
+ @Test
+ public void testOnInactiveUnregister() {
+ startObserving();
+ mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY);
+
+ verify(mMockCall).unregisterCallback(mCallbackCaptor.getValue());
+ assertThat(mCallDetailLiveData.hasObservers()).isFalse();
+ assertThat(mCallDetailLiveData.hasActiveObservers()).isFalse();
+ }
+
+ private void startObserving() {
+ mCallDetailLiveData.observe(mMockLifecycleOwner, (value) -> mMockObserver.onChanged(value));
+ mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
+ }
+}
diff --git a/tests/robotests/src/com/android/car/dialer/livedata/CallStateLiveDataTest.java b/tests/robotests/src/com/android/car/dialer/livedata/CallStateLiveDataTest.java
new file mode 100644
index 00000000..b9e4c287
--- /dev/null
+++ b/tests/robotests/src/com/android/car/dialer/livedata/CallStateLiveDataTest.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.livedata;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.telecom.Call;
+
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LifecycleRegistry;
+
+import com.android.car.dialer.CarDialerRobolectricTestRunner;
+import com.android.car.dialer.LiveDataObserver;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(CarDialerRobolectricTestRunner.class)
+public class CallStateLiveDataTest {
+
+ private CallStateLiveData mCallStateLiveData;
+ private LifecycleRegistry mLifecycleRegistry;
+ @Mock
+ private Call mMockCall;
+ @Mock
+ private LifecycleOwner mMockLifecycleOwner;
+ @Mock
+ private LiveDataObserver<Integer> mMockObserver;
+ @Captor
+ private ArgumentCaptor<Call.Callback> mCallbackCaptor;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+
+ doNothing().when(mMockCall).registerCallback(mCallbackCaptor.capture());
+ when(mMockCall.getState()).thenReturn(Call.STATE_NEW);
+
+ mCallStateLiveData = new CallStateLiveData(mMockCall);
+ mLifecycleRegistry = new LifecycleRegistry(mMockLifecycleOwner);
+ when(mMockLifecycleOwner.getLifecycle()).thenReturn(mLifecycleRegistry);
+ }
+
+ @Test
+ public void testOnActiveRegistry() {
+ mCallStateLiveData.onActive();
+
+ verify(mMockCall).registerCallback(any());
+ }
+
+ @Test
+ public void testOnLifecycleStart() {
+ mCallStateLiveData.observe(mMockLifecycleOwner, (value) -> mMockObserver.onChanged(value));
+ verify(mMockObserver, never()).onChanged(any());
+ assertThat(mCallStateLiveData.hasObservers()).isTrue();
+ assertThat(mCallStateLiveData.hasActiveObservers()).isFalse();
+
+ mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
+ verify(mMockObserver).onChanged(any());
+ assertThat(mCallStateLiveData.hasActiveObservers()).isTrue();
+ verify(mMockCall).registerCallback(any());
+ }
+
+ @Test
+ public void testOnStateChanged() {
+ ArgumentCaptor<Integer> valueCaptor = ArgumentCaptor.forClass(Integer.class);
+ doNothing().when(mMockObserver).onChanged(valueCaptor.capture());
+
+ mCallStateLiveData.observe(mMockLifecycleOwner, (value) -> mMockObserver.onChanged(value));
+ mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
+
+ assertThat(valueCaptor.getValue()).isEqualTo(Call.STATE_NEW);
+ mCallbackCaptor.getValue().onStateChanged(mMockCall, Call.STATE_ACTIVE);
+ assertThat(valueCaptor.getValue()).isEqualTo(Call.STATE_ACTIVE);
+ }
+
+ @Test
+ public void testOnInactiveUnregister() {
+ mCallStateLiveData.observe(mMockLifecycleOwner, (value) -> mMockObserver.onChanged(value));
+ mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
+
+ mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY);
+
+ verify(mMockCall).unregisterCallback(mCallbackCaptor.getValue());
+ assertThat(mCallStateLiveData.hasObservers()).isFalse();
+ assertThat(mCallStateLiveData.hasActiveObservers()).isFalse();
+ }
+}
diff --git a/tests/robotests/src/com/android/car/dialer/livedata/HeartBeatLiveDataTest.java b/tests/robotests/src/com/android/car/dialer/livedata/HeartBeatLiveDataTest.java
new file mode 100644
index 00000000..d5c421e4
--- /dev/null
+++ b/tests/robotests/src/com/android/car/dialer/livedata/HeartBeatLiveDataTest.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.dialer.livedata;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.text.format.DateUtils;
+
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LifecycleRegistry;
+
+import com.android.car.dialer.LiveDataObserver;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.shadows.ShadowLooper;
+
+@RunWith(RobolectricTestRunner.class)
+public class HeartBeatLiveDataTest {
+
+ private HeartBeatLiveData mHeartBeatLiveData;
+
+ private LifecycleRegistry mLifecycleRegistry;
+ private LifecycleOwner mLifecycleOwner;
+
+ @Before
+ public void setup() {
+ mHeartBeatLiveData = new HeartBeatLiveData(DateUtils.SECOND_IN_MILLIS);
+ mLifecycleOwner = mock(LifecycleOwner.class);
+ mLifecycleRegistry = new LifecycleRegistry(mLifecycleOwner);
+ when(mLifecycleOwner.getLifecycle()).thenReturn(mLifecycleRegistry);
+ }
+
+ @Test
+ public void active_onLifecycleStart() {
+ LiveDataObserver<HeartBeatLiveData> mockObserver = mock(LiveDataObserver.class);
+ mHeartBeatLiveData.observe(mLifecycleOwner, (value) -> mockObserver.onChanged(value));
+ verify(mockObserver, never()).onChanged(any());
+
+ mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
+ ShadowLooper.runUiThreadTasks();
+
+ verify(mockObserver).onChanged(any());
+ }
+}
diff --git a/tests/robotests/src/com/android/car/dialer/telecom/InCallServiceImplTest.java b/tests/robotests/src/com/android/car/dialer/telecom/InCallServiceImplTest.java
new file mode 100644
index 00000000..e0d8a489
--- /dev/null
+++ b/tests/robotests/src/com/android/car/dialer/telecom/InCallServiceImplTest.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.telecom;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.car.Car;
+import android.car.CarProjectionManager;
+import android.content.Context;
+import android.content.Intent;
+import android.telecom.Call;
+
+import com.android.car.dialer.CarDialerRobolectricTestRunner;
+import com.android.car.dialer.testutils.ShadowCar;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.Robolectric;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.android.controller.ServiceController;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowContextWrapper;
+import org.robolectric.shadows.ShadowLooper;
+
+/**
+ * Tests for {@link InCallServiceImpl}.
+ */
+@Config(shadows = {ShadowCar.class})
+@RunWith(CarDialerRobolectricTestRunner.class)
+public class InCallServiceImplTest {
+ private static final String TELECOM_CALL_ID = "TC@1234";
+
+ private InCallServiceImpl mInCallServiceImpl;
+ private Context mContext;
+
+ @Mock
+ Car mCar;
+ @Mock
+ CarProjectionManager mCarProjectionManager;
+ @Mock
+ private Call mMockTelecomCall;
+ @Mock
+ private Call.Details mMockCallDetails;
+ @Mock
+ private InCallServiceImpl.Callback mCallback;
+ @Mock
+ private InCallServiceImpl.ActiveCallListChangedCallback mActiveCallListChangedCallback;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ mContext = RuntimeEnvironment.application;
+
+ when(mCar.getCarManager(Car.PROJECTION_SERVICE)).thenReturn(mCarProjectionManager);
+ ShadowCar.setCar(mCar);
+
+ ServiceController<InCallServiceImpl> inCallServiceController =
+ Robolectric.buildService(InCallServiceImpl.class);
+ inCallServiceController.create().bind();
+ mInCallServiceImpl = inCallServiceController.get();
+
+ mInCallServiceImpl.registerCallback(mCallback);
+ mInCallServiceImpl.addActiveCallListChangedCallback(mActiveCallListChangedCallback);
+ ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
+
+ when(mMockTelecomCall.getDetails()).thenReturn(mMockCallDetails);
+ when(mMockCallDetails.getTelecomCallId()).thenReturn(TELECOM_CALL_ID);
+ }
+
+ @Test
+ public void onActiveCallAdded_startInCallActivity() {
+ when(mMockTelecomCall.getState()).thenReturn(Call.STATE_ACTIVE);
+ mInCallServiceImpl.onCallAdded(mMockTelecomCall);
+
+ ArgumentCaptor<Call> callCaptor = ArgumentCaptor.forClass(Call.class);
+ verify(mCallback).onTelecomCallAdded(callCaptor.capture());
+ assertThat(callCaptor.getValue()).isEqualTo(mMockTelecomCall);
+
+ verify(mActiveCallListChangedCallback).onTelecomCallAdded(callCaptor.capture());
+ assertThat(callCaptor.getValue()).isEqualTo(mMockTelecomCall);
+
+ ShadowContextWrapper shadowContextWrapper = shadowOf(RuntimeEnvironment.application);
+ Intent intent = shadowContextWrapper.getNextStartedActivity();
+ assertThat(intent).isNotNull();
+ }
+
+ @Test
+ public void onCallRemoved() {
+ when(mMockTelecomCall.getState()).thenReturn(Call.STATE_ACTIVE);
+ mInCallServiceImpl.onCallRemoved(mMockTelecomCall);
+
+ ArgumentCaptor<Call> callCaptor = ArgumentCaptor.forClass(Call.class);
+ verify(mCallback).onTelecomCallRemoved(callCaptor.capture());
+ assertThat(callCaptor.getValue()).isEqualTo(mMockTelecomCall);
+
+ verify(mActiveCallListChangedCallback).onTelecomCallRemoved(callCaptor.capture());
+ assertThat(callCaptor.getValue()).isEqualTo(mMockTelecomCall);
+ }
+
+ @Test
+ public void onRingingCallAdded_showNotification() {
+ when(mMockTelecomCall.getState()).thenReturn(Call.STATE_RINGING);
+ mInCallServiceImpl.onCallAdded(mMockTelecomCall);
+
+ ArgumentCaptor<Call> callCaptor = ArgumentCaptor.forClass(Call.class);
+ verify(mCallback).onTelecomCallAdded(callCaptor.capture());
+ assertThat(callCaptor.getValue()).isEqualTo(mMockTelecomCall);
+
+ verify(mActiveCallListChangedCallback).onTelecomCallAdded(callCaptor.capture());
+ assertThat(callCaptor.getValue()).isEqualTo(mMockTelecomCall);
+
+ ArgumentCaptor<Call.Callback> callbackListCaptor = ArgumentCaptor.forClass(
+ Call.Callback.class);
+ verify(mMockTelecomCall).registerCallback(callbackListCaptor.capture());
+
+ NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(
+ Context.NOTIFICATION_SERVICE);
+ verify(notificationManager).notify(eq(TELECOM_CALL_ID), anyInt(), any(Notification.class));
+ }
+
+ @Test
+ public void testUnregisterCallback() {
+ mInCallServiceImpl.unregisterCallback(mCallback);
+
+ mInCallServiceImpl.onCallAdded(mMockTelecomCall);
+ verify(mCallback, never()).onTelecomCallAdded(any());
+
+ mInCallServiceImpl.onCallRemoved(mMockTelecomCall);
+ verify(mCallback, never()).onTelecomCallRemoved(any());
+ }
+
+ @Test
+ public void testRemoveActiveCallListChangedCallback() {
+ mInCallServiceImpl.removeActiveCallListChangedCallback(mActiveCallListChangedCallback);
+
+ mInCallServiceImpl.onCallAdded(mMockTelecomCall);
+ verify(mActiveCallListChangedCallback, never()).onTelecomCallAdded(any());
+
+ mInCallServiceImpl.onCallRemoved(mMockTelecomCall);
+ verify(mActiveCallListChangedCallback, never()).onTelecomCallRemoved(any());
+ }
+}
diff --git a/tests/robotests/src/com/android/car/dialer/telecom/ProjectionCallHandlerTest.java b/tests/robotests/src/com/android/car/dialer/telecom/ProjectionCallHandlerTest.java
new file mode 100644
index 00000000..293c83e9
--- /dev/null
+++ b/tests/robotests/src/com/android/car/dialer/telecom/ProjectionCallHandlerTest.java
@@ -0,0 +1,315 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.dialer.telecom;
+
+import static com.android.car.dialer.telecom.ProjectionCallHandler.HFP_CLIENT_SCHEME;
+import static com.android.car.dialer.telecom.ProjectionCallHandler.PROJECTION_STATUS_EXTRA_DEVICE_STATE;
+import static com.android.car.dialer.telecom.ProjectionCallHandler.PROJECTION_STATUS_EXTRA_HANDLES_PHONE_UI;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.annotation.Nullable;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.car.CarProjectionManager;
+import android.car.projection.ProjectionStatus;
+import android.content.ComponentName;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.telecom.Call;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.Collections;
+
+@RunWith(RobolectricTestRunner.class)
+public class ProjectionCallHandlerTest {
+ private static final String HFP_ADDRESS = "00:11:22:33:44:55";
+ private static final String NON_HFP_ADDRESS = "AA:BB:CC:DD:EE:FF";
+
+ private static PhoneAccount.Builder phoneAccountBuilder(String name) {
+ return PhoneAccount.builder(
+ new PhoneAccountHandle(
+ ComponentName.unflattenFromString("package.name/.class"),
+ name),
+ name);
+ }
+
+ private final PhoneAccount mHfpAccount = phoneAccountBuilder("HFP")
+ .setAddress(Uri.fromParts(HFP_CLIENT_SCHEME, HFP_ADDRESS, null))
+ .build();
+
+ private final PhoneAccount mNonHfpAccount = phoneAccountBuilder("Non-HFP")
+ .setAddress(Uri.fromParts("tel", "1234567890", null))
+ .build();
+
+ private final PhoneAccount mAccountWithNoAddress = phoneAccountBuilder("No Address").build();
+
+ private Context mContext;
+ private TelecomManager mTelecomManager;
+ private CarProjectionManager mCarProjectionManager;
+
+ private ProjectionCallHandler mProjectionCallHandler;
+
+ @Before
+ public void setUp() {
+ mContext = RuntimeEnvironment.application;
+ mTelecomManager = spy(mContext.getSystemService(TelecomManager.class));
+ mCarProjectionManager = mock(CarProjectionManager.class);
+
+ when(mTelecomManager.isInEmergencyCall()).thenReturn(false);
+
+ mTelecomManager.registerPhoneAccount(mHfpAccount);
+ mTelecomManager.registerPhoneAccount(mNonHfpAccount);
+ mTelecomManager.registerPhoneAccount(mAccountWithNoAddress);
+
+ mProjectionCallHandler = new ProjectionCallHandler(mTelecomManager, mCarProjectionManager);
+ }
+
+ @Test
+ public void startAndStop_registerAndUnregisterProjectionStatusListener() {
+ mProjectionCallHandler.start();
+ verify(mCarProjectionManager).registerProjectionStatusListener(mProjectionCallHandler);
+
+ mProjectionCallHandler.stop();
+ verify(mCarProjectionManager).unregisterProjectionStatusListener(mProjectionCallHandler);
+ }
+
+ @Test
+ public void noProjectionApps_doesNotSuppress() {
+ assertThat(shouldSuppressCallFor(mHfpAccount)).isFalse();
+ }
+
+ @Test
+ public void projectionApp_inactive_doesNotSuppress() {
+ sendProjectionStatus(
+ projectionStatusBuilder(ProjectionStatus.PROJECTION_STATE_INACTIVE)
+ .build());
+
+ assertThat(shouldSuppressCallFor(mHfpAccount)).isFalse();
+ }
+
+ @Test
+ public void projectionApp_readyToProject_doesNotSuppress() {
+ sendProjectionStatus(
+ projectionStatusBuilder(ProjectionStatus.PROJECTION_STATE_READY_TO_PROJECT)
+ .build());
+
+ assertThat(shouldSuppressCallFor(mHfpAccount)).isFalse();
+ }
+
+ @Test
+ public void activeApp_noProjectingDevices_doesNotSuppress() {
+ sendProjectionStatus(
+ projectionStatusBuilder(ProjectionStatus.PROJECTION_STATE_ACTIVE_FOREGROUND)
+ .addMobileDevice(
+ mobileDeviceBuilder().setProjecting(false).build())
+ .build());
+
+ assertThat(shouldSuppressCallFor(mHfpAccount)).isFalse();
+ }
+
+ @Test
+ public void activeApp_withProjectingDevice_suppresses() {
+ sendProjectionStatus(
+ projectionStatusBuilder(ProjectionStatus.PROJECTION_STATE_ACTIVE_FOREGROUND)
+ .addMobileDevice(
+ mobileDeviceBuilder().setProjecting(true).build())
+ .build());
+
+ assertThat(shouldSuppressCallFor(mHfpAccount)).isTrue();
+ }
+
+ @Test
+ public void activeApp_thatHandlesPhoneUi_suppresses() {
+ Bundle extras = new Bundle();
+ extras.putBoolean(PROJECTION_STATUS_EXTRA_HANDLES_PHONE_UI, true);
+
+ sendProjectionStatus(suppressableStatusBuilder().setExtras(extras).build());
+
+ assertThat(shouldSuppressCallFor(mHfpAccount)).isTrue();
+ }
+
+ @Test
+ public void activeApp_thatDoesNotHandlePhoneUi_doesNotSuppress() {
+ Bundle extras = new Bundle();
+ extras.putBoolean(PROJECTION_STATUS_EXTRA_HANDLES_PHONE_UI, false);
+
+ sendProjectionStatus(suppressableStatusBuilder().setExtras(extras).build());
+
+ assertThat(shouldSuppressCallFor(mHfpAccount)).isFalse();
+ }
+
+ @Test
+ public void activeApp_withProjectingDevice_withBackgroundStateExtra_doesNotSuppress() {
+ Bundle deviceExtras = new Bundle();
+ deviceExtras.putInt(
+ PROJECTION_STATUS_EXTRA_DEVICE_STATE,
+ ProjectionStatus.PROJECTION_STATE_ACTIVE_BACKGROUND);
+
+ sendProjectionStatus(suppressableStatus(deviceExtras));
+
+ assertThat(shouldSuppressCallFor(mHfpAccount)).isFalse();
+ }
+
+ @Test
+ public void activeApp_withProjectingDevice_withForegroundStateExtra_suppresses() {
+ Bundle deviceExtras = new Bundle();
+ deviceExtras.putInt(
+ PROJECTION_STATUS_EXTRA_DEVICE_STATE,
+ ProjectionStatus.PROJECTION_STATE_ACTIVE_FOREGROUND);
+
+ sendProjectionStatus(suppressableStatus(deviceExtras));
+
+ assertThat(shouldSuppressCallFor(mHfpAccount)).isTrue();
+ }
+
+ @Test
+ public void activeApp_withInvalidBluetoothDeviceExtra_suppresses() {
+ Bundle deviceExtras = new Bundle();
+ deviceExtras.putParcelable(BluetoothDevice.EXTRA_DEVICE, mock(Parcelable.class));
+
+ sendProjectionStatus(suppressableStatus(deviceExtras));
+
+ assertThat(shouldSuppressCallFor(mHfpAccount)).isTrue();
+ }
+
+ @Test
+ public void activeApp_withNonMatchingBluetoothDeviceExtra_doesNotSuppress() {
+ Bundle deviceExtras = new Bundle();
+ deviceExtras.putParcelable(
+ BluetoothDevice.EXTRA_DEVICE,
+ BluetoothAdapter.getDefaultAdapter().getRemoteDevice(NON_HFP_ADDRESS));
+
+ sendProjectionStatus(suppressableStatus(deviceExtras));
+
+ assertThat(shouldSuppressCallFor(mHfpAccount)).isFalse();
+ }
+
+ @Test
+ public void activeApp_withMatchingBluetoothDeviceExtra_suppresses() {
+ Bundle deviceExtras = new Bundle();
+ deviceExtras.putParcelable(
+ BluetoothDevice.EXTRA_DEVICE,
+ BluetoothAdapter.getDefaultAdapter().getRemoteDevice(HFP_ADDRESS));
+
+ sendProjectionStatus(suppressableStatus(deviceExtras));
+
+ assertThat(shouldSuppressCallFor(mHfpAccount)).isTrue();
+ }
+
+ @Test
+ public void emergencyCall_isNotSuppressed() {
+ sendProjectionStatus(suppressableStatus());
+
+ when(mTelecomManager.isInEmergencyCall()).thenReturn(true);
+
+ assertThat(shouldSuppressCallFor(mHfpAccount)).isFalse();
+ }
+
+ @Test
+ public void call_fromTelAccount_isNotSuppressed() {
+ sendProjectionStatus(suppressableStatus());
+
+ assertThat(shouldSuppressCallFor(mNonHfpAccount)).isFalse();
+ }
+
+ @Test
+ public void call_fromAccount_withNoAddress_isNotSuppressed() {
+ sendProjectionStatus(suppressableStatus());
+
+ assertThat(shouldSuppressCallFor(mAccountWithNoAddress)).isFalse();
+ }
+
+ @Test
+ public void call_fromAccount_withInvalidPhoneAccountHandle_isNotSuppressed() {
+ sendProjectionStatus(suppressableStatus());
+ mTelecomManager.unregisterPhoneAccount(mHfpAccount.getAccountHandle());
+
+ assertThat(shouldSuppressCallFor(mHfpAccount)).isFalse();
+ }
+
+ @Test
+ public void call_withNullDetails_isNotSuppressed() {
+ sendProjectionStatus(suppressableStatus());
+
+ Call call = createCall(mHfpAccount);
+ when(call.getDetails()).thenReturn(null);
+
+ assertThat(mProjectionCallHandler.onTelecomCallAdded(call)).isFalse();
+ }
+
+ private Call createCall(PhoneAccount account) {
+ Call.Details details = mock(Call.Details.class);
+ when(details.getAccountHandle()).thenReturn(account.getAccountHandle());
+
+ Call call = mock(Call.class);
+ when(call.getDetails()).thenReturn(details);
+
+ return call;
+ }
+
+ private boolean shouldSuppressCallFor(PhoneAccount account) {
+ return mProjectionCallHandler.onTelecomCallAdded(createCall(account));
+ }
+
+ private ProjectionStatus.Builder projectionStatusBuilder(int state) {
+ return ProjectionStatus.builder(mContext.getPackageName(), state);
+ }
+
+ private ProjectionStatus.MobileDevice.Builder mobileDeviceBuilder() {
+ return ProjectionStatus.MobileDevice.builder(0, "device");
+ }
+
+ private ProjectionStatus suppressableStatus() {
+ return suppressableStatus(null);
+ }
+
+ private ProjectionStatus suppressableStatus(@Nullable Bundle deviceExtras) {
+ return suppressableStatusBuilder(deviceExtras).build();
+ }
+
+ private ProjectionStatus.Builder suppressableStatusBuilder() {
+ return suppressableStatusBuilder(null);
+ }
+
+ private ProjectionStatus.Builder suppressableStatusBuilder(@Nullable Bundle deviceExtras) {
+ return projectionStatusBuilder(ProjectionStatus.PROJECTION_STATE_ACTIVE_FOREGROUND)
+ .addMobileDevice(
+ mobileDeviceBuilder().setProjecting(true).setExtras(deviceExtras).build());
+ }
+
+ private void sendProjectionStatus(ProjectionStatus status) {
+ mProjectionCallHandler.onProjectionStatusChanged(
+ status.getState(),
+ status.getPackageName(),
+ Collections.singletonList(status));
+ }
+}
diff --git a/tests/robotests/src/com/android/car/dialer/telecom/UiBluetoothMonitorTest.java b/tests/robotests/src/com/android/car/dialer/telecom/UiBluetoothMonitorTest.java
new file mode 100644
index 00000000..9d469505
--- /dev/null
+++ b/tests/robotests/src/com/android/car/dialer/telecom/UiBluetoothMonitorTest.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.telecom;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+import android.content.Context;
+
+import com.android.car.dialer.CarDialerRobolectricTestRunner;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(CarDialerRobolectricTestRunner.class)
+public class UiBluetoothMonitorTest {
+
+ private Context mContext;
+ private UiBluetoothMonitor mUiBluetoothMonitor;
+
+ @Before
+ public void setup() {
+ mContext = RuntimeEnvironment.application;
+ mUiBluetoothMonitor = UiBluetoothMonitor.init(mContext);
+ }
+
+ @Test
+ public void testInit_initTwice_ThrowException() {
+ assertNotNull(mUiBluetoothMonitor);
+
+ try {
+ UiBluetoothMonitor.init(mContext);
+ fail();
+ } catch (IllegalStateException e) {
+ // This is expected.
+ }
+ }
+
+ @Test
+ public void testGet() {
+ assertThat(UiBluetoothMonitor.get()).isEqualTo(mUiBluetoothMonitor);
+ }
+
+ @After
+ public void tearDown() {
+ mUiBluetoothMonitor.tearDown();
+ }
+}
diff --git a/tests/robotests/src/com/android/car/dialer/telecom/UiCallManagerTest.java b/tests/robotests/src/com/android/car/dialer/telecom/UiCallManagerTest.java
new file mode 100644
index 00000000..ea5e3153
--- /dev/null
+++ b/tests/robotests/src/com/android/car/dialer/telecom/UiCallManagerTest.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.telecom;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+import static org.mockito.Matchers.isNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.Application;
+import android.content.ComponentName;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Bundle;
+import android.telecom.CallAudioState;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+
+import com.android.car.dialer.CarDialerRobolectricTestRunner;
+import com.android.car.dialer.R;
+import com.android.car.dialer.TestDialerApplication;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.ShadowContextImpl;
+import org.robolectric.shadows.ShadowToast;
+
+import java.util.List;
+
+@RunWith(CarDialerRobolectricTestRunner.class)
+public class UiCallManagerTest {
+
+ private static final String TEL_SCHEME = "tel";
+
+ private Context mContext;
+ private UiCallManager mUiCallManager;
+ @Mock
+ private TelecomManager mMockTelecomManager;
+ @Mock
+ private InCallServiceImpl mMockInCallService;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+
+ mContext = RuntimeEnvironment.application;
+
+ ShadowContextImpl shadowContext = Shadow.extract(((Application) mContext).getBaseContext());
+ shadowContext.setSystemService(Context.TELECOM_SERVICE, mMockTelecomManager);
+ }
+
+ private void initUiCallManager() {
+ ((TestDialerApplication) mContext).setupInCallServiceImpl(mMockInCallService);
+ ((TestDialerApplication) mContext).initUiCallManager();
+
+ mUiCallManager = UiCallManager.get();
+ }
+
+ private void initUiCallManager_InCallServiceIsNull() {
+ ((TestDialerApplication) mContext).setupInCallServiceImpl(null);
+ ((TestDialerApplication) mContext).initUiCallManager();
+
+ mUiCallManager = UiCallManager.get();
+ }
+
+ @Test
+ public void testInit_initTwice_ThrowException() {
+ initUiCallManager();
+
+ assertNotNull(mUiCallManager);
+
+ try {
+ UiCallManager.init(mContext);
+ fail();
+ } catch (IllegalStateException e) {
+ // This is expected.
+ }
+ }
+
+ @Test
+ public void testPlaceCall() {
+ initUiCallManager();
+
+ String[] phoneNumbers = {
+ "6505551234", // US Number
+ "511", // Special number
+ "911", // Emergency number
+ "122", // Emergency number
+ "#77" // Emergency number
+ };
+
+ for (int i = 0; i < phoneNumbers.length; i++) {
+ checkPlaceCall(phoneNumbers[i], i + 1);
+ }
+ }
+
+ private void checkPlaceCall(String phoneNumber, int timesCalled) {
+ ArgumentCaptor<Uri> uriCaptor = ArgumentCaptor.forClass(Uri.class);
+
+ assertThat(mUiCallManager.placeCall(phoneNumber)).isTrue();
+ verify(mMockTelecomManager, times(timesCalled)).placeCall(uriCaptor.capture(),
+ (Bundle) isNull());
+ assertThat(uriCaptor.getValue().getScheme()).isEqualTo(TEL_SCHEME);
+ assertThat(uriCaptor.getValue().getSchemeSpecificPart()).isEqualTo(phoneNumber);
+ assertThat(uriCaptor.getValue().getFragment()).isNull();
+ }
+
+ @Test
+ public void testPlaceCall_invalidNumber() {
+ initUiCallManager();
+ String[] phoneNumbers = {
+ "xxxxx",
+ "51f"
+ };
+
+ for (String phoneNumber : phoneNumbers) {
+ checkPlaceCallForInvalidNumber(phoneNumber);
+ }
+ }
+
+ private void checkPlaceCallForInvalidNumber(String phoneNumber) {
+ ArgumentCaptor<Uri> uriCaptor = ArgumentCaptor.forClass(Uri.class);
+
+ assertThat(mUiCallManager.placeCall(phoneNumber)).isFalse();
+ verify(mMockTelecomManager, never()).placeCall(uriCaptor.capture(), isNull());
+
+ assertThat(ShadowToast.getTextOfLatestToast()).isEqualTo(
+ mContext.getString(R.string.error_invalid_phone_number));
+ }
+
+ @Test
+ public void testGetMuted_isMuted() {
+ initUiCallManager();
+
+ CallAudioState callAudioState = new CallAudioState(true,
+ CallAudioState.ROUTE_BLUETOOTH, CallAudioState.ROUTE_ALL);
+ when(mMockInCallService.getCallAudioState()).thenReturn(callAudioState);
+
+ assertThat(mUiCallManager.getMuted()).isTrue();
+ }
+
+ @Test
+ public void testGetMuted_audioRouteIsNull() {
+ initUiCallManager();
+
+ when(mMockInCallService.getCallAudioState()).thenReturn(null);
+
+ assertThat(mUiCallManager.getMuted()).isFalse();
+ }
+
+ @Test
+ public void testGetMuted_InCallServiceIsNull() {
+ initUiCallManager_InCallServiceIsNull();
+
+ assertThat(mUiCallManager.getMuted()).isFalse();
+ }
+
+ @Test
+ public void testSetMuted() {
+ initUiCallManager();
+
+ mUiCallManager.setMuted(true);
+
+ ArgumentCaptor<Boolean> captor = ArgumentCaptor.forClass(Boolean.class);
+ verify(mMockInCallService).setMuted(captor.capture());
+ assertThat(captor.getValue()).isTrue();
+ }
+
+ @Test
+ public void testGetSupportedAudioRouteMask() {
+ initUiCallManager();
+
+ CallAudioState callAudioState = new CallAudioState(
+ true, CallAudioState.ROUTE_BLUETOOTH, CallAudioState.ROUTE_ALL);
+ when(mMockInCallService.getCallAudioState()).thenReturn(callAudioState);
+
+ assertThat(mUiCallManager.getSupportedAudioRouteMask()).isEqualTo(CallAudioState.ROUTE_ALL);
+ }
+
+ @Test
+ public void testGetSupportedAudioRouteMask_InCallServiceIsNull() {
+ initUiCallManager_InCallServiceIsNull();
+
+ assertThat(mUiCallManager.getSupportedAudioRouteMask()).isEqualTo(0);
+ }
+
+ @Test
+ public void testGetSupportedAudioRoute_isBluetoothCall() {
+ initUiCallManager();
+
+ PhoneAccountHandle mockPhoneAccountHandle = mock(PhoneAccountHandle.class);
+ ComponentName mockComponentName = mock(ComponentName.class);
+ when(mockComponentName.getClassName()).thenReturn(
+ UiCallManager.HFP_CLIENT_CONNECTION_SERVICE_CLASS_NAME);
+ when(mockPhoneAccountHandle.getComponentName()).thenReturn(mockComponentName);
+ when(mMockTelecomManager.getUserSelectedOutgoingPhoneAccount())
+ .thenReturn(mockPhoneAccountHandle);
+
+ assertThat(mUiCallManager.isBluetoothCall()).isTrue();
+ List<Integer> supportedAudioRoute = mUiCallManager.getSupportedAudioRoute();
+ assertThat(supportedAudioRoute.get(0)).isEqualTo(CallAudioState.ROUTE_BLUETOOTH);
+ assertThat(supportedAudioRoute.get(1)).isEqualTo(CallAudioState.ROUTE_EARPIECE);
+ }
+
+ @Test
+ public void testGetSupportedAudioRoute_supportedAudioRouteMaskIs0() {
+ initUiCallManager();
+
+ // SupportedAudioRouteMask is 0.
+ assertThat(mUiCallManager.isBluetoothCall()).isFalse();
+ assertThat(mUiCallManager.getSupportedAudioRoute().size()).isEqualTo(0);
+ }
+
+ @Test
+ public void testGetSupportedAudioRoute_supportedAudioRouteMaskIsRouteAll() {
+ initUiCallManager();
+
+ // SupportedAudioRouteMask is CallAudioState.ROUTE_ALL.
+ CallAudioState callAudioState = new CallAudioState(
+ true, CallAudioState.ROUTE_BLUETOOTH, CallAudioState.ROUTE_ALL);
+ when(mMockInCallService.getCallAudioState()).thenReturn(callAudioState);
+
+ assertThat(mUiCallManager.isBluetoothCall()).isFalse();
+ assertThat(mUiCallManager.getSupportedAudioRoute().size()).isEqualTo(1);
+ assertThat(mUiCallManager.getSupportedAudioRoute().get(0))
+ .isEqualTo(CallAudioState.ROUTE_EARPIECE);
+ }
+
+ @Test
+ public void testGetSupportedAudioRoute_supportedAudioRouteMaskIsRouteSpeaker() {
+ initUiCallManager();
+
+ // SupportedAudioRouteMask is CallAudioState.ROUTE_SPEAKER.
+ CallAudioState callAudioState = new CallAudioState(
+ true, CallAudioState.ROUTE_BLUETOOTH, CallAudioState.ROUTE_SPEAKER);
+ when(mMockInCallService.getCallAudioState()).thenReturn(callAudioState);
+
+ assertThat(mUiCallManager.isBluetoothCall()).isFalse();
+ assertThat(mUiCallManager.getSupportedAudioRoute().size()).isEqualTo(1);
+ assertThat(mUiCallManager.getSupportedAudioRoute().get(0))
+ .isEqualTo(CallAudioState.ROUTE_SPEAKER);
+ }
+
+ @After
+ public void tearDown() {
+ mUiCallManager.tearDown();
+ }
+}
diff --git a/tests/robotests/src/com/android/car/dialer/testutils/BroadcastReceiverVerifier.java b/tests/robotests/src/com/android/car/dialer/testutils/BroadcastReceiverVerifier.java
new file mode 100644
index 00000000..f7f3518f
--- /dev/null
+++ b/tests/robotests/src/com/android/car/dialer/testutils/BroadcastReceiverVerifier.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.testutils;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.robolectric.Shadows.shadowOf;
+
+import android.app.Application;
+import android.content.BroadcastReceiver;
+
+import androidx.annotation.Nullable;
+
+import org.robolectric.shadows.ShadowApplication;
+
+import java.util.List;
+
+/**
+ * Helper class for checking if certain broadcast receiver is registered or unregistered.
+ */
+public class BroadcastReceiverVerifier {
+
+ private ShadowApplication mShadowApplication;
+
+ public BroadcastReceiverVerifier(Application application) {
+ mShadowApplication = shadowOf(application);
+ }
+
+ /**
+ * Verifies that certain broadcast receiver is registered
+ */
+ public void verifyReceiverRegistered(String action) {
+ assertThat(getReceiverNumber()).isGreaterThan(0);
+ assertThat(hasMatchForIntentAction(action)).isTrue();
+ }
+
+ /**
+ * Verifies that certain broadcast receiver is unregistered
+ */
+ public void verifyReceiverUnregistered(String action, int preNumber) {
+ assertThat(getReceiverNumber()).isLessThan(preNumber);
+ assertThat(hasMatchForIntentAction(action)).isFalse();
+ }
+
+ /**
+ * Returns the BroadcastReceiver with certain Intent filter.
+ */
+ @Nullable
+ public BroadcastReceiver getBroadcastReceiverFor(String action) {
+ List<ShadowApplication.Wrapper> wrappers = mShadowApplication.getRegisteredReceivers();
+ for (int i = 0; i < wrappers.size(); i++) {
+ if (wrappers.get(i).getIntentFilter().hasAction(action)) {
+ return wrappers.get(i).getBroadcastReceiver();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the number of receivers.
+ */
+ public int getReceiverNumber() {
+ return mShadowApplication.getRegisteredReceivers().size();
+ }
+
+ private boolean hasMatchForIntentAction(String action) {
+ List<ShadowApplication.Wrapper> wrappers = mShadowApplication.getRegisteredReceivers();
+ boolean hasMatch = false;
+ for (int i = 0; i < wrappers.size(); i++) {
+ if (wrappers.get(i).getIntentFilter().hasAction(action)) {
+ hasMatch = true;
+ }
+ }
+ return hasMatch;
+ }
+}
diff --git a/tests/robotests/src/com/android/car/dialer/testutils/ShadowAndroidViewModelFactory.java b/tests/robotests/src/com/android/car/dialer/testutils/ShadowAndroidViewModelFactory.java
new file mode 100644
index 00000000..b1f3d688
--- /dev/null
+++ b/tests/robotests/src/com/android/car/dialer/testutils/ShadowAndroidViewModelFactory.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.testutils;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.ViewModel;
+import androidx.lifecycle.ViewModelProvider;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Shadow class for {@link ViewModelProvider}.
+ */
+@Implements(ViewModelProvider.AndroidViewModelFactory.class)
+public class ShadowAndroidViewModelFactory {
+
+ private static final Map<Class, ViewModel> VIEW_MODEL_MAP = new HashMap<>();
+
+ /**
+ * Adds class and view model pairs to the map.
+ */
+ public static <T extends ViewModel> void add(Class<T> modelClass, T viewModel) {
+ VIEW_MODEL_MAP.put(modelClass, viewModel);
+ }
+
+ /**
+ * Returns a ViewModel from the map.
+ */
+ @Implementation
+ public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
+ return (T) VIEW_MODEL_MAP.get(modelClass);
+ }
+}
diff --git a/tests/robotests/src/com/android/car/dialer/testutils/ShadowBluetoothAdapterForDialer.java b/tests/robotests/src/com/android/car/dialer/testutils/ShadowBluetoothAdapterForDialer.java
new file mode 100644
index 00000000..0686dd6c
--- /dev/null
+++ b/tests/robotests/src/com/android/car/dialer/testutils/ShadowBluetoothAdapterForDialer.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.testutils;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothProfile;
+
+import androidx.annotation.Nullable;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.shadows.ShadowApplication;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Derived from {@link org.robolectric.shadows.ShadowBluetoothAdapter}
+ *
+ * Needed for Bluetooth related tests because the default ShadowBluetooth Adapter does not include
+ * an implementation of setProfileConnectionState.
+ */
+@Implements(value = BluetoothAdapter.class)
+public class ShadowBluetoothAdapterForDialer extends
+ org.robolectric.shadows.ShadowBluetoothAdapter {
+
+ private static boolean bluetoothAvailable = true;
+ private Map<Integer, Integer> profileConnectionStateData = new HashMap<>();
+
+ @Nullable
+ @Implementation
+ public static synchronized BluetoothAdapter getDefaultAdapter() {
+ if (!bluetoothAvailable) {
+ return null;
+ }
+ return (BluetoothAdapter) ShadowApplication.getInstance().getBluetoothAdapter();
+ }
+
+ /**
+ * Sets if the default Bluetooth Adapter is null
+ */
+ public static void setBluetoothAvailable(boolean available) {
+ bluetoothAvailable = available;
+ }
+}
diff --git a/tests/robotests/src/com/android/car/dialer/testutils/ShadowCallLogCalls.java b/tests/robotests/src/com/android/car/dialer/testutils/ShadowCallLogCalls.java
new file mode 100644
index 00000000..4a6541cd
--- /dev/null
+++ b/tests/robotests/src/com/android/car/dialer/testutils/ShadowCallLogCalls.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.testutils;
+
+import android.content.Context;
+import android.provider.CallLog;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+
+/**
+ * Shadow for the system's CallLog.Call class that allows tests to configure the most recent call.
+ */
+@Implements(CallLog.Calls.class)
+public class ShadowCallLogCalls {
+ private static String lastOutgoingCall;
+
+ @Implementation
+ public static String getLastOutgoingCall(Context context) {
+ return lastOutgoingCall;
+ }
+
+ public static void setLastOutgoingCall(String lastCall) {
+ lastOutgoingCall = lastCall;
+ }
+}
diff --git a/tests/robotests/src/com/android/car/dialer/testutils/ShadowCar.java b/tests/robotests/src/com/android/car/dialer/testutils/ShadowCar.java
new file mode 100644
index 00000000..3f7c4254
--- /dev/null
+++ b/tests/robotests/src/com/android/car/dialer/testutils/ShadowCar.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.testutils;
+
+import android.car.Car;
+import android.content.Context;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+
+/**
+ * Shadow class for {@link Car}.
+ */
+@Implements(Car.class)
+public class ShadowCar {
+
+ private static Car sCar;
+
+ /**
+ * Returns a mocked version of a {@link Car} object.
+ */
+ @Implementation
+ protected static Car createCar(Context context) {
+ return sCar;
+ }
+
+ /**
+ * Sets {@code sCar}.
+ */
+ public static void setCar(Car car) {
+ sCar = car;
+ }
+}
diff --git a/tests/robotests/src/com/android/car/dialer/testutils/ShadowInMemoryPhoneBook.java b/tests/robotests/src/com/android/car/dialer/testutils/ShadowInMemoryPhoneBook.java
new file mode 100644
index 00000000..2aadeee3
--- /dev/null
+++ b/tests/robotests/src/com/android/car/dialer/testutils/ShadowInMemoryPhoneBook.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.testutils;
+
+import com.android.car.telephony.common.Contact;
+import com.android.car.telephony.common.InMemoryPhoneBook;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Shadow class for {@link InMemoryPhoneBook}.
+ */
+@Implements(InMemoryPhoneBook.class)
+public class ShadowInMemoryPhoneBook {
+
+ private Map<String, Contact> contactMap = new HashMap<>();
+
+ /**
+ * Adds phone number and contact pairs to the map.
+ */
+ public void add(String number, Contact contact) {
+ contactMap.put(number, contact);
+ }
+
+ /**
+ * Returns a ViewModel from the map.
+ */
+ @Implementation
+ public Contact lookupContactEntry(String number) {
+ return contactMap.get(number);
+ }
+}
diff --git a/tests/robotests/src/com/android/car/dialer/ui/TelecomActivityViewModelTest.java b/tests/robotests/src/com/android/car/dialer/ui/TelecomActivityViewModelTest.java
new file mode 100644
index 00000000..7443d4e4
--- /dev/null
+++ b/tests/robotests/src/com/android/car/dialer/ui/TelecomActivityViewModelTest.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.app.Application;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
+import android.content.Context;
+
+import com.android.car.dialer.CarDialerRobolectricTestRunner;
+import com.android.car.dialer.R;
+import com.android.car.dialer.TestDialerApplication;
+import com.android.car.dialer.livedata.BluetoothHfpStateLiveData;
+import com.android.car.dialer.livedata.BluetoothPairListLiveData;
+import com.android.car.dialer.livedata.BluetoothStateLiveData;
+import com.android.car.dialer.telecom.UiBluetoothMonitor;
+import com.android.car.dialer.telecom.UiCallManager;
+import com.android.car.dialer.testutils.ShadowBluetoothAdapterForDialer;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+import java.util.Arrays;
+import java.util.HashSet;
+
+@RunWith(CarDialerRobolectricTestRunner.class)
+@Config(shadows = ShadowBluetoothAdapterForDialer.class)
+public class TelecomActivityViewModelTest {
+
+ private TelecomActivityViewModel mTelecomActivityViewModel;
+ private Context mContext;
+ private BluetoothHfpStateLiveData mHfpStateLiveData;
+ private BluetoothPairListLiveData mPairedListLiveData;
+ private BluetoothStateLiveData mBluetoothStateLiveData;
+
+ @Before
+ public void setUp() {
+ mContext = RuntimeEnvironment.application;
+ ((TestDialerApplication) RuntimeEnvironment.application).initUiCallManager();
+ }
+
+ @After
+ public void tearDown() {
+ UiBluetoothMonitor.get().tearDown();
+ UiCallManager.get().tearDown();
+ }
+
+ @Test
+ public void testDialerAppState_defaultBluetoothAdapterIsNull_bluetoothError() {
+ initializeBluetoothMonitor(false);
+ initializeViewModel();
+
+ assertThat(mTelecomActivityViewModel.getErrorMessage().getValue()).isEqualTo(
+ mContext.getString(R.string.bluetooth_unavailable));
+ assertThat(mTelecomActivityViewModel.getDialerAppState().getValue()).isEqualTo(
+ TelecomActivityViewModel.DialerAppState.BLUETOOTH_ERROR);
+ }
+
+ @Test
+ public void testDialerAppState_bluetoothNotEnabled_bluetoothError() {
+ initializeBluetoothMonitor(true);
+ ShadowBluetoothAdapterForDialer shadowBluetoothAdapter =
+ (ShadowBluetoothAdapterForDialer) shadowOf(BluetoothAdapter.getDefaultAdapter());
+ shadowBluetoothAdapter.setEnabled(false);
+ initializeViewModel();
+
+ assertThat(mBluetoothStateLiveData.getValue()).isEqualTo(
+ BluetoothStateLiveData.BluetoothState.DISABLED);
+ assertThat(mTelecomActivityViewModel.getErrorMessage().getValue()).isEqualTo(
+ mContext.getString(R.string.bluetooth_disabled));
+ assertThat(mTelecomActivityViewModel.getDialerAppState().getValue()).isEqualTo(
+ TelecomActivityViewModel.DialerAppState.BLUETOOTH_ERROR);
+ }
+
+ @Test
+ public void testDialerAppState_noPairedDevices_bluetoothError() {
+ initializeBluetoothMonitor(true);
+ ShadowBluetoothAdapterForDialer shadowBluetoothAdapter =
+ (ShadowBluetoothAdapterForDialer) shadowOf(BluetoothAdapter.getDefaultAdapter());
+ shadowBluetoothAdapter.setEnabled(true);
+ shadowBluetoothAdapter.setBondedDevices(new HashSet<BluetoothDevice>());
+ initializeViewModel();
+
+ assertThat(mBluetoothStateLiveData.getValue()).isEqualTo(
+ BluetoothStateLiveData.BluetoothState.ENABLED);
+
+ assertThat(mPairedListLiveData.getValue().isEmpty()).isTrue();
+ assertThat(mTelecomActivityViewModel.getErrorMessage().getValue()).isEqualTo(
+ mContext.getString(R.string.bluetooth_unpaired));
+ assertThat(mTelecomActivityViewModel.getDialerAppState().getValue()).isEqualTo(
+ TelecomActivityViewModel.DialerAppState.BLUETOOTH_ERROR);
+ }
+
+ @Test
+ public void testDialerAppState_hfpNoConnected_bluetoothError() {
+ initializeBluetoothMonitor(true);
+ ShadowBluetoothAdapterForDialer shadowBluetoothAdapter =
+ (ShadowBluetoothAdapterForDialer) shadowOf(BluetoothAdapter.getDefaultAdapter());
+ shadowBluetoothAdapter.setEnabled(true);
+ shadowBluetoothAdapter.setBondedDevices(
+ new HashSet<>(Arrays.asList(mock(BluetoothDevice.class))));
+ shadowBluetoothAdapter.setProfileConnectionState(BluetoothProfile.HEADSET_CLIENT,
+ BluetoothProfile.STATE_DISCONNECTED);
+ initializeViewModel();
+
+ assertThat(mBluetoothStateLiveData.getValue()).isEqualTo(
+ BluetoothStateLiveData.BluetoothState.ENABLED);
+ assertThat(mPairedListLiveData.getValue().isEmpty()).isFalse();
+
+ assertThat(mHfpStateLiveData.getValue() == BluetoothProfile.STATE_DISCONNECTED).isTrue();
+ assertThat(mTelecomActivityViewModel.getErrorMessage().getValue()).isEqualTo(
+ mContext.getString(R.string.no_hfp));
+ assertThat(mTelecomActivityViewModel.getDialerAppState().getValue()).isEqualTo(
+ TelecomActivityViewModel.DialerAppState.BLUETOOTH_ERROR);
+ }
+
+ @Test
+ public void testDialerAppState_bluetoothAllSet_dialerAppStateDefault() {
+ initializeBluetoothMonitor(true);
+ ShadowBluetoothAdapterForDialer shadowBluetoothAdapter =
+ (ShadowBluetoothAdapterForDialer) shadowOf(BluetoothAdapter.getDefaultAdapter());
+ shadowBluetoothAdapter.setEnabled(true);
+ shadowBluetoothAdapter.setBondedDevices(
+ new HashSet<>(Arrays.asList(mock(BluetoothDevice.class))));
+ shadowBluetoothAdapter.setProfileConnectionState(BluetoothProfile.HEADSET_CLIENT,
+ BluetoothProfile.STATE_CONNECTED);
+ initializeViewModel();
+
+ assertThat(mTelecomActivityViewModel.getErrorMessage().getValue()).isEqualTo(
+ TelecomActivityViewModel.NO_BT_ERROR);
+ assertThat(mTelecomActivityViewModel.getDialerAppState().getValue()).isEqualTo(
+ TelecomActivityViewModel.DialerAppState.DEFAULT);
+ }
+
+ private void initializeBluetoothMonitor(boolean availability) {
+ ShadowBluetoothAdapterForDialer.setBluetoothAvailable(availability);
+
+ UiBluetoothMonitor.init(mContext);
+ mHfpStateLiveData = UiBluetoothMonitor.get().getHfpStateLiveData();
+ mPairedListLiveData = UiBluetoothMonitor.get().getPairListLiveData();
+ mBluetoothStateLiveData = UiBluetoothMonitor.get().getBluetoothStateLiveData();
+ }
+
+ private void initializeViewModel() {
+ mTelecomActivityViewModel = new TelecomActivityViewModel((Application) mContext);
+ // Observers needed so that the liveData's internal initialization is triggered
+ mTelecomActivityViewModel.getErrorMessage().observeForever(s -> {
+ });
+ mTelecomActivityViewModel.getDialerAppState().observeForever(s -> {
+ });
+ }
+}
diff --git a/tests/robotests/src/com/android/car/dialer/ui/activecall/InCallViewModelTest.java b/tests/robotests/src/com/android/car/dialer/ui/activecall/InCallViewModelTest.java
new file mode 100644
index 00000000..444f2e46
--- /dev/null
+++ b/tests/robotests/src/com/android/car/dialer/ui/activecall/InCallViewModelTest.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.activecall;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.app.Application;
+import android.content.Context;
+import android.net.Uri;
+import android.telecom.Call;
+import android.telecom.CallAudioState;
+import android.telecom.DisconnectCause;
+import android.telecom.GatewayInfo;
+
+import androidx.core.util.Pair;
+
+import com.android.car.dialer.CarDialerRobolectricTestRunner;
+import com.android.car.dialer.TestDialerApplication;
+import com.android.car.dialer.telecom.InCallServiceImpl;
+import com.android.car.dialer.telecom.UiCallManager;
+import com.android.car.telephony.common.CallDetail;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+@RunWith(CarDialerRobolectricTestRunner.class)
+public class InCallViewModelTest {
+ private static final String NUMBER = "6505551234";
+ private static final long CONNECT_TIME_MILLIS = 500000000;
+ private static final CharSequence LABEL = "DisconnectCause";
+ private static final Uri GATEWAY_ADDRESS = Uri.fromParts("tel", NUMBER, null);
+
+ private InCallViewModel mInCallViewModel;
+ private List<Call> mListForMockCalls;
+
+ @Mock
+ private InCallServiceImpl mInCallService;
+
+ @Mock
+ private UiCallManager mMockUiCallManager;
+ @Mock
+ private Call mMockActiveCall;
+ @Mock
+ private Call mMockDialingCall;
+ @Mock
+ private Call mMockHoldingCall;
+ @Mock
+ private Call mMockRingingCall;
+ @Mock
+ private Call.Details mMockDetails;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ Context context = RuntimeEnvironment.application;
+
+ ((TestDialerApplication) context).setupInCallServiceImpl(mInCallService);
+
+ when(mMockActiveCall.getState()).thenReturn(Call.STATE_ACTIVE);
+ when(mMockDialingCall.getState()).thenReturn(Call.STATE_DIALING);
+ when(mMockHoldingCall.getState()).thenReturn(Call.STATE_HOLDING);
+ when(mMockRingingCall.getState()).thenReturn(Call.STATE_RINGING);
+
+ // Set up call details
+ GatewayInfo gatewayInfo = new GatewayInfo("", GATEWAY_ADDRESS, GATEWAY_ADDRESS);
+ DisconnectCause disconnectCause = new DisconnectCause(1, LABEL, null, "");
+ when(mMockDetails.getHandle()).thenReturn(GATEWAY_ADDRESS);
+ when(mMockDetails.getDisconnectCause()).thenReturn(disconnectCause);
+ when(mMockDetails.getGatewayInfo()).thenReturn(gatewayInfo);
+ when(mMockDetails.getConnectTimeMillis()).thenReturn(CONNECT_TIME_MILLIS);
+
+ when(mMockDialingCall.getDetails()).thenReturn(mMockDetails);
+
+ mListForMockCalls = new ArrayList<>();
+ mListForMockCalls.add(mMockActiveCall);
+ mListForMockCalls.add(mMockDialingCall);
+ mListForMockCalls.add(mMockHoldingCall);
+ mListForMockCalls.add(mMockRingingCall);
+ when(mInCallService.getCalls()).thenReturn(mListForMockCalls);
+ UiCallManager.set(mMockUiCallManager);
+ when(mMockUiCallManager.getAudioRoute()).thenReturn(CallAudioState.ROUTE_BLUETOOTH);
+
+ mInCallViewModel = new InCallViewModel((Application) context);
+ mInCallViewModel.getIncomingCall().observeForever(s -> { });
+ mInCallViewModel.getOngoingCallList().observeForever(s -> { });
+ mInCallViewModel.getPrimaryCall().observeForever(s -> { });
+ mInCallViewModel.getPrimaryCallState().observeForever(s -> { });
+ mInCallViewModel.getPrimaryCallDetail().observeForever(s -> { });
+ mInCallViewModel.getCallStateAndConnectTime().observeForever(s -> { });
+ mInCallViewModel.getAudioRoute().observeForever(s -> { });
+ }
+
+ @After
+ public void tearDown() {
+ UiCallManager.set(null);
+ }
+
+ @Test
+ public void testGetCallList() {
+ List<Call> callListInOrder =
+ Arrays.asList(mMockDialingCall, mMockActiveCall, mMockHoldingCall);
+ List<Call> viewModelCallList = mInCallViewModel.getOngoingCallList().getValue();
+ assertArrayEquals(callListInOrder.toArray(), viewModelCallList.toArray());
+ }
+
+ @Test
+ public void testGetIncomingCall() {
+ Call incomingCall = mInCallViewModel.getIncomingCall().getValue();
+ assertThat(incomingCall).isEqualTo(mMockRingingCall);
+ }
+
+ @Test
+ public void testGetPrimaryCall() {
+ assertThat(mInCallViewModel.getPrimaryCall().getValue()).isEqualTo(mMockDialingCall);
+ }
+
+ @Test
+ public void testGetPrimaryCallState() {
+ assertThat(mInCallViewModel.getPrimaryCallState().getValue()).isEqualTo(Call.STATE_DIALING);
+ }
+
+ @Test
+ public void testGetPrimaryCallDetail() {
+ CallDetail callDetail = mInCallViewModel.getPrimaryCallDetail().getValue();
+ assertThat(callDetail.getNumber()).isEqualTo(NUMBER);
+ assertThat(callDetail.getConnectTimeMillis()).isEqualTo(CONNECT_TIME_MILLIS);
+ assertThat(callDetail.getDisconnectCause()).isEqualTo(LABEL);
+ assertThat(callDetail.getGatewayInfoOriginalAddress()).isEqualTo(GATEWAY_ADDRESS);
+ }
+
+ @Test
+ public void testGetCallStateAndConnectTime() {
+ Pair<Integer, Long> pair = mInCallViewModel.getCallStateAndConnectTime().getValue();
+ assertThat(pair.first).isEqualTo(Call.STATE_DIALING);
+ assertThat(pair.second).isEqualTo(CONNECT_TIME_MILLIS);
+ }
+
+ @Test
+ public void testGetAudioRoute() {
+ assertThat(mInCallViewModel.getAudioRoute().getValue())
+ .isEqualTo(CallAudioState.ROUTE_BLUETOOTH);
+ }
+
+ @Test
+ public void testOnTelecomCallAdded_updateCallList() {
+ Call mockDialingCall = mock(Call.class);
+ when(mockDialingCall.getState()).thenReturn(Call.STATE_DIALING);
+ mListForMockCalls.clear();
+ mListForMockCalls.addAll(Arrays.asList(mMockActiveCall, mMockHoldingCall, mockDialingCall));
+
+ mInCallViewModel.onTelecomCallAdded(mockDialingCall);
+
+ List<Call> callListInOrder =
+ Arrays.asList(mockDialingCall, mMockActiveCall, mMockHoldingCall);
+ List<Call> viewModelCallList = mInCallViewModel.getOngoingCallList().getValue();
+ assertArrayEquals(callListInOrder.toArray(), viewModelCallList.toArray());
+ assertThat(mInCallViewModel.getIncomingCall().getValue()).isEqualTo(null);
+ assertThat(mInCallViewModel.getPrimaryCall().getValue()).isEqualTo(mockDialingCall);
+ }
+
+ @Test
+ public void testOnTelecomCallRemoved_updateCallList() {
+ mListForMockCalls.remove(1);
+
+ mInCallViewModel.onTelecomCallRemoved(mock(Call.class));
+
+ List<Call> callListInOrder = Arrays.asList(mMockActiveCall, mMockHoldingCall);
+ List<Call> viewModelCallList = mInCallViewModel.getOngoingCallList().getValue();
+ assertArrayEquals(callListInOrder.toArray(), viewModelCallList.toArray());
+
+ assertThat(mInCallViewModel.getPrimaryCall().getValue()).isEqualTo(mMockActiveCall);
+ }
+}
diff --git a/tests/robotests/src/com/android/car/dialer/ui/activecall/IncomingCallFragmentTest.java b/tests/robotests/src/com/android/car/dialer/ui/activecall/IncomingCallFragmentTest.java
new file mode 100644
index 00000000..bc4ac60a
--- /dev/null
+++ b/tests/robotests/src/com/android/car/dialer/ui/activecall/IncomingCallFragmentTest.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.activecall;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.telecom.Call;
+import android.widget.TextView;
+
+import androidx.lifecycle.MutableLiveData;
+
+import com.android.car.dialer.CarDialerRobolectricTestRunner;
+import com.android.car.dialer.FragmentTestActivity;
+import com.android.car.dialer.R;
+import com.android.car.dialer.testutils.ShadowAndroidViewModelFactory;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.Robolectric;
+import org.robolectric.annotation.Config;
+
+@Config(shadows = {ShadowAndroidViewModelFactory.class})
+@RunWith(CarDialerRobolectricTestRunner.class)
+public class IncomingCallFragmentTest {
+ private IncomingCallFragment mIncomingCallFragment;
+ @Mock
+ private Call mMockCall;
+ @Mock
+ private InCallViewModel mMockInCallViewModel;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ ShadowAndroidViewModelFactory.add(InCallViewModel.class, mMockInCallViewModel);
+
+ MutableLiveData<Call> callLiveData = new MutableLiveData<>();
+ callLiveData.setValue(mMockCall);
+ when(mMockInCallViewModel.getIncomingCall()).thenReturn(callLiveData);
+
+ FragmentTestActivity fragmentTestActivity = Robolectric.buildActivity(
+ FragmentTestActivity.class).create().start().resume().get();
+ mIncomingCallFragment = new IncomingCallFragment();
+ fragmentTestActivity.setFragment(mIncomingCallFragment);
+ }
+
+ @Test
+ public void testCallStateIsRinging() {
+ TextView callStateView = mIncomingCallFragment.getView().findViewById(
+ R.id.user_profile_call_state);
+
+ assertThat(callStateView.getText()).isEqualTo(
+ mIncomingCallFragment.getResources().getString(
+ R.string.call_state_call_ringing));
+ }
+}
diff --git a/tests/robotests/src/com/android/car/dialer/ui/activecall/OnGoingCallControllerBarFragmentTest.java b/tests/robotests/src/com/android/car/dialer/ui/activecall/OnGoingCallControllerBarFragmentTest.java
new file mode 100644
index 00000000..1be16eac
--- /dev/null
+++ b/tests/robotests/src/com/android/car/dialer/ui/activecall/OnGoingCallControllerBarFragmentTest.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.activecall;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.telecom.Call;
+import android.telecom.CallAudioState;
+import android.telecom.TelecomManager;
+import android.view.View;
+import android.widget.ImageView;
+
+import androidx.core.util.Pair;
+import androidx.lifecycle.MutableLiveData;
+
+import com.android.car.dialer.CarDialerRobolectricTestRunner;
+import com.android.car.dialer.FragmentTestActivity;
+import com.android.car.dialer.R;
+import com.android.car.dialer.TestDialerApplication;
+import com.android.car.dialer.telecom.InCallServiceImpl;
+import com.android.car.dialer.telecom.UiCallManager;
+import com.android.car.dialer.testutils.ShadowAndroidViewModelFactory;
+import com.android.car.telephony.common.CallDetail;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.Robolectric;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.ShadowContextImpl;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+@Config(shadows = {ShadowAndroidViewModelFactory.class})
+@RunWith(CarDialerRobolectricTestRunner.class)
+public class OnGoingCallControllerBarFragmentTest {
+ private OnGoingCallControllerBarFragment mOnGoingCallControllerBarFragment;
+ private List<Integer> mAudioRouteList = new ArrayList<>();
+ private OngoingCallStateViewModel mOngoingCallStateViewModel;
+ private MutableLiveData<Call> mCallLiveData;
+ private MutableLiveData<Integer> mCallStateLiveData;
+ private MutableLiveData<CallDetail> mCallDetailLiveData;
+ @Mock
+ private Call mMockCall;
+ @Mock
+ private TelecomManager mMockTelecomManager;
+ @Mock
+ private CallAudioState mMockAudioState;
+ @Mock
+ private UiCallManager mMockUiCallManager;
+ @Mock
+ private InCallViewModel mMockInCallViewModel;
+ @Mock
+ private InCallServiceImpl mMockInCallService;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ mCallLiveData = new MutableLiveData<>();
+ mCallLiveData.setValue(mMockCall);
+ mCallStateLiveData = new MutableLiveData<>();
+ mCallDetailLiveData = new MutableLiveData<>();
+
+ ((TestDialerApplication) RuntimeEnvironment.application).setupInCallServiceImpl(
+ mMockInCallService);
+ when(mMockInCallService.getCallAudioState()).thenReturn(mMockAudioState);
+ ShadowContextImpl shadowContext = Shadow.extract(
+ RuntimeEnvironment.application.getBaseContext());
+ shadowContext.setSystemService(Context.TELECOM_SERVICE, mMockTelecomManager);
+
+ mOngoingCallStateViewModel = new OngoingCallStateViewModel(RuntimeEnvironment.application);
+ }
+
+ @Test
+ public void testMuteButton() {
+ addFragment(Call.STATE_ACTIVE);
+
+ View muteButton = mOnGoingCallControllerBarFragment.getView().findViewById(
+ R.id.mute_button);
+ // Test initial state
+ assertThat(muteButton.hasOnClickListeners()).isTrue();
+ assertThat(muteButton.isActivated()).isFalse();
+ // Mute
+ muteButton.performClick();
+ assertThat(muteButton.isActivated()).isTrue();
+ verify(mMockUiCallManager).setMuted(true);
+ // Unmute
+ muteButton.performClick();
+ assertThat(muteButton.isActivated()).isFalse();
+ verify(mMockUiCallManager).setMuted(false);
+ }
+
+ @Test
+ public void testDialpadButton() {
+ addFragment(Call.STATE_ACTIVE);
+ mOngoingCallStateViewModel.getDialpadState().setValue(false);
+
+ View dialpadButton = mOnGoingCallControllerBarFragment.getView().findViewById(
+ R.id.toggle_dialpad_button);
+
+ // Test initial state
+ assertThat(dialpadButton.hasOnClickListeners()).isTrue();
+ assertThat(dialpadButton.isActivated()).isFalse();
+ assertThat(mOngoingCallStateViewModel.getDialpadState().getValue()).isFalse();
+ // On open dialpad
+ dialpadButton.performClick();
+ assertThat(dialpadButton.isActivated()).isTrue();
+ assertThat(mOngoingCallStateViewModel.getDialpadState().getValue()).isTrue();
+ // On close dialpad
+ dialpadButton.performClick();
+ assertThat(dialpadButton.isActivated()).isFalse();
+ assertThat(mOngoingCallStateViewModel.getDialpadState().getValue()).isFalse();
+ }
+
+ @Test
+ public void testEndCallButton() {
+ addFragment(Call.STATE_ACTIVE);
+
+ View endCallButton = mOnGoingCallControllerBarFragment.getView().findViewById(
+ R.id.end_call_button);
+ assertThat(endCallButton.hasOnClickListeners()).isTrue();
+ // onEndCall
+ endCallButton.performClick();
+ verify(mMockCall).disconnect();
+ }
+
+ @Test
+ public void testAudioRouteButton_withOneAudioRoute() {
+ addFragment(Call.STATE_ACTIVE);
+
+ View fragmentView = mOnGoingCallControllerBarFragment.getView();
+ assertThat(fragmentView.findViewById(
+ R.id.voice_channel_button).hasOnClickListeners()).isFalse();
+ }
+
+ @Test
+ public void testAudioRouteButtonView_withMultipleAudioRoutes() {
+ mAudioRouteList.add(CallAudioState.ROUTE_EARPIECE);
+ mAudioRouteList.add(CallAudioState.ROUTE_BLUETOOTH);
+ addFragment(Call.STATE_ACTIVE);
+
+ View fragmentView = mOnGoingCallControllerBarFragment.getView();
+ ImageView audioRouteButton = fragmentView.findViewById(R.id.voice_channel_button);
+ assertThat(audioRouteButton.hasOnClickListeners()).isTrue();
+ }
+
+ @Test
+ public void testClickPauseButton_activeCall() {
+ addFragment(Call.STATE_ACTIVE);
+
+ ImageView pauseButton = mOnGoingCallControllerBarFragment.getView().findViewById(
+ R.id.pause_button);
+ assertThat(pauseButton.hasOnClickListeners()).isTrue();
+ assertThat(pauseButton.isActivated()).isFalse();
+
+ // onHoldCall
+ pauseButton.performClick();
+ verify(mMockCall).hold();
+ // onUnHoldCall
+ mCallStateLiveData.setValue(Call.STATE_HOLDING);
+ pauseButton.performClick();
+ verify(mMockCall).unhold();
+ }
+
+ @Test
+ public void testClickPauseButton_connectingCall() {
+ addFragment(Call.STATE_DIALING);
+ ImageView pauseButton = mOnGoingCallControllerBarFragment.getView().findViewById(
+ R.id.pause_button);
+ pauseButton.performClick();
+ verify(mMockCall, never()).hold();
+ verify(mMockCall, never()).unhold();
+ }
+
+ private void addFragment(int callState) {
+ mAudioRouteList.add(CallAudioState.ROUTE_SPEAKER);
+ when(mMockUiCallManager.getSupportedAudioRoute()).thenReturn(mAudioRouteList);
+ UiCallManager.set(mMockUiCallManager);
+
+ mCallStateLiveData.setValue(callState);
+ when(mMockInCallViewModel.getPrimaryCall()).thenReturn(mCallLiveData);
+ when(mMockInCallViewModel.getPrimaryCallDetail()).thenReturn(mCallDetailLiveData);
+ when(mMockInCallViewModel.getPrimaryCallState()).thenReturn(mCallStateLiveData);
+
+ MutableLiveData<Integer> audioRouteLiveData = new MutableLiveData<>();
+ audioRouteLiveData.setValue(CallAudioState.ROUTE_BLUETOOTH);
+ when(mMockInCallViewModel.getAudioRoute()).thenReturn(audioRouteLiveData);
+
+ MutableLiveData<Pair<Integer, Long>> stateAndConnectTimeLiveData = new MutableLiveData<>();
+ when(mMockInCallViewModel.getCallStateAndConnectTime())
+ .thenReturn(stateAndConnectTimeLiveData);
+
+ ShadowAndroidViewModelFactory.add(InCallViewModel.class, mMockInCallViewModel);
+ ShadowAndroidViewModelFactory.add(OngoingCallStateViewModel.class,
+ mOngoingCallStateViewModel);
+
+ FragmentTestActivity fragmentTestActivity = Robolectric.buildActivity(
+ FragmentTestActivity.class).create().start().resume().get();
+ mOnGoingCallControllerBarFragment = new OnGoingCallControllerBarFragment();
+ fragmentTestActivity.setFragment(mOnGoingCallControllerBarFragment);
+ }
+}
diff --git a/tests/robotests/src/com/android/car/dialer/ui/activecall/OngoingCallFragmentTest.java b/tests/robotests/src/com/android/car/dialer/ui/activecall/OngoingCallFragmentTest.java
new file mode 100644
index 00000000..cc8629cb
--- /dev/null
+++ b/tests/robotests/src/com/android/car/dialer/ui/activecall/OngoingCallFragmentTest.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.activecall;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.view.View;
+
+import androidx.fragment.app.Fragment;
+
+import com.android.car.dialer.CarDialerRobolectricTestRunner;
+import com.android.car.dialer.FragmentTestActivity;
+import com.android.car.dialer.R;
+import com.android.car.dialer.TestDialerApplication;
+import com.android.car.dialer.telecom.UiCallManager;
+import com.android.car.telephony.common.InMemoryPhoneBook;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.Robolectric;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(CarDialerRobolectricTestRunner.class)
+public class OngoingCallFragmentTest {
+
+ private OngoingCallFragment mOngoingCallFragment;
+ private FragmentTestActivity mFragmentTestActivity;
+ private View mUserProfileContainerView;
+ private Fragment mInCallDialpadFragment;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+
+ Context context = RuntimeEnvironment.application;
+ ((TestDialerApplication) context).setupInCallServiceImpl();
+ ((TestDialerApplication) context).initUiCallManager();
+ InMemoryPhoneBook.init(context);
+
+ mOngoingCallFragment = new OngoingCallFragment();
+ mFragmentTestActivity = Robolectric.buildActivity(
+ FragmentTestActivity.class).create().start().resume().get();
+ mFragmentTestActivity.setFragment(mOngoingCallFragment);
+
+ mUserProfileContainerView = mOngoingCallFragment.getView().findViewById(
+ R.id.user_profile_container);
+ mInCallDialpadFragment = mOngoingCallFragment.getChildFragmentManager().findFragmentById(
+ R.id.incall_dialpad_fragment);
+ }
+
+ @After
+ public void tearDown() {
+ UiCallManager.get().tearDown();
+ InMemoryPhoneBook.tearDown();
+ }
+
+ @Test
+ public void testOnCreateView() {
+ assertThat(mInCallDialpadFragment.isHidden()).isTrue();
+ assertThat(mUserProfileContainerView.getVisibility()).isEqualTo(View.VISIBLE);
+ }
+
+ @Test
+ public void testOnOpenDialpad() {
+ mOngoingCallFragment.onOpenDialpad();
+
+ assertThat(mInCallDialpadFragment.isHidden()).isFalse();
+ assertThat(mUserProfileContainerView.getVisibility()).isEqualTo(View.GONE);
+ }
+
+ @Test
+ public void testOnCloseDialpad() {
+ mOngoingCallFragment.onCloseDialpad();
+
+ assertThat(mInCallDialpadFragment.isHidden()).isTrue();
+ assertThat(mUserProfileContainerView.getVisibility()).isEqualTo(View.VISIBLE);
+ }
+}
diff --git a/tests/robotests/src/com/android/car/dialer/ui/activecall/RingingCallControllerBarFragmentTest.java b/tests/robotests/src/com/android/car/dialer/ui/activecall/RingingCallControllerBarFragmentTest.java
new file mode 100644
index 00000000..457a9f15
--- /dev/null
+++ b/tests/robotests/src/com/android/car/dialer/ui/activecall/RingingCallControllerBarFragmentTest.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.activecall;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.telecom.Call;
+import android.view.View;
+
+import androidx.lifecycle.MutableLiveData;
+
+import com.android.car.dialer.CarDialerRobolectricTestRunner;
+import com.android.car.dialer.FragmentTestActivity;
+import com.android.car.dialer.R;
+import com.android.car.dialer.testutils.ShadowAndroidViewModelFactory;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.Robolectric;
+import org.robolectric.annotation.Config;
+
+@Config(shadows = {ShadowAndroidViewModelFactory.class})
+@RunWith(CarDialerRobolectricTestRunner.class)
+public class RingingCallControllerBarFragmentTest {
+
+ private RingingCallControllerBarFragment mRingingCallControllerBarFragment;
+ @Mock
+ private InCallViewModel mMockInCallViewModel;
+ @Mock
+ private Call mMockCall;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ MutableLiveData<Call> callLiveData = new MutableLiveData<>();
+ callLiveData.setValue(mMockCall);
+ when(mMockInCallViewModel.getIncomingCall()).thenReturn(callLiveData);
+ ShadowAndroidViewModelFactory.add(InCallViewModel.class, mMockInCallViewModel);
+
+ FragmentTestActivity fragmentTestActivity = Robolectric.buildActivity(
+ FragmentTestActivity.class).create().start().resume().get();
+ mRingingCallControllerBarFragment = new RingingCallControllerBarFragment();
+ fragmentTestActivity.setFragment(mRingingCallControllerBarFragment);
+ }
+
+ @Test
+ public void testAnswerCallButton() {
+ View answerCallButton = mRingingCallControllerBarFragment.getView().findViewById(
+ R.id.answer_call_button);
+ assertThat(answerCallButton.hasOnClickListeners()).isTrue();
+
+ answerCallButton.performClick();
+
+ verifyAnswerCall();
+ }
+
+ @Test
+ public void testAnswerCallText() {
+ View answerCallText = mRingingCallControllerBarFragment.getView().findViewById(
+ R.id.answer_call_text);
+ assertThat(answerCallText.hasOnClickListeners()).isTrue();
+
+ answerCallText.performClick();
+
+ verifyAnswerCall();
+ }
+
+ @Test
+ public void testEndCallButton() {
+ View endCallButton = mRingingCallControllerBarFragment.getView().findViewById(
+ R.id.end_call_button);
+ assertThat(endCallButton.hasOnClickListeners()).isTrue();
+
+ endCallButton.performClick();
+
+ verifyRejectCall();
+ }
+
+ @Test
+ public void testEndCallText() {
+ View endCallText = mRingingCallControllerBarFragment.getView().findViewById(
+ R.id.end_call_text);
+ assertThat(endCallText.hasOnClickListeners()).isTrue();
+
+ endCallText.performClick();
+
+ verifyRejectCall();
+ }
+
+ private void verifyAnswerCall() {
+ ArgumentCaptor<Integer> captor = ArgumentCaptor.forClass(Integer.class);
+ verify(mMockCall).answer(captor.capture());
+ assertThat(captor.getValue()).isEqualTo(/* videoState= */0);
+ }
+
+ private void verifyRejectCall() {
+ ArgumentCaptor<Boolean> booleanCaptor = ArgumentCaptor.forClass(Boolean.class);
+ ArgumentCaptor<String> stringCaptor = ArgumentCaptor.forClass(String.class);
+ verify(mMockCall).reject(booleanCaptor.capture(), stringCaptor.capture());
+ // Make sure to reject a call without a message.
+ assertThat(booleanCaptor.getValue()).isFalse();
+ // verify the text message after rejecting the call is null.
+ assertThat(stringCaptor.getValue()).isNull();
+ }
+}
diff --git a/tests/robotests/src/com/android/car/dialer/ui/calllog/CallHistoryFragmentTest.java b/tests/robotests/src/com/android/car/dialer/ui/calllog/CallHistoryFragmentTest.java
new file mode 100644
index 00000000..63383620
--- /dev/null
+++ b/tests/robotests/src/com/android/car/dialer/ui/calllog/CallHistoryFragmentTest.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.calllog;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.net.Uri;
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.lifecycle.MutableLiveData;
+
+import com.android.car.apps.common.widget.PagedRecyclerView;
+import com.android.car.dialer.CarDialerRobolectricTestRunner;
+import com.android.car.dialer.FragmentTestActivity;
+import com.android.car.dialer.R;
+import com.android.car.dialer.livedata.CallHistoryLiveData;
+import com.android.car.dialer.telecom.UiCallManager;
+import com.android.car.dialer.testutils.ShadowAndroidViewModelFactory;
+import com.android.car.dialer.ui.common.entity.UiCallLog;
+import com.android.car.dialer.widget.CallTypeIconsView;
+import com.android.car.telephony.common.InMemoryPhoneBook;
+import com.android.car.telephony.common.PhoneCallLog;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.Robolectric;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+import java.util.Arrays;
+import java.util.List;
+
+@Config(shadows = {ShadowAndroidViewModelFactory.class})
+@RunWith(CarDialerRobolectricTestRunner.class)
+public class CallHistoryFragmentTest {
+ private static final String PHONE_NUMBER = "6502530000";
+ private static final String UI_CALLOG_TITLE = "TITLE";
+ private static final String UI_CALLOG_TEXT = "TEXT";
+ private static final long TIME_STAMP_1 = 5000;
+ private static final long TIME_STAMP_2 = 10000;
+
+ private CallLogViewHolder mViewHolder;
+ @Mock
+ private UiCallManager mMockUiCallManager;
+ @Mock
+ private Uri mMockUri;
+ @Mock
+ private CallHistoryViewModel mMockCallHistoryViewModel;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+
+ InMemoryPhoneBook.init(RuntimeEnvironment.application);
+ UiCallManager.set(mMockUiCallManager);
+
+ PhoneCallLog.Record record1 = new PhoneCallLog.Record(TIME_STAMP_1,
+ CallHistoryLiveData.CallType.INCOMING_TYPE);
+ PhoneCallLog.Record record2 = new PhoneCallLog.Record(TIME_STAMP_2,
+ CallHistoryLiveData.CallType.OUTGOING_TYPE);
+ UiCallLog uiCallLog = new UiCallLog(UI_CALLOG_TITLE, UI_CALLOG_TEXT, PHONE_NUMBER, mMockUri,
+ Arrays.asList(record1, record2));
+
+ MutableLiveData<List<UiCallLog>> callLog = new MutableLiveData<>();
+ callLog.setValue(Arrays.asList(uiCallLog));
+ ShadowAndroidViewModelFactory.add(CallHistoryViewModel.class, mMockCallHistoryViewModel);
+ when(mMockCallHistoryViewModel.getCallHistory()).thenReturn(callLog);
+
+ CallHistoryFragment callHistoryFragment = CallHistoryFragment.newInstance();
+ FragmentTestActivity mFragmentTestActivity = Robolectric.buildActivity(
+ FragmentTestActivity.class).create().resume().get();
+ mFragmentTestActivity.setFragment(callHistoryFragment);
+
+ PagedRecyclerView recyclerView = callHistoryFragment.getView().findViewById(R.id.list_view);
+ // set up layout for recyclerView
+ recyclerView.layoutBothForTesting(0, 0, 100, 1000);
+ mViewHolder = (CallLogViewHolder) recyclerView.findViewHolderForLayoutPosition(0);
+ }
+
+ @After
+ public void tearDown() {
+ InMemoryPhoneBook.tearDown();
+ }
+
+ @Test
+ public void testUI() {
+ TextView titleView = mViewHolder.itemView.findViewById(R.id.title);
+ TextView textView = mViewHolder.itemView.findViewById(R.id.text);
+ CallTypeIconsView callTypeIconsView = mViewHolder.itemView.findViewById(
+ R.id.call_type_icons);
+
+ assertThat(titleView.getText()).isEqualTo(UI_CALLOG_TITLE);
+ assertThat(textView.getText().toString()).isEqualTo(UI_CALLOG_TEXT);
+ assertThat(callTypeIconsView.getCallType(0)).isEqualTo(
+ CallHistoryLiveData.CallType.INCOMING_TYPE);
+ assertThat(callTypeIconsView.getCallType(1)).isEqualTo(
+ CallHistoryLiveData.CallType.OUTGOING_TYPE);
+ }
+
+ @Test
+ public void testClick_placeCall() {
+ View callButton = mViewHolder.itemView.findViewById(R.id.call_action_id);
+ assertThat(callButton.hasOnClickListeners()).isTrue();
+
+ callButton.performClick();
+
+ ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
+ verify(mMockUiCallManager).placeCall(captor.capture());
+ assertThat(captor.getValue()).isEqualTo(PHONE_NUMBER);
+ }
+}
diff --git a/tests/robotests/src/com/android/car/dialer/ui/contact/ContactDetailsFragmentTest.java b/tests/robotests/src/com/android/car/dialer/ui/contact/ContactDetailsFragmentTest.java
new file mode 100644
index 00000000..c41f2a65
--- /dev/null
+++ b/tests/robotests/src/com/android/car/dialer/ui/contact/ContactDetailsFragmentTest.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.contact;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.net.Uri;
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.lifecycle.MutableLiveData;
+
+import com.android.car.apps.common.widget.PagedRecyclerView;
+import com.android.car.dialer.CarDialerRobolectricTestRunner;
+import com.android.car.dialer.FragmentTestActivity;
+import com.android.car.dialer.R;
+import com.android.car.dialer.telecom.UiCallManager;
+import com.android.car.dialer.testutils.ShadowAndroidViewModelFactory;
+import com.android.car.telephony.common.Contact;
+import com.android.car.telephony.common.PhoneNumber;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.Robolectric;
+import org.robolectric.annotation.Config;
+
+import java.util.Arrays;
+
+@Config(shadows = {ShadowAndroidViewModelFactory.class}, qualifiers = "h610dp")
+@RunWith(CarDialerRobolectricTestRunner.class)
+public class ContactDetailsFragmentTest {
+ private static final String DISPLAY_NAME = "NAME";
+ private static final String[] RAW_NUMBERS = {"6505550000", "6502370000"};
+
+ private ContactDetailsFragment mContactDetailsFragment;
+ private FragmentTestActivity mFragmentTestActivity;
+ private PagedRecyclerView mListView;
+ @Mock
+ private ContactDetailsViewModel mMockContactDetailsViewModel;
+ @Mock
+ private Uri mMockContactLookupUri;
+ @Mock
+ private Contact mMockContact;
+ @Mock
+ private PhoneNumber mMockPhoneNumber1;
+ @Mock
+ private PhoneNumber mMockPhoneNumber2;
+ @Mock
+ private UiCallManager mMockUiCallManager;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ when(mMockContact.getDisplayName()).thenReturn(DISPLAY_NAME);
+ when(mMockPhoneNumber1.getRawNumber()).thenReturn(RAW_NUMBERS[0]);
+ when(mMockPhoneNumber2.getRawNumber()).thenReturn(RAW_NUMBERS[1]);
+ when(mMockContact.getNumbers()).thenReturn(
+ Arrays.asList(mMockPhoneNumber1, mMockPhoneNumber2));
+
+ UiCallManager.set(mMockUiCallManager);
+
+ MutableLiveData<Contact> contactDetails = new MutableLiveData<>();
+ contactDetails.setValue(mMockContact);
+ ShadowAndroidViewModelFactory.add(ContactDetailsViewModel.class,
+ mMockContactDetailsViewModel);
+ when(mMockContactDetailsViewModel.getContactDetails(mMockContactLookupUri)).thenReturn(
+ contactDetails);
+ }
+
+ @Test
+ public void testCreateWithContact() {
+ when(mMockContact.getLookupUri()).thenReturn(mMockContactLookupUri);
+ mContactDetailsFragment = ContactDetailsFragment.newInstance(mMockContact);
+
+ setUpFragment();
+
+ verifyHeader();
+ verifyPhoneNumber(1);
+ verifyPhoneNumber(2);
+ }
+
+ private void setUpFragment() {
+ mFragmentTestActivity = Robolectric.buildActivity(
+ FragmentTestActivity.class).create().resume().get();
+ mFragmentTestActivity.setFragment(mContactDetailsFragment);
+
+ mListView = mContactDetailsFragment.getView().findViewById(R.id.list_view);
+ // Set up layout for recyclerView
+ mListView.layoutBothForTesting(0, 0, 100, 1000);
+ }
+
+ /**
+ * Verify the title of the Contact
+ */
+ private void verifyHeader() {
+ View firstChild = mListView.findViewHolderForLayoutPosition(0).itemView;
+ assertThat(((TextView) firstChild.findViewById(R.id.title)).getText().toString()).isEqualTo(
+ DISPLAY_NAME);
+ assertThat(firstChild.hasOnClickListeners()).isFalse();
+ }
+
+ /**
+ * Verify the phone numbers for the Contact
+ */
+ private void verifyPhoneNumber(int position) {
+ View child = mListView.findViewHolderForLayoutPosition(position).itemView;
+ View callButton = child.findViewById(R.id.call_action_id);
+
+ assertThat(((TextView) child.findViewById(R.id.title)).getText().toString()).isEqualTo(
+ RAW_NUMBERS[position - 1]);
+ assertThat(callButton.hasOnClickListeners()).isTrue();
+
+ int invocations = Mockito.mockingDetails(mMockUiCallManager).getInvocations().size();
+
+ callButton.performClick();
+
+ verify(mMockUiCallManager, times(invocations + 1)).placeCall(Mockito.any());
+ }
+}
diff --git a/tests/robotests/src/com/android/car/dialer/ui/contact/ContactListFragmentTest.java b/tests/robotests/src/com/android/car/dialer/ui/contact/ContactListFragmentTest.java
new file mode 100644
index 00000000..a6227730
--- /dev/null
+++ b/tests/robotests/src/com/android/car/dialer/ui/contact/ContactListFragmentTest.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.contact;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.view.View;
+
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+import androidx.lifecycle.MutableLiveData;
+
+import com.android.car.apps.common.widget.PagedRecyclerView;
+import com.android.car.dialer.CarDialerRobolectricTestRunner;
+import com.android.car.dialer.FragmentTestActivity;
+import com.android.car.dialer.R;
+import com.android.car.dialer.telecom.UiCallManager;
+import com.android.car.dialer.testutils.ShadowAndroidViewModelFactory;
+import com.android.car.telephony.common.Contact;
+import com.android.car.telephony.common.PhoneNumber;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.Robolectric;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowAlertDialog;
+
+import java.util.Arrays;
+import java.util.List;
+
+@Config(shadows = {ShadowAndroidViewModelFactory.class})
+@RunWith(CarDialerRobolectricTestRunner.class)
+public class ContactListFragmentTest {
+ private static final String RAW_NUMBNER = "6502530000";
+
+ private ContactListFragment mContactListFragment;
+ private FragmentTestActivity mFragmentTestActivity;
+ private ContactListViewHolder mViewHolder;
+ @Mock
+ private UiCallManager mMockUiCallManager;
+ @Mock
+ private ContactListViewModel mMockContactListViewModel;
+ @Mock
+ private ContactDetailsViewModel mMockContactDetailsViewModel;
+ @Mock
+ private Contact mMockContact1;
+ @Mock
+ private Contact mMockContact2;
+ @Mock
+ private Contact mMockContact3;
+ @Mock
+ private PhoneNumber mMockPhoneNumber;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ MutableLiveData<List<Contact>> contactList = new MutableLiveData<>();
+ contactList.setValue(Arrays.asList(mMockContact1, mMockContact2, mMockContact3));
+ ShadowAndroidViewModelFactory.add(ContactListViewModel.class, mMockContactListViewModel);
+ when(mMockContactListViewModel.getAllContacts()).thenReturn(contactList);
+
+ MutableLiveData<Contact> contactDetail = new MutableLiveData<>();
+ contactDetail.setValue(mMockContact1);
+ ShadowAndroidViewModelFactory.add(ContactDetailsViewModel.class,
+ mMockContactDetailsViewModel);
+ when(mMockContactDetailsViewModel.getContactDetails(any())).thenReturn(contactDetail);
+ }
+
+ @Test
+ public void testClickCallActionButton_ContactHasOneNumber_placeCall() {
+ UiCallManager.set(mMockUiCallManager);
+ when(mMockContact1.getNumbers()).thenReturn(Arrays.asList(mMockPhoneNumber));
+ when(mMockPhoneNumber.getRawNumber()).thenReturn(RAW_NUMBNER);
+ setUpFragment();
+
+ View callActionView = mViewHolder.itemView.findViewById(R.id.call_action_id);
+ assertThat(callActionView.hasOnClickListeners()).isTrue();
+
+ callActionView.performClick();
+
+ ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
+ verify(mMockUiCallManager).placeCall(captor.capture());
+ assertThat(captor.getValue()).isEqualTo(RAW_NUMBNER);
+ }
+
+ @Test
+ public void testClickCallActionButton_ContactHasMultipleNumbers_showAlertDialog() {
+ PhoneNumber otherMockPhoneNumber = mock(PhoneNumber.class);
+ when(mMockContact1.getNumbers()).thenReturn(
+ Arrays.asList(mMockPhoneNumber, otherMockPhoneNumber));
+ setUpFragment();
+
+ assertThat(ShadowAlertDialog.getLatestAlertDialog()).isNull();
+ View callActionView = mViewHolder.itemView.findViewById(R.id.call_action_id);
+ callActionView.performClick();
+
+ verify(mMockUiCallManager, never()).placeCall(any());
+ assertThat(ShadowAlertDialog.getLatestAlertDialog()).isNotNull();
+ }
+
+ @Test
+ public void testClickShowContactDetailView_showContactDetail() {
+ setUpFragment();
+
+ View showContactDetailActionView = mViewHolder.itemView.findViewById(
+ R.id.show_contact_detail_id);
+ assertThat(showContactDetailActionView.hasOnClickListeners()).isTrue();
+
+ showContactDetailActionView.performClick();
+
+ // verify contact detail is shown.
+ verifyShowContactDetail();
+ }
+
+ private void setUpFragment() {
+ mContactListFragment = ContactListFragment.newInstance();
+ mFragmentTestActivity = Robolectric.buildActivity(
+ FragmentTestActivity.class).create().resume().get();
+ mFragmentTestActivity.setFragment(mContactListFragment);
+
+ PagedRecyclerView recyclerView = mContactListFragment.getView()
+ .findViewById(R.id.list_view);
+ //Force RecyclerView to layout to ensure findViewHolderForLayoutPosition works.
+ recyclerView.layoutBothForTesting(0, 0, 100, 1000);
+ mViewHolder = (ContactListViewHolder) recyclerView.findViewHolderForLayoutPosition(0);
+ }
+
+ private void verifyShowContactDetail() {
+ FragmentManager manager = mFragmentTestActivity.getSupportFragmentManager();
+ String tag = manager.getBackStackEntryAt(manager.getBackStackEntryCount() - 1).getName();
+ Fragment fragment = manager.findFragmentByTag(tag);
+ assertThat(fragment instanceof ContactDetailsFragment).isTrue();
+ }
+}
diff --git a/tests/robotests/src/com/android/car/dialer/ui/contact/ContactListViewHolderTest.java b/tests/robotests/src/com/android/car/dialer/ui/contact/ContactListViewHolderTest.java
new file mode 100644
index 00000000..982991da
--- /dev/null
+++ b/tests/robotests/src/com/android/car/dialer/ui/contact/ContactListViewHolderTest.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.contact;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.provider.ContactsContract;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.TextView;
+
+import com.android.car.dialer.CarDialerRobolectricTestRunner;
+import com.android.car.dialer.R;
+import com.android.car.dialer.telecom.UiCallManager;
+import com.android.car.telephony.common.Contact;
+import com.android.car.telephony.common.PhoneNumber;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.shadows.ShadowAlertDialog;
+
+import java.util.Arrays;
+
+@RunWith(CarDialerRobolectricTestRunner.class)
+public class ContactListViewHolderTest {
+ private static final String DISPLAY_NAME = "Display Name";
+ private static final String LABEL_1 = "Work";
+ private static final String LABEL_2 = "Mobile";
+ private static final String PHONE_NUMBER_1 = "6502530000";
+ private static final String PHONE_NUMBER_2 = "6505550000";
+ private static final int TYPE = ContactsContract.CommonDataKinds.Phone.TYPE_MAIN;
+
+ private Context mContext;
+ private View mItemView;
+ private ContactListViewHolder mContactListViewHolder;
+ @Mock
+ private Contact mMockContact;
+ @Mock
+ private UiCallManager mMockUiCallManager;
+ @Mock
+ private ContactListAdapter.OnShowContactDetailListener mMockListener;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mContext = RuntimeEnvironment.application;
+
+ mItemView = LayoutInflater.from(mContext)
+ .inflate(R.layout.contact_list_item, null, false);
+ mContactListViewHolder = new ContactListViewHolder(mItemView, mMockListener);
+ }
+
+ @Test
+ public void testDisplayName() {
+ when(mMockContact.getDisplayName()).thenReturn(DISPLAY_NAME);
+ mContactListViewHolder.onBind(mMockContact);
+
+ assertThat(((TextView) mItemView.findViewById(R.id.title)).getText()).isEqualTo(
+ DISPLAY_NAME);
+ }
+
+ @Test
+ public void testLabel_withOnlyOneNumber_showLabel() {
+ PhoneNumber phoneNumber = PhoneNumber.newInstance(mContext, PHONE_NUMBER_1, 0, LABEL_1,
+ false, 0, null, null, 0);
+ when(mMockContact.getNumbers()).thenReturn(Arrays.asList(phoneNumber));
+ mContactListViewHolder.onBind(mMockContact);
+
+ assertThat(((TextView) mItemView.findViewById(R.id.text)).getText()).isEqualTo(LABEL_1);
+ }
+
+ @Test
+ public void testLabel_withOneNumberAndNumberHasNullLabel_showTypeLabel() {
+ PhoneNumber phoneNumber = PhoneNumber.newInstance(mContext, PHONE_NUMBER_1, TYPE, null,
+ false, 0, null, null, 0);
+ when(mMockContact.getNumbers()).thenReturn(Arrays.asList(phoneNumber));
+ mContactListViewHolder.onBind(mMockContact);
+
+ assertThat(((TextView) mItemView.findViewById(R.id.text)).getText()).isEqualTo(
+ mContext.getResources().getText(
+ ContactsContract.CommonDataKinds.Phone.getTypeLabelResource(TYPE)));
+ }
+
+ @Test
+ public void testLabel_withOneNumberAndGetNullLabel_showEmptyString() {
+ // There will not be situations where PhoneNumber gets a null label.
+ // But we keep this unit test to make sure the logic is correct.
+ PhoneNumber phoneNumber = mock(PhoneNumber.class);
+ when(phoneNumber.getLabel()).thenReturn(null);
+ when(mMockContact.getNumbers()).thenReturn(Arrays.asList(phoneNumber));
+ mContactListViewHolder.onBind(mMockContact);
+
+ assertThat(((TextView) mItemView.findViewById(R.id.text)).getText()).isEqualTo("");
+ }
+
+ @Test
+ public void testLabel_withMultipleNumbersAndNoPrimaryNumber_showMultipleLabel() {
+ PhoneNumber phoneNumber1 = PhoneNumber.newInstance(mContext, PHONE_NUMBER_1, 0, LABEL_1,
+ false, 0, null, null, 0);
+ PhoneNumber phoneNumber2 = PhoneNumber.newInstance(mContext, PHONE_NUMBER_2, 0, LABEL_2,
+ false, 0, null, null, 0);
+ when(mMockContact.getNumbers()).thenReturn(Arrays.asList(phoneNumber1, phoneNumber2));
+ when(mMockContact.hasPrimaryPhoneNumber()).thenReturn(false);
+ mContactListViewHolder.onBind(mMockContact);
+
+ assertThat(((TextView) mItemView.findViewById(R.id.text)).getText()).isEqualTo(
+ mContext.getString(R.string.type_multiple));
+ }
+
+ @Test
+ public void testLabel_withMultipleNumbersAndHasPrimaryNumber_showPrimaryNumberLabel() {
+ PhoneNumber phoneNumber1 = PhoneNumber.newInstance(mContext, PHONE_NUMBER_1, 0, LABEL_1,
+ false, 0, null, null, 0);
+ PhoneNumber phoneNumber2 = PhoneNumber.newInstance(mContext, PHONE_NUMBER_2, 0, LABEL_2,
+ false, 0, null, null, 0);
+ when(mMockContact.getNumbers()).thenReturn(Arrays.asList(phoneNumber1, phoneNumber2));
+ when(mMockContact.hasPrimaryPhoneNumber()).thenReturn(true);
+ when(mMockContact.getPrimaryPhoneNumber()).thenReturn(phoneNumber2);
+ mContactListViewHolder.onBind(mMockContact);
+
+ assertThat(((TextView) mItemView.findViewById(R.id.text)).getText()).isEqualTo(
+ mContext.getString(R.string.primary_number_description, LABEL_2));
+ }
+
+ @Test
+ public void testLabel_HasPrimaryNumberAndPrimaryNumberHasNullLabel_showPrimaryNumberLabel() {
+ PhoneNumber phoneNumber1 = PhoneNumber.newInstance(mContext, PHONE_NUMBER_1, 0, LABEL_1,
+ false, 0, null, null, 0);
+ PhoneNumber phoneNumber2 = PhoneNumber.newInstance(mContext, PHONE_NUMBER_2, TYPE, null,
+ false, 0, null, null, 0);
+ when(mMockContact.getNumbers()).thenReturn(Arrays.asList(phoneNumber1, phoneNumber2));
+ when(mMockContact.hasPrimaryPhoneNumber()).thenReturn(true);
+ when(mMockContact.getPrimaryPhoneNumber()).thenReturn(phoneNumber2);
+ mContactListViewHolder.onBind(mMockContact);
+
+ assertThat(((TextView) mItemView.findViewById(R.id.text)).getText()).isEqualTo(
+ mContext.getString(R.string.primary_number_description,
+ mContext.getResources().getText(
+ ContactsContract.CommonDataKinds.Phone.getTypeLabelResource(
+ TYPE))));
+ }
+
+ @Test
+ public void testLabel_HasPrimaryNumberButGetNullLabel_showMultipleLabel() {
+ // There will not be situations where PhoneNumber gets a null label.
+ // But we keep this unit test to make sure the logic is correct.
+ PhoneNumber phoneNumber1 = mock(PhoneNumber.class);
+ PhoneNumber phoneNumber2 = mock(PhoneNumber.class);
+ when(phoneNumber2.getLabel()).thenReturn(null);
+ when(mMockContact.getNumbers()).thenReturn(Arrays.asList(phoneNumber1, phoneNumber2));
+ when(mMockContact.hasPrimaryPhoneNumber()).thenReturn(true);
+ when(mMockContact.getPrimaryPhoneNumber()).thenReturn(phoneNumber2);
+ mContactListViewHolder.onBind(mMockContact);
+
+ assertThat(((TextView) mItemView.findViewById(R.id.text)).getText()).isEqualTo(
+ mContext.getString(R.string.primary_number_description, "null"));
+ }
+
+ @Test
+ public void testClickCallActionButton_ContactHasOneNumber_placeCall() {
+ UiCallManager.set(mMockUiCallManager);
+ PhoneNumber phoneNumber = PhoneNumber.newInstance(mContext, PHONE_NUMBER_1, 0, LABEL_1,
+ false, 0, null, null, 0);
+ when(mMockContact.getNumbers()).thenReturn(Arrays.asList(phoneNumber));
+ mContactListViewHolder.onBind(mMockContact);
+
+ View callActionView = mItemView.findViewById(R.id.call_action_id);
+ assertThat(callActionView.hasOnClickListeners()).isTrue();
+
+ callActionView.performClick();
+
+ ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
+ verify(mMockUiCallManager).placeCall(captor.capture());
+ assertThat(captor.getValue()).isEqualTo(PHONE_NUMBER_1);
+ }
+
+ @Test
+ public void testClickCallActionButton_HasMultipleNumbersAndNoPrimaryNumber_showAlertDialog() {
+ UiCallManager.set(mMockUiCallManager);
+ PhoneNumber phoneNumber1 = PhoneNumber.newInstance(mContext, PHONE_NUMBER_1, 0, LABEL_1,
+ false, 0, null, null, 0);
+ PhoneNumber phoneNumber2 = PhoneNumber.newInstance(mContext, PHONE_NUMBER_2, 0, LABEL_2,
+ false, 0, null, null, 0);
+ when(mMockContact.getNumbers()).thenReturn(Arrays.asList(phoneNumber1, phoneNumber2));
+ when(mMockContact.hasPrimaryPhoneNumber()).thenReturn(false);
+ mContactListViewHolder.onBind(mMockContact);
+
+ assertThat(ShadowAlertDialog.getLatestAlertDialog()).isNull();
+ View callActionView = mItemView.findViewById(R.id.call_action_id);
+ callActionView.performClick();
+
+ verify(mMockUiCallManager, never()).placeCall(any());
+ assertThat(ShadowAlertDialog.getLatestAlertDialog()).isNotNull();
+ }
+
+ @Test
+ public void testClickCallActionButton_HasMultipleNumbersAndPrimaryNumber_callPrimaryNumber() {
+ UiCallManager.set(mMockUiCallManager);
+ PhoneNumber phoneNumber1 = PhoneNumber.newInstance(mContext, PHONE_NUMBER_1, 0, LABEL_1,
+ false, 0, null, null, 0);
+ PhoneNumber phoneNumber2 = PhoneNumber.newInstance(mContext, PHONE_NUMBER_2, 0, LABEL_2,
+ false, 0, null, null, 0);
+ when(mMockContact.getNumbers()).thenReturn(Arrays.asList(phoneNumber1, phoneNumber2));
+ when(mMockContact.hasPrimaryPhoneNumber()).thenReturn(true);
+ when(mMockContact.getPrimaryPhoneNumber()).thenReturn(phoneNumber2);
+ mContactListViewHolder.onBind(mMockContact);
+
+ View callActionView = mItemView.findViewById(R.id.call_action_id);
+ assertThat(callActionView.hasOnClickListeners()).isTrue();
+
+ callActionView.performClick();
+
+ ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
+ verify(mMockUiCallManager).placeCall(captor.capture());
+ assertThat(captor.getValue()).isEqualTo(PHONE_NUMBER_2);
+ }
+
+ @Test
+ public void testClickShowContactDetailView_showContactDetail() {
+ mContactListViewHolder.onBind(mMockContact);
+
+ View showContactDetailActionView = mItemView.findViewById(R.id.show_contact_detail_id);
+ assertThat(showContactDetailActionView.hasOnClickListeners()).isTrue();
+
+ showContactDetailActionView.performClick();
+
+ ArgumentCaptor<Contact> captor = ArgumentCaptor.forClass(Contact.class);
+ verify(mMockListener).onShowContactDetail(captor.capture());
+ assertThat(captor.getValue()).isEqualTo(mMockContact);
+ }
+}
diff --git a/tests/robotests/src/com/android/car/dialer/ui/dialpad/DialpadFragmentTest.java b/tests/robotests/src/com/android/car/dialer/ui/dialpad/DialpadFragmentTest.java
new file mode 100644
index 00000000..6b349213
--- /dev/null
+++ b/tests/robotests/src/com/android/car/dialer/ui/dialpad/DialpadFragmentTest.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.dialpad;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.view.KeyEvent;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.TextView;
+
+import com.android.car.dialer.CarDialerRobolectricTestRunner;
+import com.android.car.dialer.FragmentTestActivity;
+import com.android.car.dialer.R;
+import com.android.car.dialer.TestDialerApplication;
+import com.android.car.dialer.telecom.UiCallManager;
+import com.android.car.dialer.testutils.ShadowCallLogCalls;
+import com.android.car.dialer.testutils.ShadowInMemoryPhoneBook;
+import com.android.car.telephony.common.Contact;
+import com.android.car.telephony.common.InMemoryPhoneBook;
+import com.android.car.telephony.common.TelecomUtils;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.Robolectric;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
+
+@RunWith(CarDialerRobolectricTestRunner.class)
+@Config(shadows = {ShadowCallLogCalls.class, ShadowInMemoryPhoneBook.class})
+public class DialpadFragmentTest {
+ private static final String DIAL_NUMBER = "6505551234";
+ private static final String DIAL_NUMBER_LONG = "650555123465055512346505551234";
+ private static final String SINGLE_DIGIT = "0";
+ private static final String SPEC_CHAR = "123=_=%^&";
+ private static final String DISPALY_NAME = "Display Name";
+
+ private DialpadFragment mDialpadFragment;
+ @Mock
+ private Contact mMockContact;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+
+ Context context = RuntimeEnvironment.application;
+ ((TestDialerApplication) context).setupInCallServiceImpl();
+ ((TestDialerApplication) context).initUiCallManager();
+ InMemoryPhoneBook.init(context);
+ }
+
+ @After
+ public void tearDown() {
+ UiCallManager.get().tearDown();
+ InMemoryPhoneBook.tearDown();
+ }
+
+ @Test
+ public void testOnCreateView_modeDialWithNormalDialNumber() {
+ mDialpadFragment = DialpadFragment.newPlaceCallDialpad();
+ startPlaceCallActivity();
+ mDialpadFragment.setDialedNumber(DIAL_NUMBER);
+
+ verifyButtonVisibility(View.VISIBLE, View.VISIBLE);
+ verifyTitleText(DIAL_NUMBER);
+ }
+
+ @Test
+ public void testOnCreateView_modeDialWithLongDialNumber() {
+ mDialpadFragment = DialpadFragment.newPlaceCallDialpad();
+ startPlaceCallActivity();
+ mDialpadFragment.setDialedNumber(DIAL_NUMBER_LONG);
+
+ verifyButtonVisibility(View.VISIBLE, View.VISIBLE);
+ verifyTitleText(DIAL_NUMBER_LONG.substring(
+ DIAL_NUMBER_LONG.length() - DialpadFragment.MAX_DIAL_NUMBER));
+ }
+
+ @Test
+ public void testOnCreateView_modeDialWithNullDialNumber() {
+ mDialpadFragment = DialpadFragment.newPlaceCallDialpad();
+ startPlaceCallActivity();
+ mDialpadFragment.setDialedNumber(null);
+
+ verifyButtonVisibility(View.VISIBLE, View.GONE);
+ verifyTitleText(mDialpadFragment.getContext().getString(R.string.dial_a_number));
+ }
+
+ @Test
+ public void testOnCreateView_modeDialWithEmptyDialNumber() {
+ mDialpadFragment = DialpadFragment.newPlaceCallDialpad();
+ startPlaceCallActivity();
+ mDialpadFragment.setDialedNumber("");
+
+ verifyButtonVisibility(View.VISIBLE, View.GONE);
+ verifyTitleText(mDialpadFragment.getContext().getString(R.string.dial_a_number));
+ }
+
+ @Test
+ public void testOnCreateView_modeDialWithSpecialChar() {
+ mDialpadFragment = DialpadFragment.newPlaceCallDialpad();
+ startPlaceCallActivity();
+ mDialpadFragment.setDialedNumber(SPEC_CHAR);
+
+ verifyButtonVisibility(View.VISIBLE, View.VISIBLE);
+ verifyTitleText(SPEC_CHAR);
+ }
+
+ @Test
+ public void testDeleteButton_normalString() {
+ mDialpadFragment = DialpadFragment.newPlaceCallDialpad();
+ startPlaceCallActivity();
+ mDialpadFragment.setDialedNumber(DIAL_NUMBER);
+
+ ImageButton deleteButton = mDialpadFragment.getView().findViewById(R.id.delete_button);
+ deleteButton.performClick();
+
+ verifyTitleText(DIAL_NUMBER.substring(0, DIAL_NUMBER.length() - 1));
+ }
+
+ @Test
+ public void testDeleteButton_oneDigit() {
+ mDialpadFragment = DialpadFragment.newPlaceCallDialpad();
+ startPlaceCallActivity();
+ mDialpadFragment.setDialedNumber(SINGLE_DIGIT);
+
+ ImageButton deleteButton = mDialpadFragment.getView().findViewById(R.id.delete_button);
+ deleteButton.performClick();
+ verifyTitleText(mDialpadFragment.getContext().getString(R.string.dial_a_number));
+ }
+
+ @Test
+ public void testDeleteButton_emptyString() {
+ mDialpadFragment = DialpadFragment.newPlaceCallDialpad();
+ startPlaceCallActivity();
+ mDialpadFragment.setDialedNumber("");
+
+ ImageButton deleteButton = mDialpadFragment.getView().findViewById(R.id.delete_button);
+ deleteButton.performClick();
+ verifyTitleText(mDialpadFragment.getContext().getString(R.string.dial_a_number));
+ }
+
+ @Test
+ public void testLongPressDeleteButton() {
+ mDialpadFragment = DialpadFragment.newPlaceCallDialpad();
+ startPlaceCallActivity();
+ mDialpadFragment.setDialedNumber(DIAL_NUMBER);
+
+ ImageButton deleteButton = mDialpadFragment.getView().findViewById(R.id.delete_button);
+
+ deleteButton.performLongClick();
+ verifyTitleText(mDialpadFragment.getContext().getString(R.string.dial_a_number));
+ }
+
+ @Test
+ public void testCallButton_emptyString() {
+ ShadowCallLogCalls.setLastOutgoingCall(DIAL_NUMBER);
+
+ mDialpadFragment = DialpadFragment.newPlaceCallDialpad();
+ startPlaceCallActivity();
+ mDialpadFragment.setDialedNumber("");
+
+ View callButton = mDialpadFragment.getView().findViewById(R.id.call_button);
+ callButton.performClick();
+ verifyTitleText(DIAL_NUMBER);
+ }
+
+ @Test
+ public void testOnKeyLongPressed_KeyCode0() {
+ mDialpadFragment = DialpadFragment.newPlaceCallDialpad();
+ startPlaceCallActivity();
+ mDialpadFragment.setDialedNumber(DIAL_NUMBER);
+
+ mDialpadFragment.onKeypadKeyLongPressed(KeyEvent.KEYCODE_0);
+ verifyTitleText(DIAL_NUMBER.substring(0, DIAL_NUMBER.length() - 1) + "+");
+ }
+
+ @Test
+ public void testDisplayName() {
+ ShadowInMemoryPhoneBook phoneBook = Shadow.extract(InMemoryPhoneBook.get());
+ when(mMockContact.getDisplayName()).thenReturn(DISPALY_NAME);
+ phoneBook.add(DIAL_NUMBER, mMockContact);
+
+ mDialpadFragment = DialpadFragment.newPlaceCallDialpad();
+ startPlaceCallActivity();
+ mDialpadFragment.setDialedNumber(DIAL_NUMBER);
+
+ TextView displayName = mDialpadFragment.getView().findViewById(R.id.display_name);
+ assertThat(displayName.getText()).isEqualTo(DISPALY_NAME);
+ }
+
+ private void startPlaceCallActivity() {
+ FragmentTestActivity fragmentTestActivity;
+ fragmentTestActivity = Robolectric.buildActivity(FragmentTestActivity.class)
+ .create().start().resume().get();
+ fragmentTestActivity.setFragment(mDialpadFragment);
+ }
+
+ private void verifyButtonVisibility(int callButtonVisibility, int deleteButtonVisibility) {
+ View callButton = mDialpadFragment.getView().findViewById(R.id.call_button);
+ ImageButton deleteButton = mDialpadFragment.getView().findViewById(R.id.delete_button);
+
+ assertThat(callButton.getVisibility()).isEqualTo(callButtonVisibility);
+ assertThat(deleteButton.getVisibility()).isEqualTo(deleteButtonVisibility);
+ }
+
+ private void verifyTitleText(String expectedText) {
+ expectedText = TelecomUtils.getFormattedNumber(mDialpadFragment.getContext(), expectedText);
+ TextView mTitleView = mDialpadFragment.getView().findViewById(R.id.title);
+ TelecomUtils.getFormattedNumber(mDialpadFragment.getContext(), null);
+ assertThat(mTitleView.getText().toString()).isEqualTo(expectedText);
+ }
+}
diff --git a/tests/robotests/src/com/android/car/dialer/ui/dialpad/InCallDialpadFragmentTest.java b/tests/robotests/src/com/android/car/dialer/ui/dialpad/InCallDialpadFragmentTest.java
new file mode 100644
index 00000000..baa131c1
--- /dev/null
+++ b/tests/robotests/src/com/android/car/dialer/ui/dialpad/InCallDialpadFragmentTest.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.dialpad;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.widget.TextView;
+
+import com.android.car.dialer.CarDialerRobolectricTestRunner;
+import com.android.car.dialer.FragmentTestActivity;
+import com.android.car.dialer.R;
+import com.android.car.dialer.TestDialerApplication;
+import com.android.car.dialer.testutils.ShadowCallLogCalls;
+import com.android.car.dialer.testutils.ShadowInMemoryPhoneBook;
+import com.android.car.dialer.ui.activecall.OngoingCallFragment;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.Robolectric;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+@RunWith(CarDialerRobolectricTestRunner.class)
+@Config(shadows = {ShadowCallLogCalls.class, ShadowInMemoryPhoneBook.class})
+public class InCallDialpadFragmentTest {
+
+ private InCallDialpadFragment mInCallDialpadFragment;
+
+ @Before
+ public void setup() {
+ ((TestDialerApplication) RuntimeEnvironment.application).setupInCallServiceImpl();
+ ((TestDialerApplication) RuntimeEnvironment.application).initUiCallManager();
+ }
+
+ @Test
+ public void testOnCreateView_modeInCall() {
+ startInCallActivity();
+
+ TextView mTitleView = mInCallDialpadFragment.getView().findViewById(R.id.title);
+ assertThat(mTitleView.getText()).isEqualTo("");
+ }
+
+ private void startInCallActivity() {
+ OngoingCallFragment ongoingCallFragment = new OngoingCallFragment();
+ FragmentTestActivity fragmentTestActivity = Robolectric.buildActivity(
+ FragmentTestActivity.class).create().start().resume().get();
+ fragmentTestActivity.setFragment(ongoingCallFragment);
+ mInCallDialpadFragment =
+ (InCallDialpadFragment) ongoingCallFragment.getChildFragmentManager()
+ .findFragmentById(R.id.incall_dialpad_fragment);
+ }
+
+}
diff --git a/tests/robotests/src/com/android/car/dialer/ui/favorite/FavoriteListFragmentTest.java b/tests/robotests/src/com/android/car/dialer/ui/favorite/FavoriteListFragmentTest.java
new file mode 100644
index 00000000..260bb481
--- /dev/null
+++ b/tests/robotests/src/com/android/car/dialer/ui/favorite/FavoriteListFragmentTest.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.favorite;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import androidx.lifecycle.MutableLiveData;
+
+import com.android.car.apps.common.widget.PagedRecyclerView;
+import com.android.car.dialer.CarDialerRobolectricTestRunner;
+import com.android.car.dialer.FragmentTestActivity;
+import com.android.car.dialer.R;
+import com.android.car.dialer.telecom.UiCallManager;
+import com.android.car.dialer.testutils.ShadowAndroidViewModelFactory;
+import com.android.car.telephony.common.Contact;
+import com.android.car.telephony.common.PhoneNumber;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.Robolectric;
+import org.robolectric.annotation.Config;
+
+import java.util.Arrays;
+import java.util.List;
+
+@Config(shadows = {ShadowAndroidViewModelFactory.class})
+@RunWith(CarDialerRobolectricTestRunner.class)
+public class FavoriteListFragmentTest {
+ private static final String RAW_NUMBER = "6502530000";
+
+ private FavoriteListFragment mFavoriteFragment;
+ private FavoriteContactViewHolder mViewHolder;
+ @Mock
+ private UiCallManager mMockUiCallManager;
+ @Mock
+ private Contact mMockContact;
+ @Mock
+ private FavoriteViewModel mMockFavoriteViewModel;
+ @Mock
+ private PhoneNumber mMockPhoneNumber;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ UiCallManager.set(mMockUiCallManager);
+
+ when(mMockPhoneNumber.getRawNumber()).thenReturn(RAW_NUMBER);
+ MutableLiveData<List<Contact>> favoriteContacts = new MutableLiveData<>();
+ favoriteContacts.setValue(Arrays.asList(mMockContact));
+ ShadowAndroidViewModelFactory.add(FavoriteViewModel.class, mMockFavoriteViewModel);
+ when(mMockFavoriteViewModel.getFavoriteContacts()).thenReturn(favoriteContacts);
+
+ mFavoriteFragment = FavoriteListFragment.newInstance();
+ FragmentTestActivity fragmentTestActivity = Robolectric.buildActivity(
+ FragmentTestActivity.class).create().resume().get();
+ fragmentTestActivity.setFragment(mFavoriteFragment);
+
+ PagedRecyclerView recyclerView = mFavoriteFragment.getView().findViewById(R.id.list_view);
+ // set up layout for recyclerView
+ recyclerView.layoutBothForTesting(0, 0, 100, 1000);
+ mViewHolder = (FavoriteContactViewHolder) recyclerView.findViewHolderForLayoutPosition(0);
+ }
+
+ @Test
+ public void testOnItemClick_contactHasPrimaryNumber_placeCall() {
+ when(mMockContact.getNumbers()).thenReturn(Arrays.asList(mMockPhoneNumber));
+ when(mMockContact.hasPrimaryPhoneNumber()).thenReturn(true);
+ when(mMockContact.getPrimaryPhoneNumber()).thenReturn(mMockPhoneNumber);
+
+ mViewHolder.itemView.performClick();
+
+ ArgumentCaptor<String> mCaptor = ArgumentCaptor.forClass(String.class);
+ verify(mMockUiCallManager).placeCall(mCaptor.capture());
+ assertThat(mCaptor.getValue()).isEqualTo(RAW_NUMBER);
+ }
+
+ @Test
+ public void testOnItemClick_contactHasOnlyOneNumber_placeCall() {
+ when(mMockContact.hasPrimaryPhoneNumber()).thenReturn(false);
+ when(mMockContact.getNumbers()).thenReturn(Arrays.asList(mMockPhoneNumber));
+
+ mViewHolder.itemView.performClick();
+
+ ArgumentCaptor<String> mCaptor = ArgumentCaptor.forClass(String.class);
+ verify(mMockUiCallManager).placeCall(mCaptor.capture());
+ assertThat(mCaptor.getValue()).isEqualTo(RAW_NUMBER);
+ }
+
+ @Test
+ public void testOnItemClick_contactHasMultiNumbers_notPlaceCall() {
+ when(mMockContact.hasPrimaryPhoneNumber()).thenReturn(false);
+ PhoneNumber otherMockPhoneNumber = mock(PhoneNumber.class);
+ when(mMockContact.getNumbers()).thenReturn(
+ Arrays.asList(mMockPhoneNumber, otherMockPhoneNumber));
+
+ mViewHolder.itemView.performClick();
+
+ verify(mMockUiCallManager, never()).placeCall(any());
+ }
+}
diff --git a/tests/robotests/src/com/android/car/dialer/ui/search/ContactResultsFragmentTest.java b/tests/robotests/src/com/android/car/dialer/ui/search/ContactResultsFragmentTest.java
new file mode 100644
index 00000000..2e9f8087
--- /dev/null
+++ b/tests/robotests/src/com/android/car/dialer/ui/search/ContactResultsFragmentTest.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.search;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.net.Uri;
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+import androidx.lifecycle.MutableLiveData;
+
+import com.android.car.apps.common.widget.PagedRecyclerView;
+import com.android.car.dialer.CarDialerRobolectricTestRunner;
+import com.android.car.dialer.FragmentTestActivity;
+import com.android.car.dialer.R;
+import com.android.car.dialer.testutils.ShadowAndroidViewModelFactory;
+import com.android.car.dialer.ui.contact.ContactDetailsFragment;
+import com.android.car.dialer.ui.contact.ContactDetailsViewModel;
+import com.android.car.telephony.common.Contact;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.Robolectric;
+import org.robolectric.annotation.Config;
+
+import java.util.Arrays;
+import java.util.List;
+
+@Config(shadows = {ShadowAndroidViewModelFactory.class})
+@RunWith(CarDialerRobolectricTestRunner.class)
+public class ContactResultsFragmentTest {
+
+ private static final String INITIAL_SEARCH_QUERY = "";
+ private static final String[] DISPLAY_NAMES = {"name1", "name2", "name3"};
+
+ private ContactResultsFragment mContactResultsFragment;
+ private FragmentTestActivity mFragmentTestActivity;
+ private PagedRecyclerView mListView;
+ private MutableLiveData<List<ContactDetails>> mContactSearchResultsLiveData;
+ @Mock
+ private ContactResultsViewModel mMockContactResultsViewModel;
+ @Mock
+ private ContactDetailsViewModel mMockContactDetailsViewModel;
+ @Mock
+ private Contact mMockContact;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ mContactSearchResultsLiveData = new MutableLiveData<>();
+ when(mMockContactResultsViewModel.getContactSearchResults())
+ .thenReturn(mContactSearchResultsLiveData);
+ ShadowAndroidViewModelFactory.add(
+ ContactResultsViewModel.class, mMockContactResultsViewModel);
+ }
+
+ @Test
+ public void testDisplaySearchResults_emptyResult() {
+ mContactResultsFragment = ContactResultsFragment.newInstance(INITIAL_SEARCH_QUERY);
+ setUpFragment();
+
+ assertThat(mListView.findViewHolderForLayoutPosition(0)).isNull();
+ }
+
+ @Test
+ public void testDisplaySearchResults_multipleResults() {
+ ContactDetails contactDetails1 = new ContactDetails(DISPLAY_NAMES[0], "", mock(Uri.class));
+ ContactDetails contactDetails2 = new ContactDetails(DISPLAY_NAMES[1], "", mock(Uri.class));
+ ContactDetails contactDetails3 = new ContactDetails(DISPLAY_NAMES[2], "", mock(Uri.class));
+ mContactSearchResultsLiveData.setValue(
+ Arrays.asList(contactDetails1, contactDetails2, contactDetails3));
+
+ mContactResultsFragment = ContactResultsFragment.newInstance(INITIAL_SEARCH_QUERY);
+ setUpFragment();
+
+ verifyChildAt(0);
+ verifyChildAt(1);
+ verifyChildAt(2);
+ }
+
+ @Test
+ public void testClickSearchResult_showContactDetailPage() {
+ ContactDetails contactDetails1 = new ContactDetails(DISPLAY_NAMES[0], "", mock(Uri.class));
+ ContactDetails contactDetails2 = new ContactDetails(DISPLAY_NAMES[1], "", mock(Uri.class));
+ ContactDetails contactDetails3 = new ContactDetails(DISPLAY_NAMES[2], "", mock(Uri.class));
+ mContactSearchResultsLiveData.setValue(
+ Arrays.asList(contactDetails1, contactDetails2, contactDetails3));
+
+ MutableLiveData<Contact> contactDetailLiveData = new MutableLiveData<>();
+ contactDetailLiveData.setValue(mMockContact);
+ ShadowAndroidViewModelFactory
+ .add(ContactDetailsViewModel.class, mMockContactDetailsViewModel);
+ when(mMockContactDetailsViewModel.getContactDetails(any()))
+ .thenReturn(contactDetailLiveData);
+
+ mContactResultsFragment = ContactResultsFragment.newInstance(INITIAL_SEARCH_QUERY);
+ setUpFragment();
+
+ mListView.findViewHolderForLayoutPosition(1).itemView.findViewById(R.id.contact_result)
+ .performClick();
+
+ // verify contact detail is shown.
+ verifyShowContactDetail();
+ }
+
+ private void setUpFragment() {
+ mFragmentTestActivity = Robolectric.buildActivity(
+ FragmentTestActivity.class).create().resume().get();
+ mFragmentTestActivity.setFragment(mContactResultsFragment);
+
+ mListView = mContactResultsFragment.getView().findViewById(R.id.list_view);
+ // Set up layout for recyclerView
+ mListView.layoutBothForTesting(0, 0, 100, 1000);
+ }
+
+ private void verifyChildAt(int position) {
+ View childView = mListView.findViewHolderForLayoutPosition(position).itemView;
+
+ assertThat(childView).isNotNull();
+ assertThat(childView.findViewById(R.id.contact_result).hasOnClickListeners()).isTrue();
+ assertThat(((TextView) childView.findViewById(R.id.contact_name)).getText())
+ .isEqualTo(DISPLAY_NAMES[position]);
+ }
+
+ private void verifyShowContactDetail() {
+ FragmentManager manager = mFragmentTestActivity.getSupportFragmentManager();
+ String tag = manager.getBackStackEntryAt(manager.getBackStackEntryCount() - 1).getName();
+ Fragment fragment = manager.findFragmentByTag(tag);
+ assertThat(fragment instanceof ContactDetailsFragment).isTrue();
+ }
+}
diff --git a/tests/robotests/src/com/android/car/dialer/ui/settings/common/SettingsListPreferenceDialogFragmentTest.java b/tests/robotests/src/com/android/car/dialer/ui/settings/common/SettingsListPreferenceDialogFragmentTest.java
new file mode 100644
index 00000000..5b49713a
--- /dev/null
+++ b/tests/robotests/src/com/android/car/dialer/ui/settings/common/SettingsListPreferenceDialogFragmentTest.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.settings.common;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Bundle;
+
+import androidx.preference.ListPreference;
+
+import com.android.car.dialer.CarDialerRobolectricTestRunner;
+import com.android.car.dialer.FragmentTestActivity;
+import com.android.car.dialer.R;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.android.controller.ActivityController;
+import org.robolectric.shadows.ShadowAlertDialog;
+import org.robolectric.shadows.ShadowApplication;
+
+/** Unit test for {@link SettingsListPreferenceDialogFragment}. */
+@RunWith(CarDialerRobolectricTestRunner.class)
+public class SettingsListPreferenceDialogFragmentTest {
+
+ private ActivityController<FragmentTestActivity> mTestActivityController;
+ private FragmentTestActivity mTestActivity;
+ private ListPreference mPreference;
+ private SettingsListPreferenceDialogFragment mFragment;
+
+ @Before
+ public void setUp() {
+ Context context = RuntimeEnvironment.application;
+
+ mTestActivityController = ActivityController.of(new FragmentTestActivity());
+ mTestActivity = mTestActivityController.get();
+ mTestActivityController.setup();
+
+ SettingsPreferenceDialogFragmentTest.TestTargetFragment targetFragment =
+ new SettingsPreferenceDialogFragmentTest.TestTargetFragment();
+ mTestActivity.setFragment(targetFragment);
+ mPreference = new ListPreference(context);
+ mPreference.setDialogLayoutResource(R.layout.preference_dialog_edittext);
+ mPreference.setKey("key");
+ mPreference.setEntries(R.array.entries);
+ mPreference.setEntryValues(R.array.entry_values);
+ targetFragment.getPreferenceScreen().addPreference(mPreference);
+
+ mFragment = SettingsListPreferenceDialogFragment.newInstance(mPreference.getKey());
+ mFragment.setTargetFragment(targetFragment, /* requestCode= */ 0);
+ }
+
+ @Test
+ public void dialogPopulatedWithPreferenceEntries() {
+ mTestActivity.showDialog(mFragment, /* tag= */ null);
+
+ assertThat(getShadowAlertDialog().getItems()).isEqualTo(mPreference.getEntries());
+ }
+
+ @Test
+ public void itemSelected_dismissesDialog() {
+ mTestActivity.showDialog(mFragment, /* tag= */ null);
+
+ getShadowAlertDialog().clickOnItem(1);
+
+ assertThat(getShadowAlertDialog().hasBeenDismissed()).isTrue();
+ }
+
+ @Test
+ public void itemSelected_setsPreferenceValue() {
+ mPreference.setValueIndex(0);
+ mTestActivity.showDialog(mFragment, /* tag= */ null);
+
+ getShadowAlertDialog().clickOnItem(1);
+
+ assertThat(mPreference.getValue()).isEqualTo(mPreference.getEntryValues()[1]);
+ }
+
+ @Test
+ public void onDialogClosed_negativeResult_doesNothing() {
+ mPreference.setValueIndex(0);
+ mTestActivity.showDialog(mFragment, /* tag= */ null);
+ AlertDialog dialog = ShadowAlertDialog.getLatestAlertDialog();
+
+ dialog.getButton(DialogInterface.BUTTON_NEGATIVE).performClick();
+
+ assertThat(mPreference.getValue()).isEqualTo(mPreference.getEntryValues()[0]);
+ }
+
+ @Test
+ public void instanceStateRetained() {
+ mPreference.setValueIndex(0);
+ mTestActivity.showDialog(mFragment, /* tag= */ null);
+
+ // Save instance state.
+ Bundle outState = new Bundle();
+ mTestActivityController.pause().saveInstanceState(outState).stop();
+
+ // Recreate everything with saved state.
+ mTestActivityController = ActivityController.of(new FragmentTestActivity());
+ mTestActivity = mTestActivityController.get();
+ mTestActivityController.setup(outState);
+
+ // Ensure saved entries were applied.
+ assertThat(getShadowAlertDialog().getItems()).isEqualTo(mPreference.getEntries());
+ }
+
+ private ShadowAlertDialog getShadowAlertDialog() {
+ return ShadowApplication.getInstance().getLatestAlertDialog();
+ }
+}
diff --git a/tests/robotests/src/com/android/car/dialer/ui/settings/common/SettingsPreferenceDialogFragmentTest.java b/tests/robotests/src/com/android/car/dialer/ui/settings/common/SettingsPreferenceDialogFragmentTest.java
new file mode 100644
index 00000000..6e49bc96
--- /dev/null
+++ b/tests/robotests/src/com/android/car/dialer/ui/settings/common/SettingsPreferenceDialogFragmentTest.java
@@ -0,0 +1,251 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.dialer.ui.settings.common;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.view.View;
+import android.view.WindowManager;
+
+import androidx.preference.DialogPreference;
+import androidx.preference.PreferenceFragmentCompat;
+
+import com.android.car.dialer.CarDialerRobolectricTestRunner;
+import com.android.car.dialer.FragmentTestActivity;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.android.controller.ActivityController;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.ShadowAlertDialog;
+import org.robolectric.shadows.ShadowApplication;
+import org.robolectric.shadows.ShadowWindow;
+
+/** Unit test for {@link SettingsPreferenceDialogFragment}. */
+@RunWith(CarDialerRobolectricTestRunner.class)
+public class SettingsPreferenceDialogFragmentTest {
+
+ private ActivityController<FragmentTestActivity> mTestActivityController;
+ private FragmentTestActivity mTestActivity;
+ private DialogPreference mPreference;
+ private TestSettingsPreferenceDialogFragment mFragment;
+
+ @Before
+ public void setUp() {
+ Context context = RuntimeEnvironment.application;
+
+ mTestActivityController = ActivityController.of(new FragmentTestActivity());
+ mTestActivity = mTestActivityController.get();
+ mTestActivityController.setup();
+
+ TestTargetFragment targetFragment = new TestTargetFragment();
+ mTestActivity.setFragment(targetFragment);
+ mPreference = new TestDialogPreference(context);
+ mPreference.setKey("key");
+ targetFragment.getPreferenceScreen().addPreference(mPreference);
+
+ mFragment = TestSettingsPreferenceDialogFragment.newInstance(mPreference.getKey());
+ mFragment.setTargetFragment(targetFragment, /* requestCode= */ 0);
+ }
+
+ @Test
+ public void dialogFieldsPopulatedWithPreferenceFields() {
+ mPreference.setDialogTitle("title");
+ mPreference.setPositiveButtonText("positive button text");
+ mPreference.setNegativeButtonText("negative button text");
+ mPreference.setDialogMessage("dialog message");
+
+ mTestActivity.showDialog(mFragment, /* tag= */ null);
+
+ assertThat(getShadowAlertDialog().getTitle()).isEqualTo(mPreference.getDialogTitle());
+ assertThat(ShadowAlertDialog.getLatestAlertDialog().getButton(
+ DialogInterface.BUTTON_POSITIVE).getText()).isEqualTo(
+ mPreference.getPositiveButtonText());
+ assertThat(ShadowAlertDialog.getLatestAlertDialog().getButton(
+ DialogInterface.BUTTON_NEGATIVE).getText()).isEqualTo(
+ mPreference.getNegativeButtonText());
+ assertThat(getShadowAlertDialog().getMessage()).isEqualTo(mPreference.getDialogMessage());
+ }
+
+ @Test
+ public void dialogMessage_messageViewShown() {
+ mPreference.setDialogTitle("title");
+ mPreference.setPositiveButtonText("positive button text");
+ mPreference.setNegativeButtonText("negative button text");
+ mPreference.setDialogMessage("dialog message");
+
+ mTestActivity.showDialog(mFragment, /* tag= */ null);
+ View messageView = ShadowAlertDialog.getLatestAlertDialog().findViewById(
+ android.R.id.message);
+
+ assertThat(messageView.getVisibility()).isEqualTo(View.VISIBLE);
+ }
+
+ @Test
+ public void noDialogMessage_messageViewHidden() {
+ mPreference.setDialogTitle("title");
+ mPreference.setPositiveButtonText("positive button text");
+ mPreference.setNegativeButtonText("negative button text");
+
+ mTestActivity.showDialog(mFragment, /* tag= */ null);
+ View messageView = ShadowAlertDialog.getLatestAlertDialog().findViewById(
+ android.R.id.message);
+
+ assertThat(messageView.getVisibility()).isEqualTo(View.GONE);
+ }
+
+ @Test
+ public void getPreference_returnsDialogRequestingPreference() {
+ mTestActivity.showDialog(mFragment, /* tag= */ null);
+
+ assertThat(mFragment.getPreference()).isEqualTo(mPreference);
+ }
+
+ @Test
+ public void dialogClosed_positiveButton_callsOnDialogClosed() {
+ mTestActivity.showDialog(mFragment, /* tag= */ null);
+ AlertDialog dialog = ShadowAlertDialog.getLatestAlertDialog();
+
+ dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick();
+
+ assertThat(mFragment.getDialogClosedResult()).isEqualTo(Boolean.TRUE);
+ }
+
+ @Test
+ public void dialogClosed_negativeButton_callsOnDialogClosed() {
+ mTestActivity.showDialog(mFragment, /* tag= */ null);
+ AlertDialog dialog = ShadowAlertDialog.getLatestAlertDialog();
+
+ dialog.getButton(DialogInterface.BUTTON_NEGATIVE).performClick();
+
+ assertThat(mFragment.getDialogClosedResult()).isEqualTo(Boolean.FALSE);
+ }
+
+ @Test
+ public void subclassNeedsInputMethod_softInputModeSetOnWindow() {
+ mFragment.setNeedsInputMethod(true);
+ mTestActivity.showDialog(mFragment, /* tag= */ null);
+
+ assertThat(getShadowWindowFromDialog(
+ ShadowAlertDialog.getLatestAlertDialog()).getSoftInputMode()).isEqualTo(
+ WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
+ }
+
+ @Test
+ public void subclassDoesNotNeedInputMethod_noWindowSoftInputMode() {
+ mFragment.setNeedsInputMethod(false);
+ mTestActivity.showDialog(mFragment, /* tag= */ null);
+
+ assertThat(getShadowWindowFromDialog(
+ ShadowAlertDialog.getLatestAlertDialog()).getSoftInputMode()).isEqualTo(0);
+ }
+
+ @Test
+ public void instanceStateRetained() {
+ String dialogTitle = "dialog title";
+ String positiveButtonText = "positive button text";
+ String negativeButtonText = "negative button text";
+ String dialogMessage = "dialog message";
+ mPreference.setDialogTitle(dialogTitle);
+ mPreference.setPositiveButtonText(positiveButtonText);
+ mPreference.setNegativeButtonText(negativeButtonText);
+ mPreference.setDialogMessage(dialogMessage);
+
+ mTestActivity.showDialog(mFragment, /* tag= */ null);
+
+ // Save instance state.
+ Bundle outState = new Bundle();
+ mTestActivityController.pause().saveInstanceState(outState).stop();
+
+ // Recreate everything with saved state.
+ mTestActivityController = ActivityController.of(new FragmentTestActivity());
+ mTestActivity = mTestActivityController.get();
+ mTestActivityController.setup(outState);
+
+ // Ensure saved fields were applied.
+ assertThat(getShadowAlertDialog().getTitle()).isEqualTo(dialogTitle);
+ assertThat(ShadowAlertDialog.getLatestAlertDialog().getButton(
+ DialogInterface.BUTTON_POSITIVE).getText()).isEqualTo(positiveButtonText);
+ assertThat(ShadowAlertDialog.getLatestAlertDialog().getButton(
+ DialogInterface.BUTTON_NEGATIVE).getText()).isEqualTo(negativeButtonText);
+ assertThat(getShadowAlertDialog().getMessage()).isEqualTo(dialogMessage);
+ }
+
+ private ShadowAlertDialog getShadowAlertDialog() {
+ return ShadowApplication.getInstance().getLatestAlertDialog();
+ }
+
+ private ShadowWindow getShadowWindowFromDialog(AlertDialog dialog) {
+ return (ShadowWindow) Shadow.extract(dialog.getWindow());
+ }
+
+ /** Concrete implementation of the fragment under test. */
+ public static class TestSettingsPreferenceDialogFragment extends
+ SettingsPreferenceDialogFragment {
+
+ private Boolean mDialogClosedResult;
+ private boolean mNeedsInputMethod;
+
+ static TestSettingsPreferenceDialogFragment newInstance(String key) {
+ TestSettingsPreferenceDialogFragment fragment =
+ new TestSettingsPreferenceDialogFragment();
+ Bundle b = new Bundle(/* capacity= */ 1);
+ b.putString(ARG_KEY, key);
+ fragment.setArguments(b);
+ return fragment;
+ }
+
+ @Override
+ protected boolean needInputMethod() {
+ return mNeedsInputMethod;
+ }
+
+ void setNeedsInputMethod(boolean needsInputMethod) {
+ mNeedsInputMethod = needsInputMethod;
+ }
+
+ @Override
+ protected void onDialogClosed(boolean positiveResult) {
+ mDialogClosedResult = positiveResult;
+ }
+
+ Boolean getDialogClosedResult() {
+ return mDialogClosedResult;
+ }
+ }
+
+ /** Concrete implementation of {@link DialogPreference} for testing use. */
+ private static class TestDialogPreference extends DialogPreference {
+ TestDialogPreference(Context context) {
+ super(context);
+ }
+ }
+
+ /** Simple {@link PreferenceFragmentCompat} implementation to serve as the target fragment. */
+ public static class TestTargetFragment extends PreferenceFragmentCompat {
+ @Override
+ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
+ setPreferenceScreen(getPreferenceManager().createPreferenceScreen(getContext()));
+ }
+ }
+}
diff --git a/tests/robotests/src/com/android/car/dialer/ui/warning/NoHfpFragmentTest.java b/tests/robotests/src/com/android/car/dialer/ui/warning/NoHfpFragmentTest.java
new file mode 100644
index 00000000..89df20a0
--- /dev/null
+++ b/tests/robotests/src/com/android/car/dialer/ui/warning/NoHfpFragmentTest.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.dialer.ui.warning;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.car.Car;
+import android.car.CarNotConnectedException;
+import android.car.drivingstate.CarUxRestrictions;
+import android.car.drivingstate.CarUxRestrictionsManager;
+import android.view.View;
+import android.widget.TextView;
+
+import com.android.car.dialer.CarDialerRobolectricTestRunner;
+import com.android.car.dialer.FragmentTestActivity;
+import com.android.car.dialer.R;
+import com.android.car.dialer.TestDialerApplication;
+import com.android.car.dialer.telecom.UiBluetoothMonitor;
+import com.android.car.dialer.telecom.UiCallManager;
+import com.android.car.dialer.testutils.ShadowCar;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.Robolectric;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+@RunWith(CarDialerRobolectricTestRunner.class)
+@Config(shadows = ShadowCar.class)
+public class NoHfpFragmentTest {
+ private static final String ERROR_MSG = "ERROR!!!";
+ private static final String UPDATED_ERROR_MSG = "ANOTHER ERROR!!!!";
+
+ private NoHfpFragment mNoHfpFragment;
+ private FragmentTestActivity mFragmentTestActivity;
+
+ @Mock
+ private Car mMockCar;
+ @Mock
+ private CarUxRestrictionsManager mMockCarUxRestrictionsManager;
+ @Mock
+ private CarUxRestrictions mMockCarUxRestrictions;
+
+ @Before
+ public void setup() throws CarNotConnectedException {
+ MockitoAnnotations.initMocks(this);
+
+ ((TestDialerApplication) RuntimeEnvironment.application).initUiCallManager();
+ UiBluetoothMonitor.init(RuntimeEnvironment.application);
+
+ when(mMockCarUxRestrictionsManager.getCurrentCarUxRestrictions()).thenReturn(
+ mMockCarUxRestrictions);
+ when(mMockCar.getCarManager(Car.CAR_UX_RESTRICTION_SERVICE)).thenReturn(
+ mMockCarUxRestrictionsManager);
+ ShadowCar.setCar(mMockCar);
+
+ mNoHfpFragment = NoHfpFragment.newInstance(ERROR_MSG);
+ mFragmentTestActivity = Robolectric.buildActivity(
+ FragmentTestActivity.class).create().start().resume().get();
+ mFragmentTestActivity.setFragment(mNoHfpFragment);
+ }
+
+ @Test
+ public void createView_notNull() {
+ assertThat(mNoHfpFragment.getView()).isNotNull();
+ }
+
+ @Test
+ public void createView_displayErrorMsg() {
+ View rootView = mNoHfpFragment.getView();
+ TextView errorMsgView = rootView.findViewById(R.id.error_string);
+ assertThat(errorMsgView.getText()).isEqualTo(ERROR_MSG);
+ }
+
+ @Test
+ public void setErrorMsg_updateErrorMsgView() {
+ mNoHfpFragment.setErrorMessage(UPDATED_ERROR_MSG);
+
+ View rootView = mNoHfpFragment.getView();
+ TextView errorMsgView = rootView.findViewById(R.id.error_string);
+ assertThat(errorMsgView.getText()).isEqualTo(UPDATED_ERROR_MSG);
+ }
+
+ @After
+ public void tearDown() {
+ UiBluetoothMonitor.get().tearDown();
+ UiCallManager.get().tearDown();
+ ShadowCar.setCar(null);
+ }
+}