diff options
author | android-build-team Robot <android-build-team-robot@google.com> | 2019-08-15 04:38:48 +0000 |
---|---|---|
committer | android-build-team Robot <android-build-team-robot@google.com> | 2019-08-15 04:38:48 +0000 |
commit | 3224f9d299a7bcfbca10c767625ea79fc828c095 (patch) | |
tree | b84ede97ae4e25e24f41b48ec0f72ce0bab9a5fe | |
parent | d09ff8acda8ba990c6a124595e6d7505efa77d40 (diff) | |
parent | ecf167812fc2d94aaee73d3128b003e75ff61131 (diff) | |
download | platform_packages_apps_Car_Dialer-android10-mainline-a-release.tar.gz platform_packages_apps_Car_Dialer-android10-mainline-a-release.tar.bz2 platform_packages_apps_Car_Dialer-android10-mainline-a-release.zip |
Snap for 5803298 from ecf167812fc2d94aaee73d3128b003e75ff61131 to qt-aml-releaseandroid-mainline-10.0.0_r2android10-mainline-a-release
Change-Id: I7dd461df7cb6f52be0d6cc0bbbf6119377937e8f
103 files changed, 2458 insertions, 950 deletions
@@ -60,7 +60,29 @@ LOCAL_STATIC_JAVA_LIBRARIES := \ car-glide \ car-glide-disklrucache \ car-gifdecoder \ - libphonenumber + libphonenumber \ + androidx.sqlite_sqlite-framework \ + androidx.sqlite_sqlite \ + car-androidx-room-common-nodeps \ + car-androidx-room-runtime-nodeps + +LOCAL_ANNOTATION_PROCESSORS := \ + car-androidx-annotation-nodeps \ + car-androidx-room-common-nodeps \ + car-androidx-room-compiler-nodeps \ + car-androidx-room-migration-nodeps \ + car-antlr4-nodeps \ + car-apache-commons-codec-nodeps \ + car-auto-common-nodeps \ + car-javapoet-nodeps \ + car-jetbrains-annotations-nodeps \ + car-kotlin-metadata-nodeps \ + car-sqlite-jdbc-nodeps \ + guava-21.0 \ + kotlin-stdlib + +LOCAL_ANNOTATION_PROCESSOR_CLASSES := \ + androidx.room.RoomProcessor LOCAL_PROGUARD_ENABLED := disabled @@ -109,7 +131,29 @@ LOCAL_STATIC_JAVA_LIBRARIES := \ car-glide \ car-glide-disklrucache \ car-gifdecoder \ - libphonenumber + libphonenumber \ + androidx.sqlite_sqlite-framework \ + androidx.sqlite_sqlite \ + car-androidx-room-common-nodeps \ + car-androidx-room-runtime-nodeps + +LOCAL_ANNOTATION_PROCESSORS := \ + car-androidx-annotation-nodeps \ + car-androidx-room-common-nodeps \ + car-androidx-room-compiler-nodeps \ + car-androidx-room-migration-nodeps \ + car-antlr4-nodeps \ + car-apache-commons-codec-nodeps \ + car-auto-common-nodeps \ + car-javapoet-nodeps \ + car-jetbrains-annotations-nodeps \ + car-kotlin-metadata-nodeps \ + car-sqlite-jdbc-nodeps \ + guava-21.0 \ + kotlin-stdlib + +LOCAL_ANNOTATION_PROCESSOR_CLASSES := \ + androidx.room.RoomProcessor LOCAL_PROGUARD_ENABLED := disabled diff --git a/AndroidManifest.xml b/AndroidManifest.xml index cdde5cf1..2349b5fb 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -115,5 +115,13 @@ <action android:name="android.telecom.action.SHOW_MISSED_CALLS_NOTIFICATION"/> </intent-filter> </receiver> + + <receiver + android:directBootAware="true" + android:name="com.android.car.dialer.storage.BluetoothBondedListReceiver"> + <intent-filter> + <action android:name="android.bluetooth.device.action.BOND_STATE_CHANGED"/> + </intent-filter> + </receiver> </application> </manifest> diff --git a/res/drawable/ic_add_favorite.xml b/res/drawable/ic_add_favorite.xml index 20d741cc..903f56d7 100644 --- a/res/drawable/ic_add_favorite.xml +++ b/res/drawable/ic_add_favorite.xml @@ -17,7 +17,7 @@ <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"> + <shape android:shape="oval"> <solid android:color="@color/add_favorite_background_color"/> </shape> </item> diff --git a/res/drawable/ic_bluetooth_disable.xml b/res/drawable/ic_bluetooth_disable.xml deleted file mode 100644 index fbbeadaf..00000000 --- a/res/drawable/ic_bluetooth_disable.xml +++ /dev/null @@ -1,24 +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 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/layout-h456dp/keypad_dividers.xml b/res/layout-h456dp/keypad_dividers.xml new file mode 100644 index 00000000..196543ab --- /dev/null +++ b/res/layout-h456dp/keypad_dividers.xml @@ -0,0 +1,68 @@ +<?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. + --> +<merge + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + <!-- 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"/> + +</merge> diff --git a/res/layout-port/dialpad_fragment.xml b/res/layout-port/dialpad_fragment.xml index 8eef7c94..1581dbdc 100644 --- a/res/layout-port/dialpad_fragment.xml +++ b/res/layout-port/dialpad_fragment.xml @@ -30,11 +30,12 @@ <TextView android:id="@+id/title" android:layout_width="0dp" - android:layout_height="wrap_content" - android:focusable="true" - android:singleLine="true" + android:layout_height="@dimen/dialpad_info_title_container_size" + android:maxLines="1" android:textAppearance="@style/TextAppearance.DialNumber" - android:layout_marginStart="@dimen/dialpad_info_edge_padding_size" + android:autoSizeTextType="uniform" + android:autoSizeMinTextSize="@dimen/dialpad_info_title_text_size_min" + android:autoSizeMaxTextSize="@dimen/dialpad_info_title_text_size_max" app:layout_constraintVertical_chainStyle="packed" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toTopOf="@id/dialpad_fragment" diff --git a/res/layout-port/user_profile_large.xml b/res/layout-port/user_profile_large.xml index 02d7e807..fbc601c6 100644 --- a/res/layout-port/user_profile_large.xml +++ b/res/layout-port/user_profile_large.xml @@ -35,11 +35,15 @@ limitations under the License. android:textAppearance="@style/TextAppearance.InCallUserPhoneNumber" android:singleLine="true" android:paddingTop="@dimen/in_call_phone_number_margin_top"/> + + <!-- A textView without a fixed width will call requestLayout() every time its text is changed --> + <!-- So we have to make this Chronometer match_parent to avoid redrawing the whole screen --> <Chronometer android:id="@+id/user_profile_call_state" - android:layout_width="wrap_content" + android:layout_width="match_parent" android:layout_height="wrap_content" android:textAppearance="@style/TextAppearance.InCallState" android:singleLine="true" - android:paddingTop="@dimen/in_call_state_margin_top"/> + android:paddingTop="@dimen/in_call_state_margin_top" + android:gravity="center"/> </LinearLayout> diff --git a/res/layout/add_favorite_list_item.xml b/res/layout/add_favorite_list_item.xml new file mode 100644 index 00000000..c81fa3b0 --- /dev/null +++ b/res/layout/add_favorite_list_item.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. +--> +<com.android.car.apps.common.UxrButton + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/title" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/add_favorite_button" + android:textAppearance="?android:attr/textAppearanceLarge" + android:singleLine="true" + android:gravity="top|center_horizontal" + android:background="?android:attr/selectableItemBackground" + android:paddingStart="0dp" + android:paddingEnd="0dp" + android:drawableTop="@drawable/ic_add_favorite" + android:drawablePadding="@dimen/favorites_avatar_margin_bottom"/> diff --git a/res/layout/add_favorite_number_list_item.xml b/res/layout/add_favorite_number_list_item.xml new file mode 100644 index 00000000..0cbdb82a --- /dev/null +++ b/res/layout/add_favorite_number_list_item.xml @@ -0,0 +1,57 @@ +<?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="@dimen/add_favorite_number_list_height" + android:background="?android:attr/selectableItemBackground" + android:paddingStart="@dimen/add_favorite_number_list_padding" + android:paddingEnd="@dimen/add_favorite_number_list_padding"> + + <TextView + android:id="@+id/phone_number" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:textAppearance="?android:attr/textAppearanceMedium" + app:layout_constraintVertical_chainStyle="packed" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toTopOf="@+id/phone_number_description" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toStartOf="@+id/phone_number_checkbox"/> + + <TextView + android:id="@+id/phone_number_description" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:textAppearance="?android:attr/textAppearanceSmall" + app:layout_constraintTop_toBottomOf="@id/phone_number" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toStartOf="@id/phone_number_checkbox"/> + + <ImageView + android:id="@+id/phone_number_checkbox" + android:src="@drawable/ic_favorite_activatable" + android:layout_width="@dimen/primary_icon_size" + android:layout_height="@dimen/primary_icon_size" + android:tint="@color/primary_icon_color" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent"/> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/res/layout/add_to_favorite_dialog.xml b/res/layout/add_to_favorite_dialog.xml new file mode 100644 index 00000000..d8ec551c --- /dev/null +++ b/res/layout/add_to_favorite_dialog.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. + --> + +<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:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/> diff --git a/res/layout/contact_details_number.xml b/res/layout/contact_details_number.xml index 3a18d9d3..c7ef8cb5 100644 --- a/res/layout/contact_details_number.xml +++ b/res/layout/contact_details_number.xml @@ -35,14 +35,14 @@ <TextView android:id="@+id/title" - android:textAppearance="?android:attr/textAppearanceLarge" + android:textAppearance="@style/TextAppearance.ContactDetailsListTitle" android:layout_width="match_parent" android:layout_height="wrap_content" android:singleLine="true"/> <TextView android:id="@id/text" - android:textAppearance="?android:attr/textAppearanceSmall" + android:textAppearance="@style/TextAppearance.ContactDetailsListSubtitle" android:layout_width="match_parent" android:layout_height="wrap_content" android:singleLine="true"/> diff --git a/res/layout/contact_list_item.xml b/res/layout/contact_list_item.xml index e106ab2f..6ed6e1c9 100644 --- a/res/layout/contact_list_item.xml +++ b/res/layout/contact_list_item.xml @@ -18,85 +18,23 @@ 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"> + android:layout_height="wrap_content"> - <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" + <include + layout="@layout/header_item" + android:id="@+id/header" 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_constraintBottom_toTopOf="@+id/user_profile_container" 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" + <include + layout="@layout/contact_user_profile" + android:id="@+id/user_profile_container" + android:layout_width="match_parent" + android:layout_height="@dimen/contact_list_item_height" + app:layout_constraintTop_toBottomOf="@id/header" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintStart_toEndOf="@id/contact_list_guideline_end" + app:layout_constraintStart_toStartOf="parent" 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> +</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file diff --git a/res/layout/contact_user_profile.xml b/res/layout/contact_user_profile.xml new file mode 100644 index 00000000..aa6e1d8f --- /dev/null +++ b/res/layout/contact_user_profile.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="match_parent"> + + <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/dialpad_info.xml b/res/layout/dialpad_info.xml index 93eaf778..d9411da6 100644 --- a/res/layout/dialpad_info.xml +++ b/res/layout/dialpad_info.xml @@ -29,10 +29,13 @@ <TextView android:id="@+id/title" android:layout_width="0dp" - android:layout_height="wrap_content" - android:singleLine="true" + android:layout_height="@dimen/dialpad_info_title_container_size" + android:maxLines="1" android:textAppearance="@style/TextAppearance.DialNumber" android:gravity="center" + android:autoSizeTextType="uniform" + android:autoSizeMinTextSize="@dimen/dialpad_info_title_text_size_min" + android:autoSizeMaxTextSize="@dimen/dialpad_info_title_text_size_max" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="@id/dialpad_info_guideline" app:layout_constraintStart_toStartOf="parent" diff --git a/res/layout/favorite_fragment.xml b/res/layout/favorite_fragment.xml index f21bc489..bc8611f9 100644 --- a/res/layout/favorite_fragment.xml +++ b/res/layout/favorite_fragment.xml @@ -30,7 +30,8 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" - android:orientation="vertical"> + android:orientation="vertical" + android:visibility="gone"> <TextView android:layout_width="wrap_content" @@ -39,7 +40,7 @@ android:text="@string/favorites_empty" android:layout_marginBottom="@dimen/favorite_add_button_and_text_separation"/> - <TextView + <com.android.car.apps.common.UxrButton android:id="@+id/add_favorite_button" android:layout_width="wrap_content" android:layout_height="@dimen/touch_target_size" diff --git a/res/layout/header_item.xml b/res/layout/header_item.xml new file mode 100644 index 00000000..5a7a374c --- /dev/null +++ b/res/layout/header_item.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. +--> + +<TextView + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/title" + android:layout_width="match_parent" + android:layout_height="@dimen/subheader_list_height" + android:singleLine="true" + android:gravity="center_vertical" + style="@style/SubheaderText"/> diff --git a/res/layout/keypad.xml b/res/layout/keypad.xml index 42e004e4..a63b5ae3 100644 --- a/res/layout/keypad.xml +++ b/res/layout/keypad.xml @@ -31,7 +31,7 @@ limitations under the License. app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toTopOf="@+id/four" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintEnd_toStartOf="@+id/two" /> + app:layout_constraintEnd_toStartOf="@+id/two"/> <com.android.car.dialer.ui.dialpad.KeypadButton android:id="@+id/two" @@ -75,7 +75,7 @@ limitations under the License. app:layout_constraintTop_toBottomOf="@id/two" app:layout_constraintBottom_toTopOf="@+id/eight" app:layout_constraintStart_toEndOf="@id/four" - app:layout_constraintEnd_toStartOf="@+id/six" /> + app:layout_constraintEnd_toStartOf="@+id/six"/> <com.android.car.dialer.ui.dialpad.KeypadButton android:id="@+id/six" @@ -152,50 +152,6 @@ limitations under the License. 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"/> + <include layout="@layout/keypad_dividers"/> </androidx.constraintlayout.widget.ConstraintLayout> diff --git a/res/layout/keypad_dividers.xml b/res/layout/keypad_dividers.xml new file mode 100644 index 00000000..27d87366 --- /dev/null +++ b/res/layout/keypad_dividers.xml @@ -0,0 +1,19 @@ +<?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. + --> + +<!-- For very short screen whose height is less than 456dp, there is no dividers.--> +<merge/> diff --git a/res/layout/no_hfp.xml b/res/layout/no_hfp.xml index 115c9bba..ae24f353 100644 --- a/res/layout/no_hfp.xml +++ b/res/layout/no_hfp.xml @@ -39,7 +39,8 @@ 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" + android:src="@drawable/ic_bluetooth" + android:layout_marginBottom="@dimen/no_hfp_icon_margin_bottom" app:layout_constraintVertical_chainStyle="packed" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toTopOf="@id/error_string" @@ -48,7 +49,7 @@ <TextView android:id="@id/error_string" - android:layout_width="match_parent" + android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/no_hfp" style="@style/NoHfpText" @@ -64,9 +65,10 @@ 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" + android:textAppearance="?android:attr/textAppearanceMedium" + android:background="@drawable/hero_button_background" app:carUxRestrictions="UX_RESTRICTIONS_FULLY_RESTRICTED" + android:layout_marginTop="@dimen/connect_bluetooth_button_margin_top" app:layout_constraintTop_toBottomOf="@+id/error_string" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/res/layout/user_profile_large.xml b/res/layout/user_profile_large.xml index 84e8e392..6b74b076 100644 --- a/res/layout/user_profile_large.xml +++ b/res/layout/user_profile_large.xml @@ -26,7 +26,7 @@ limitations under the License. <LinearLayout android:layout_height="wrap_content" - android:layout_width="wrap_content" + android:layout_width="fill_parent" android:orientation="vertical" android:paddingStart="@dimen/in_call_margin_between_avatar_and_text"> <TextView @@ -42,9 +42,12 @@ limitations under the License. android:textAppearance="@style/TextAppearance.InCallUserPhoneNumber" android:singleLine="true" android:layout_marginTop="@dimen/in_call_phone_number_margin_top"/> + + <!-- A textView without a fixed width will call requestLayout() every time its text is changed --> + <!-- So we have to make this Chronometer match_parent to avoid redrawing the whole screen --> <Chronometer android:id="@+id/user_profile_call_state" - android:layout_width="wrap_content" + android:layout_width="match_parent" android:layout_height="wrap_content" android:textAppearance="@style/TextAppearance.InCallState" android:singleLine="true" diff --git a/res/menu/contact_edit.xml b/res/menu/contact_edit.xml deleted file mode 100644 index abbdabcd..00000000 --- a/res/menu/contact_edit.xml +++ /dev/null @@ -1,24 +0,0 @@ -<?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/values-bg/strings.xml b/res/values-bg/strings.xml index bdb21e0c..b5c5660c 100644 --- a/res/values-bg/strings.xml +++ b/res/values-bg/strings.xml @@ -39,6 +39,8 @@ <string name="dialpad_title" msgid="3746259645005208554">"Клавиатура за набиране"</string> <string name="add_favorite_button" msgid="7914955940249767808">"Добавяне на любим контакт"</string> <string name="favorites_empty" msgid="6712416359691979975">"Още не сте добавили любими"</string> + <string name="cancel_add_favorites_dialog" msgid="3587346864930207137">"@android:string/cancel"</string> + <string name="confirm_add_favorites_dialog" msgid="2709196337767380552">"@android:string/ok"</string> <string name="search_title" msgid="5412680850141871664">"Търсене в контактите"</string> <string name="search_hint" msgid="3099066132607042439">"Търсене в контактите"</string> <string name="type_multiple" msgid="3402949522797441236">"Няколко"</string> diff --git a/res/values-en-rAU/strings.xml b/res/values-en-rAU/strings.xml index be3aeabd..29d0c190 100644 --- a/res/values-en-rAU/strings.xml +++ b/res/values-en-rAU/strings.xml @@ -39,6 +39,8 @@ <string name="dialpad_title" msgid="3746259645005208554">"Dial Pad"</string> <string name="add_favorite_button" msgid="7914955940249767808">"Add a favourite"</string> <string name="favorites_empty" msgid="6712416359691979975">"You haven\'t added any favourites yet"</string> + <string name="cancel_add_favorites_dialog" msgid="3587346864930207137">"@android:string/cancel"</string> + <string name="confirm_add_favorites_dialog" msgid="2709196337767380552">"@android:string/ok"</string> <string name="search_title" msgid="5412680850141871664">"Search contacts"</string> <string name="search_hint" msgid="3099066132607042439">"Search contacts"</string> <string name="type_multiple" msgid="3402949522797441236">"Multiple"</string> diff --git a/res/values-en-rCA/strings.xml b/res/values-en-rCA/strings.xml index be3aeabd..29d0c190 100644 --- a/res/values-en-rCA/strings.xml +++ b/res/values-en-rCA/strings.xml @@ -39,6 +39,8 @@ <string name="dialpad_title" msgid="3746259645005208554">"Dial Pad"</string> <string name="add_favorite_button" msgid="7914955940249767808">"Add a favourite"</string> <string name="favorites_empty" msgid="6712416359691979975">"You haven\'t added any favourites yet"</string> + <string name="cancel_add_favorites_dialog" msgid="3587346864930207137">"@android:string/cancel"</string> + <string name="confirm_add_favorites_dialog" msgid="2709196337767380552">"@android:string/ok"</string> <string name="search_title" msgid="5412680850141871664">"Search contacts"</string> <string name="search_hint" msgid="3099066132607042439">"Search contacts"</string> <string name="type_multiple" msgid="3402949522797441236">"Multiple"</string> diff --git a/res/values-en-rGB/strings.xml b/res/values-en-rGB/strings.xml index be3aeabd..29d0c190 100644 --- a/res/values-en-rGB/strings.xml +++ b/res/values-en-rGB/strings.xml @@ -39,6 +39,8 @@ <string name="dialpad_title" msgid="3746259645005208554">"Dial Pad"</string> <string name="add_favorite_button" msgid="7914955940249767808">"Add a favourite"</string> <string name="favorites_empty" msgid="6712416359691979975">"You haven\'t added any favourites yet"</string> + <string name="cancel_add_favorites_dialog" msgid="3587346864930207137">"@android:string/cancel"</string> + <string name="confirm_add_favorites_dialog" msgid="2709196337767380552">"@android:string/ok"</string> <string name="search_title" msgid="5412680850141871664">"Search contacts"</string> <string name="search_hint" msgid="3099066132607042439">"Search contacts"</string> <string name="type_multiple" msgid="3402949522797441236">"Multiple"</string> diff --git a/res/values-en-rIN/strings.xml b/res/values-en-rIN/strings.xml index be3aeabd..29d0c190 100644 --- a/res/values-en-rIN/strings.xml +++ b/res/values-en-rIN/strings.xml @@ -39,6 +39,8 @@ <string name="dialpad_title" msgid="3746259645005208554">"Dial Pad"</string> <string name="add_favorite_button" msgid="7914955940249767808">"Add a favourite"</string> <string name="favorites_empty" msgid="6712416359691979975">"You haven\'t added any favourites yet"</string> + <string name="cancel_add_favorites_dialog" msgid="3587346864930207137">"@android:string/cancel"</string> + <string name="confirm_add_favorites_dialog" msgid="2709196337767380552">"@android:string/ok"</string> <string name="search_title" msgid="5412680850141871664">"Search contacts"</string> <string name="search_hint" msgid="3099066132607042439">"Search contacts"</string> <string name="type_multiple" msgid="3402949522797441236">"Multiple"</string> diff --git a/res/values-en-rXC/strings.xml b/res/values-en-rXC/strings.xml index c69550d9..00acff0f 100644 --- a/res/values-en-rXC/strings.xml +++ b/res/values-en-rXC/strings.xml @@ -39,6 +39,8 @@ <string name="dialpad_title" msgid="3746259645005208554">"Dialpad"</string> <string name="add_favorite_button" msgid="7914955940249767808">"Add a favorite"</string> <string name="favorites_empty" msgid="6712416359691979975">"You haven\'t added any favorites yet"</string> + <string name="cancel_add_favorites_dialog" msgid="3587346864930207137">"@android:string/cancel"</string> + <string name="confirm_add_favorites_dialog" msgid="2709196337767380552">"@android:string/ok"</string> <string name="search_title" msgid="5412680850141871664">"Search contacts"</string> <string name="search_hint" msgid="3099066132607042439">"Search contacts"</string> <string name="type_multiple" msgid="3402949522797441236">"Multiple"</string> diff --git a/res/values-h456dp/dimens.xml b/res/values-h456dp/dimens.xml new file mode 100644 index 00000000..4289c83f --- /dev/null +++ b/res/values-h456dp/dimens.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. + --> + +<resources> + <!-- Keypad dimensions --> + <dimen name="keypad_margin_x">@*android:dimen/car_padding_3</dimen> + <dimen name="keypad_margin_y">@*android:dimen/car_padding_1</dimen> + <dimen name="keypad_margin">@*android:dimen/car_padding_4</dimen> +</resources> diff --git a/res/values-h610dp/dimens.xml b/res/values-h610dp/dimens.xml index 1947548e..9dba83d8 100644 --- a/res/values-h610dp/dimens.xml +++ b/res/values-h610dp/dimens.xml @@ -21,8 +21,14 @@ <dimen name="keypad_margin_y">@*android:dimen/car_padding_2</dimen> <dimen name="keypad_margin">@*android:dimen/car_padding_5</dimen> + <!-- Control bar bottom padding --> + <dimen name="in_call_controller_bar_margin_bottom">@*android:dimen/car_padding_2</dimen> + <dimen name="fab_outline_size">104dp</dimen> <dimen name="fab_ripple_radius">52dp</dimen> <dimen name="contact_details_avatar_size">126dp</dimen> + + <!-- Components --> + <dimen name="control_bar_height">128dp</dimen> </resources> diff --git a/res/values-in/strings.xml b/res/values-in/strings.xml index bc6727a8..3aa9488b 100644 --- a/res/values-in/strings.xml +++ b/res/values-in/strings.xml @@ -39,6 +39,8 @@ <string name="dialpad_title" msgid="3746259645005208554">"Tombol nomor"</string> <string name="add_favorite_button" msgid="7914955940249767808">"Tambahkan favorit"</string> <string name="favorites_empty" msgid="6712416359691979975">"Anda belum menambahkan satu pun favorit"</string> + <string name="cancel_add_favorites_dialog" msgid="3587346864930207137">"@android:string/batal"</string> + <string name="confirm_add_favorites_dialog" msgid="2709196337767380552">"@android:string/oke"</string> <string name="search_title" msgid="5412680850141871664">"Telusuri kontak"</string> <string name="search_hint" msgid="3099066132607042439">"Telusuri kontak"</string> <string name="type_multiple" msgid="3402949522797441236">"Beberapa"</string> diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml index 33cf3bec..b23adf4c 100644 --- a/res/values-it/strings.xml +++ b/res/values-it/strings.xml @@ -39,6 +39,8 @@ <string name="dialpad_title" msgid="3746259645005208554">"Tastierino"</string> <string name="add_favorite_button" msgid="7914955940249767808">"Aggiungi un preferito"</string> <string name="favorites_empty" msgid="6712416359691979975">"Non hai ancora aggiunto preferiti"</string> + <string name="cancel_add_favorites_dialog" msgid="3587346864930207137">"@android:string/cancel"</string> + <string name="confirm_add_favorites_dialog" msgid="2709196337767380552">"@android:string/ok"</string> <string name="search_title" msgid="5412680850141871664">"Cerca nei contatti"</string> <string name="search_hint" msgid="3099066132607042439">"Cerca nei contatti"</string> <string name="type_multiple" msgid="3402949522797441236">"Multipli"</string> diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml index 57af8198..dc120d63 100644 --- a/res/values-ja/strings.xml +++ b/res/values-ja/strings.xml @@ -39,6 +39,8 @@ <string name="dialpad_title" msgid="3746259645005208554">"ダイヤルパッド"</string> <string name="add_favorite_button" msgid="7914955940249767808">"お気に入りとして追加"</string> <string name="favorites_empty" msgid="6712416359691979975">"お気に入りはまだ追加されていません"</string> + <string name="cancel_add_favorites_dialog" msgid="3587346864930207137">"@android:string/cancel"</string> + <string name="confirm_add_favorites_dialog" msgid="2709196337767380552">"@android:string/ok"</string> <string name="search_title" msgid="5412680850141871664">"連絡先を検索"</string> <string name="search_hint" msgid="3099066132607042439">"連絡先を検索"</string> <string name="type_multiple" msgid="3402949522797441236">"複数"</string> diff --git a/res/values-mn/strings.xml b/res/values-mn/strings.xml index a21a3bee..24ee3176 100644 --- a/res/values-mn/strings.xml +++ b/res/values-mn/strings.xml @@ -39,6 +39,8 @@ <string name="dialpad_title" msgid="3746259645005208554">"Дугаар цуглуулах самбар"</string> <string name="add_favorite_button" msgid="7914955940249767808">"Дуртай харилцагч нэмэх"</string> <string name="favorites_empty" msgid="6712416359691979975">"Та ямар нэгэн дуртай зүйл хараахан нэмээгүй байна"</string> + <string name="cancel_add_favorites_dialog" msgid="3587346864930207137">"@android:string/цуцлах"</string> + <string name="confirm_add_favorites_dialog" msgid="2709196337767380552">"@android:string/ок"</string> <string name="search_title" msgid="5412680850141871664">"Харилцагч хайх"</string> <string name="search_hint" msgid="3099066132607042439">"Харилцагч хайх"</string> <string name="type_multiple" msgid="3402949522797441236">"Олон"</string> diff --git a/res/values-ms/strings.xml b/res/values-ms/strings.xml index 9ed64b8e..d096a19d 100644 --- a/res/values-ms/strings.xml +++ b/res/values-ms/strings.xml @@ -39,6 +39,8 @@ <string name="dialpad_title" msgid="3746259645005208554">"Pad pendail"</string> <string name="add_favorite_button" msgid="7914955940249767808">"Tambah kegemaran"</string> <string name="favorites_empty" msgid="6712416359691979975">"Anda belum menambah sebarang kegemaran"</string> + <string name="cancel_add_favorites_dialog" msgid="3587346864930207137">"@android:string/cancel"</string> + <string name="confirm_add_favorites_dialog" msgid="2709196337767380552">"@android:string/ok"</string> <string name="search_title" msgid="5412680850141871664">"Cari kenalan"</string> <string name="search_hint" msgid="3099066132607042439">"Cari kenalan"</string> <string name="type_multiple" msgid="3402949522797441236">"Berbilang"</string> diff --git a/res/values-my/strings.xml b/res/values-my/strings.xml index c7002883..3e14576b 100644 --- a/res/values-my/strings.xml +++ b/res/values-my/strings.xml @@ -39,6 +39,8 @@ <string name="dialpad_title" msgid="3746259645005208554">"နံပါတ်ကွက်"</string> <string name="add_favorite_button" msgid="7914955940249767808">"အကြိုက်ဆုံး ထည့်ရန်"</string> <string name="favorites_empty" msgid="6712416359691979975">"သင့်အကြိုက်ဆုံးတစ်ခုမျှ မထည့်ရသေးပါ"</string> + <string name="cancel_add_favorites_dialog" msgid="3587346864930207137">"@android:string/ပယ်ဖျက်ရန်"</string> + <string name="confirm_add_favorites_dialog" msgid="2709196337767380552">"@android:string/ok"</string> <string name="search_title" msgid="5412680850141871664">"အဆက်အသွယ်များ ရှာရန်"</string> <string name="search_hint" msgid="3099066132607042439">"အဆက်အသွယ်များ ရှာရန်"</string> <string name="type_multiple" msgid="3402949522797441236">"အများအပြား"</string> diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml index 9d3d4da2..e45f01cf 100644 --- a/res/values-nb/strings.xml +++ b/res/values-nb/strings.xml @@ -39,6 +39,8 @@ <string name="dialpad_title" msgid="3746259645005208554">"Tastatur"</string> <string name="add_favorite_button" msgid="7914955940249767808">"Legg til en favoritt"</string> <string name="favorites_empty" msgid="6712416359691979975">"Du har ikke lagt til noen favoritter ennå"</string> + <string name="cancel_add_favorites_dialog" msgid="3587346864930207137">"@android:string/cancel"</string> + <string name="confirm_add_favorites_dialog" msgid="2709196337767380552">"@android:string/ok"</string> <string name="search_title" msgid="5412680850141871664">"Søk i kontakter"</string> <string name="search_hint" msgid="3099066132607042439">"Søk i kontakter"</string> <string name="type_multiple" msgid="3402949522797441236">"Flere"</string> diff --git a/res/values-port/dimens.xml b/res/values-port/dimens.xml index d210834f..cdb70cfc 100644 --- a/res/values-port/dimens.xml +++ b/res/values-port/dimens.xml @@ -20,4 +20,6 @@ <dimen name="fab_outline_size">84dp</dimen> <dimen name="keypad_margin">@*android:dimen/car_padding_4</dimen> + + <dimen name="dialpad_info_title_text_size_max">36sp</dimen> </resources> diff --git a/res/values-si/strings.xml b/res/values-si/strings.xml index 7e2960f7..6ed4b0b6 100644 --- a/res/values-si/strings.xml +++ b/res/values-si/strings.xml @@ -39,6 +39,8 @@ <string name="dialpad_title" msgid="3746259645005208554">"ඇමතුම් පෑඩය"</string> <string name="add_favorite_button" msgid="7914955940249767808">"ප්රියතමයක් එක් කරන්න"</string> <string name="favorites_empty" msgid="6712416359691979975">"ඔබ තවම ප්රියතමයන් එක් කර නැත"</string> + <string name="cancel_add_favorites_dialog" msgid="3587346864930207137">"@android:string/අවලංගු කරන්න"</string> + <string name="confirm_add_favorites_dialog" msgid="2709196337767380552">"@android:string/හරි"</string> <string name="search_title" msgid="5412680850141871664">"සම්බන්ධතා සොයන්න"</string> <string name="search_hint" msgid="3099066132607042439">"සම්බන්ධතා සොයන්න"</string> <string name="type_multiple" msgid="3402949522797441236">"බහුවිධ"</string> diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml index ecff10ce..678da8c6 100644 --- a/res/values-sl/strings.xml +++ b/res/values-sl/strings.xml @@ -39,6 +39,8 @@ <string name="dialpad_title" msgid="3746259645005208554">"Številčnica"</string> <string name="add_favorite_button" msgid="7914955940249767808">"Dodaj priljubljenega"</string> <string name="favorites_empty" msgid="6712416359691979975">"Dodali niste še nobenega priljubljenega"</string> + <string name="cancel_add_favorites_dialog" msgid="3587346864930207137">"@android:string/cancel"</string> + <string name="confirm_add_favorites_dialog" msgid="2709196337767380552">"@android:string/ok"</string> <string name="search_title" msgid="5412680850141871664">"Iskanje stikov"</string> <string name="search_hint" msgid="3099066132607042439">"Iskanje stikov"</string> <string name="type_multiple" msgid="3402949522797441236">"Več"</string> diff --git a/res/values-sw/strings.xml b/res/values-sw/strings.xml index dd978cd5..44cf3ff2 100644 --- a/res/values-sw/strings.xml +++ b/res/values-sw/strings.xml @@ -39,6 +39,8 @@ <string name="dialpad_title" msgid="3746259645005208554">"Vitufe vya kupiga simu"</string> <string name="add_favorite_button" msgid="7914955940249767808">"Ongeza anwani unazowasiliana nazo sana"</string> <string name="favorites_empty" msgid="6712416359691979975">"Bado hujaongeza anwani zozote unazowasiliana nazo sana"</string> + <string name="cancel_add_favorites_dialog" msgid="3587346864930207137">"@android:string/cancel"</string> + <string name="confirm_add_favorites_dialog" msgid="2709196337767380552">"@android:string/ok"</string> <string name="search_title" msgid="5412680850141871664">"Tafuta anwani"</string> <string name="search_hint" msgid="3099066132607042439">"Tafuta anwani"</string> <string name="type_multiple" msgid="3402949522797441236">"Nyingi"</string> diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml index f9b8b216..bb47580e 100644 --- a/res/values-tr/strings.xml +++ b/res/values-tr/strings.xml @@ -39,6 +39,8 @@ <string name="dialpad_title" msgid="3746259645005208554">"Tuş takımı"</string> <string name="add_favorite_button" msgid="7914955940249767808">"Favori ekle"</string> <string name="favorites_empty" msgid="6712416359691979975">"Henüz herhangi bir favori eklemediniz"</string> + <string name="cancel_add_favorites_dialog" msgid="3587346864930207137">"@android:string/iptal et"</string> + <string name="confirm_add_favorites_dialog" msgid="2709196337767380552">"@android:string/tamam"</string> <string name="search_title" msgid="5412680850141871664">"Kişilerde arayın"</string> <string name="search_hint" msgid="3099066132607042439">"Kişilerde arayın"</string> <string name="type_multiple" msgid="3402949522797441236">"Birden fazla"</string> diff --git a/res/values-w1280dp-land/dimens.xml b/res/values-w1280dp-land/dimens.xml new file mode 100644 index 00000000..2a8bf8f2 --- /dev/null +++ b/res/values-w1280dp-land/dimens.xml @@ -0,0 +1,18 @@ +<?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> + <dimen name="dialpad_info_title_text_size_max">36sp</dimen> +</resources> diff --git a/res/values/arrays.xml b/res/values/arrays.xml index 9526d33d..eadeb53e 100644 --- a/res/values/arrays.xml +++ b/res/values/arrays.xml @@ -20,20 +20,18 @@ 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>FAVORITE</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/favorites_title</item> <item>@string/dialpad_title</item> </string-array> diff --git a/res/values/colors.xml b/res/values/colors.xml index 2b99d101..7286bf54 100644 --- a/res/values/colors.xml +++ b/res/values/colors.xml @@ -30,7 +30,6 @@ <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> diff --git a/res/values/dimens.xml b/res/values/dimens.xml index 2a9f5c42..79f9755e 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -58,7 +58,7 @@ <!-- 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_controller_bar_margin_bottom">0dp</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> @@ -83,6 +83,9 @@ <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> + <dimen name="dialpad_info_title_container_size">@dimen/touch_target_size</dimen> + <dimen name="dialpad_info_title_text_size_max">28sp</dimen> + <dimen name="dialpad_info_title_text_size_min">24sp</dimen> <!-- Keypad dimensions --> <dimen name="keypad_minimum_size">@dimen/touch_target_size</dimen> @@ -91,11 +94,15 @@ <!-- 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="favorite_card_space_vertical">@*android:dimen/car_padding_2</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> + <!-- Add faovirte flow dimensions --> + <dimen name="add_favorite_number_list_height">@dimen/list_item_height</dimen> + <dimen name="add_favorite_number_list_padding">@dimen/list_item_padding</dimen> + <dimen name="call_fab_elevation">8dp</dimen> <dimen name="bksp_button_width">@dimen/touch_target_size</dimen> @@ -110,16 +117,18 @@ <dimen name="car_key2_size">18sp</dimen> <!-- NoHfp error message page --> - <dimen name="no_hfp_icon_size">@dimen/primary_icon_size</dimen> + <dimen name="no_hfp_icon_size">56dp</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="no_hfp_icon_margin_bottom">@*android:dimen/car_padding_3</dimen> + <dimen name="connect_bluetooth_button_margin_top">@*android:dimen/car_padding_5</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_height">116dp</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> @@ -138,6 +147,8 @@ <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="subheader_list_height">76dp</dimen> + <dimen name="control_bar_height">96dp</dimen> <dimen name="phone_number_radio_list_padding">@*android:dimen/car_padding_2</dimen> <!-- Toolbar --> diff --git a/res/values/strings.xml b/res/values/strings.xml index bc7996ab..3b633336 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -65,10 +65,24 @@ <!-- Toolbar title for tabbed pages --> <string name="default_toolbar_title" translatable="false"></string> + <!-- Headers for call history --> + <!-- Header in call log to group calls from the current day. [CHAR LIMIT=30] --> + <string name="call_log_header_today">Today</string> + + <!-- Header in call log to group calls from the previous day. [CHAR LIMIT=30] --> + <string name="call_log_header_yesterday">Yesterday</string> + + <!-- Header in call log to group calls from before yesterday. [CHAR LIMIT=30] --> + <string name="call_log_header_older">Older</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> + <!-- Button to cancel the dialog where you pick a phone number to add as a favorite --> + <string name="cancel_add_favorites_dialog" translatable="false">@android:string/cancel</string> + <!-- Button to confirm the dialog where you pick a phone number to add as a favorite --> + <string name="confirm_add_favorites_dialog" translatable="false">@android:string/ok</string> <!-- Keypad strings--> <string name="one" translatable="false">1</string> @@ -109,8 +123,6 @@ <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> @@ -131,15 +143,19 @@ <!-- Text to show the call is onhold [CHAR LIMIT=40]--> <string name="onhold_call_label">On Hold</string> + <!-- Contact list headers --> + <!-- Contact list label for contact names starting with special characters --> + <string name="header_for_type_other" translatable="false">…</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_title">Start screen</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_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]--> diff --git a/res/values/styles.xml b/res/values/styles.xml index 7d4fff98..5b2ecde6 100644 --- a/res/values/styles.xml +++ b/res/values/styles.xml @@ -15,6 +15,7 @@ --> <resources xmlns:android="http://schemas.android.com/apk/res/android"> <!-- Dialpad --> + <!-- The size won't matter here, as the autosizing will override it --> <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> @@ -62,7 +63,7 @@ </style> <style name="NoHfpText"> - <item name="android:textAppearance">?android:attr/textAppearanceMedium</item> + <item name="android:textAppearance">?android:attr/textAppearanceLarge</item> <item name="android:gravity">center</item> <item name="android:maxLines">3</item> </style> @@ -100,7 +101,8 @@ <item name="android:minHeight">@dimen/touch_target_size</item> </style> - <style name="Widget.Dialer.ActionButton.Overflow" parent="android:Widget.DeviceDefault.ActionButton.Overflow"> + <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> @@ -120,15 +122,27 @@ <!-- 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.ContactDetailsListTitle" parent="@style/TextAppearance.Body1"/> + <style name="TextAppearance.ContactDetailsListSubtitle" parent="@style/TextAppearance.Body3"/> + <style name="TextAppearance.DefaultNumberLabel" parent="@style/TextAppearance.Body3"> + <item name="android:textColor">@*android:color/accent_device_default_light</item> + </style> + <!-- Contact results --> <style name="TextAppearance.ContactResultTitle" parent="@style/TextAppearance.Body1"/> + <!-- Subheader --> + <style name="SubheaderText"> + <item name="android:textAppearance">@style/TextAppearance.Body3</item> + <item name="android:textFontWeight">500</item> + <item name="android:textStyle">normal</item> + </style> + <!-- Display options defined for ActionBar--> <style name="RootToolbarDisplayOptions"> <item name="android:displayOptions">useLogo|showHome|showTitle|showCustom</item> @@ -138,4 +152,10 @@ <item name="android:displayOptions">showTitle|homeAsUp|showCustom</item> </style> + <style name="Widget.Dialer.Button" parent="android:Widget.DeviceDefault.Button"> + <item name="android:ellipsize">none</item> + <item name="android:requiresFadingEdge">horizontal</item> + <item name="android:fadingEdgeLength">@*android:dimen/car_textview_fading_edge_length</item> + </style> + </resources> diff --git a/res/values/themes.xml b/res/values/themes.xml index 9317fb88..bb1f1053 100644 --- a/res/values/themes.xml +++ b/res/values/themes.xml @@ -16,6 +16,7 @@ limitations under the License. <resources> <!-- The theme for the Dialer app. --> <style name="Theme.Dialer" parent="android:Theme.DeviceDefault.NoActionBar"> + <item name="android:actionBarSize">96dp</item> <item name="android:actionBarItemBackground">@drawable/action_button_background</item> <!-- Menu button style --> <item name="android:actionButtonStyle">@style/Widget.Dialer.ActionButton</item> @@ -23,6 +24,7 @@ limitations under the License. </item> <item name="android:listDivider">@drawable/list_divider</item> <item name="android:toolbarStyle">@style/Widget.Dialer.Toolbar</item> + <item name="android:buttonStyle">@style/Widget.Dialer.Button</item> </style> <style name="Theme.Dialer.Setting" parent="Theme.Dialer"> diff --git a/src/com/android/car/dialer/livedata/BluetoothPairListLiveData.java b/src/com/android/car/dialer/livedata/BluetoothPairListLiveData.java index bc6d3a28..94b7c3ab 100644 --- a/src/com/android/car/dialer/livedata/BluetoothPairListLiveData.java +++ b/src/com/android/car/dialer/livedata/BluetoothPairListLiveData.java @@ -28,7 +28,6 @@ import androidx.lifecycle.LiveData; import com.android.car.dialer.log.L; -import java.util.Collections; import java.util.Set; /** diff --git a/src/com/android/car/dialer/livedata/ContactDetailsLiveData.java b/src/com/android/car/dialer/livedata/ContactDetailsLiveData.java deleted file mode 100644 index d708025e..00000000 --- a/src/com/android/car/dialer/livedata/ContactDetailsLiveData.java +++ /dev/null @@ -1,98 +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.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 deleted file mode 100644 index 800ee81b..00000000 --- a/src/com/android/car/dialer/livedata/FavoriteContactLiveData.java +++ /dev/null @@ -1,80 +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.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/SharedPreferencesLiveData.java b/src/com/android/car/dialer/livedata/SharedPreferencesLiveData.java index 13ae0493..c266345e 100644 --- a/src/com/android/car/dialer/livedata/SharedPreferencesLiveData.java +++ b/src/com/android/car/dialer/livedata/SharedPreferencesLiveData.java @@ -20,6 +20,7 @@ import android.content.Context; import android.content.SharedPreferences; import android.text.TextUtils; +import androidx.annotation.StringRes; import androidx.lifecycle.LiveData; import androidx.preference.PreferenceManager; @@ -48,6 +49,10 @@ public class SharedPreferencesLiveData extends LiveData<SharedPreferences> { }; } + public SharedPreferencesLiveData(Context context, @StringRes int key) { + this(context, context.getString(key)); + } + @Override protected void onActive() { updateSharedPreferences(); diff --git a/src/com/android/car/dialer/notification/InCallNotificationController.java b/src/com/android/car/dialer/notification/InCallNotificationController.java index 2d778c76..e8a0cf16 100644 --- a/src/com/android/car/dialer/notification/InCallNotificationController.java +++ b/src/com/android/car/dialer/notification/InCallNotificationController.java @@ -25,9 +25,9 @@ import android.content.Context; import android.content.Intent; import android.graphics.drawable.Icon; import android.telecom.Call; +import android.text.TextUtils; import androidx.annotation.StringRes; -import androidx.core.util.Pair; import com.android.car.dialer.Constants; import com.android.car.dialer.R; @@ -35,6 +35,9 @@ import com.android.car.dialer.log.L; import com.android.car.dialer.ui.activecall.InCallActivity; import com.android.car.telephony.common.CallDetail; +import java.util.Arrays; +import java.util.concurrent.CompletableFuture; + /** Controller that manages the heads up notification for incoming calls. */ public final class InCallNotificationController { private static final String TAG = "CD.InCallNotificationController"; @@ -77,6 +80,8 @@ public final class InCallNotificationController { private final Context mContext; private final NotificationManager mNotificationManager; + private final Notification.Builder mNotificationBuilder; + private CompletableFuture<Void> mNotificationFuture; @TargetApi(26) private InCallNotificationController(Context context) { @@ -88,17 +93,6 @@ public final class InCallNotificationController { 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); @@ -106,24 +100,56 @@ public final class InCallNotificationController { PendingIntent fullscreenIntent = PendingIntent.getActivity(mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); - Notification.Builder builder = new Notification.Builder(mContext, CHANNEL_ID) + mNotificationBuilder = 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); + } + + /** Show a new incoming call notification or update the existing incoming call notification. */ + @TargetApi(26) + public void showInCallNotification(Call call) { + L.d(TAG, "showInCallNotification"); + + if (mNotificationFuture != null) { + mNotificationFuture.cancel(true); + } + + CallDetail callDetail = CallDetail.fromTelecomCallDetail(call.getDetails()); + String number = callDetail.getNumber(); + String tag = call.getDetails().getTelecomCallId(); + mNotificationBuilder + .setLargeIcon((Icon) null) + .setContentTitle(number) + .setActions( + getAction(call, R.string.answer_call, + NotificationService.ACTION_ANSWER_CALL), + getAction(call, R.string.decline_call, + NotificationService.ACTION_DECLINE_CALL)); mNotificationManager.notify( - call.getDetails().getTelecomCallId(), + tag, NOTIFICATION_ID, - builder.build()); + mNotificationBuilder.build()); + + mNotificationFuture = NotificationUtils.getDisplayNameAndRoundedAvatar(mContext, number) + .thenAcceptAsync((pair) -> { + // Check that the notification hasn't already been dismissed + if (Arrays.stream(mNotificationManager.getActiveNotifications()).anyMatch((n) -> + n.getId() == NOTIFICATION_ID && TextUtils.equals(n.getTag(), tag))) { + mNotificationBuilder + .setLargeIcon(pair.second) + .setContentTitle(pair.first); + + mNotificationManager.notify( + tag, + NOTIFICATION_ID, + mNotificationBuilder.build()); + } + }, mContext.getMainExecutor()); } /** Cancel the incoming call notification for the given call. */ diff --git a/src/com/android/car/dialer/notification/MissedCallNotificationController.java b/src/com/android/car/dialer/notification/MissedCallNotificationController.java index a1d47f76..bcd577d2 100644 --- a/src/com/android/car/dialer/notification/MissedCallNotificationController.java +++ b/src/com/android/car/dialer/notification/MissedCallNotificationController.java @@ -23,13 +23,11 @@ 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; @@ -43,6 +41,7 @@ import com.android.car.telephony.common.PhoneCallLog; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.concurrent.CompletableFuture; /** Controller that manages the missed call notifications. */ public final class MissedCallNotificationController { @@ -94,6 +93,7 @@ public final class MissedCallNotificationController { private final UnreadMissedCallLiveData mUnreadMissedCallLiveData; private final Observer<List<PhoneCallLog>> mUnreadMissedCallObserver; private final List<PhoneCallLog> mCurrentPhoneCallLogList; + private CompletableFuture<Void> mUpdateNotificationFuture; @TargetApi(26) private MissedCallNotificationController(Context context) { @@ -132,35 +132,39 @@ public final class MissedCallNotificationController { 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 + private void showMissedCallNotification(PhoneCallLog callLog) { + L.d(TAG, "show missed call notification %s", callLog); + if (mUpdateNotificationFuture != null) { + mUpdateNotificationFuture.cancel(true); } - - mNotificationManager.notify( - getTag(phoneCallLog), - NOTIFICATION_ID, - builder.build()); + String phoneNumber = callLog.getPhoneNumberString(); + mUpdateNotificationFuture = NotificationUtils.getDisplayNameAndRoundedAvatar( + mContext, phoneNumber) + .thenAcceptAsync((pair) -> { + Notification.Builder builder = new Notification.Builder(mContext, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_phone) + .setLargeIcon(pair.second) + .setContentTitle(mContext.getString(R.string.notification_missed_call) + + String.format(" (%d)", callLog.getAllCallRecords().size())) + .setContentText(pair.first) + .setContentIntent(getContentPendingIntent()) + .setDeleteIntent(getDeleteIntent()) + .setOnlyAlertOnce(true) + .setShowWhen(true) + .setWhen(callLog.getLastCallEndTimestamp()) + .setAutoCancel(false); + + if (!TextUtils.isEmpty(phoneNumber)) { + builder.addAction(getAction(phoneNumber, R.string.call_back, + NotificationService.ACTION_CALL_BACK_MISSED)); + // TODO: add action button to send message + } + + mNotificationManager.notify( + getTag(callLog), + NOTIFICATION_ID, + builder.build()); + }, mContext.getMainExecutor()); } private void cancelMissedCallNotification(PhoneCallLog phoneCallLog) { diff --git a/src/com/android/car/dialer/notification/NotificationUtils.java b/src/com/android/car/dialer/notification/NotificationUtils.java index 9b6f9322..1812c3fc 100644 --- a/src/com/android/car/dialer/notification/NotificationUtils.java +++ b/src/com/android/car/dialer/notification/NotificationUtils.java @@ -33,27 +33,29 @@ import com.android.car.telephony.common.TelecomUtils; import java.io.FileNotFoundException; import java.io.InputStream; +import java.util.concurrent.CompletableFuture; /** 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); + static CompletableFuture<Pair<String, Icon>> getDisplayNameAndRoundedAvatar(Context context, + String number) { + return TelecomUtils.getPhoneNumberInfo(context, number) + .thenApplyAsync((info) -> { + int size = context.getResources() + .getDimensionPixelSize(R.dimen.avatar_icon_size); + Icon largeIcon = loadContactAvatar(context, info.getAvatarUri(), size); + if (largeIcon == null) { + largeIcon = createLetterTile(context, info.getDisplayName(), size); + } - 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); + return new Pair<>(info.getDisplayName(), largeIcon); + }); } - static Icon loadRoundedContactAvatar(Context context, @Nullable Uri avatarUri, int avatarSize) { + static Icon loadContactAvatar(Context context, @Nullable Uri avatarUri, int avatarSize) { if (avatarUri == null) { return null; } @@ -65,24 +67,32 @@ final class NotificationUtils { } 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); + return createFromRoundedBitmapDrawable(context, roundedBitmapDrawable, avatarSize); } catch (FileNotFoundException e) { // No-op } return null; } - static Icon createLetterTile(Context context, String displayName, int avatarSize) { + private static Icon createLetterTile(Context context, String displayName, int avatarSize) { LetterTileDrawable letterTileDrawable = TelecomUtils.createLetterTile(context, displayName); - letterTileDrawable.setIsCircular(true); - return Icon.createWithBitmap(letterTileDrawable.toBitmap(avatarSize)); + RoundedBitmapDrawable roundedBitmapDrawable = RoundedBitmapDrawableFactory.create( + context.getResources(), letterTileDrawable.toBitmap(avatarSize)); + return createFromRoundedBitmapDrawable(context, roundedBitmapDrawable, avatarSize); + } + + private static Icon createFromRoundedBitmapDrawable(Context context, + RoundedBitmapDrawable roundedBitmapDrawable, int avatarSize) { + float radiusPercent = context.getResources() + .getFloat(R.dimen.contact_avatar_corner_radius_percent); + float radius = avatarSize * radiusPercent; + roundedBitmapDrawable.setCornerRadius(radius); + + 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); + return Icon.createWithBitmap(result); } } diff --git a/src/com/android/car/dialer/storage/BluetoothBondedListReceiver.java b/src/com/android/car/dialer/storage/BluetoothBondedListReceiver.java new file mode 100644 index 00000000..b918ee55 --- /dev/null +++ b/src/com/android/car/dialer/storage/BluetoothBondedListReceiver.java @@ -0,0 +1,49 @@ +/* + * 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.storage; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import java.util.Collections; +import java.util.Set; + +/** + * Broadcast receiver that monitors the bluetooth device unpair event and removes entries for + * devices that has been unpaired. + */ +public class BluetoothBondedListReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction() != BluetoothDevice.ACTION_BOND_STATE_CHANGED) { + return; + } + + if (intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE) + == BluetoothDevice.BOND_NONE) { + FavoriteNumberRepository favoriteNumberRepository = + FavoriteNumberRepository.getRepository(context); + BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + Set<BluetoothDevice> pairedDevices = bluetoothAdapter == null ? Collections.emptySet() + : bluetoothAdapter.getBondedDevices(); + favoriteNumberRepository.cleanup(pairedDevices); + } + } +} diff --git a/src/com/android/car/dialer/storage/CipherConverter.java b/src/com/android/car/dialer/storage/CipherConverter.java new file mode 100644 index 00000000..cee0a6fd --- /dev/null +++ b/src/com/android/car/dialer/storage/CipherConverter.java @@ -0,0 +1,162 @@ +/* + * 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.storage; + +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyProperties; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; +import androidx.room.TypeConverter; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.KeyGenerator; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; + +/** + * A converter that does the encryption and decryption using android KeyStore system. See + * https://developer.android.com/training/articles/keystore + */ +public class CipherConverter { + private static final String TAG = "CD.CipherConverter"; + private static final String KEY_STORE_ALIAS = "cd-cipher-converter"; + private static final String ANDROID_KEY_STORE = "AndroidKeyStore"; + + /** + * Decryption. + * + * @param encryptedData the encrypted byte array. First byte is the initialization vector + * length, followed by the + * initialization vector and then the encrypted string. + * @return the decrypted string wrapper. It might be null if the encrypted array is not valid or + * exception happens during decryption. + */ + @WorkerThread + @TypeConverter + @Nullable + public CipherWrapper<String> decrypt(@NonNull byte[] encryptedData) { + if (encryptedData.length == 0) { + return null; + } + + try { + KeyStore ks = getKeyStore(); + SecretKey decryptionKey = (SecretKey) ks.getKey(KEY_STORE_ALIAS, null); + + Cipher cipher = getCipherInstance(); + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(encryptedData); + + int ivLength = byteArrayInputStream.read(); + + byte[] iv = new byte[ivLength]; + byteArrayInputStream.read(iv, 0, ivLength); + + byte[] encryptedPhoneNumber = new byte[encryptedData.length - ivLength - 1]; + byteArrayInputStream.read(encryptedPhoneNumber); + + cipher.init(Cipher.DECRYPT_MODE, decryptionKey, new GCMParameterSpec(128, iv)); + byte[] decryptionResult = cipher.doFinal(encryptedPhoneNumber); + String decryptString = new String(decryptionResult, "UTF-8"); + return new CipherWrapper(decryptString); + } catch (KeyStoreException | IOException | CertificateException | NoSuchAlgorithmException + | UnrecoverableKeyException | NoSuchPaddingException | BadPaddingException + | IllegalBlockSizeException | InvalidKeyException + | InvalidAlgorithmParameterException e) { + Log.e(TAG, e.toString()); + } + return null; + } + + /** + * Encryption. + * + * @param stringCipherWrapper The wrapper of string to be encrypted. + * @return byte array that includes the iv length, iv and encrypted string. + */ + @WorkerThread + @NonNull + @TypeConverter + public byte[] encrypt(CipherWrapper<String> stringCipherWrapper) { + try { + KeyStore ks = getKeyStore(); + SecretKey secretKey; + if (ks.containsAlias(KEY_STORE_ALIAS)) { + secretKey = (SecretKey) ks.getKey(KEY_STORE_ALIAS, null); + } else { + KeyGenerator kpg = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, + ANDROID_KEY_STORE); + KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder( + KEY_STORE_ALIAS, + KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .build(); + kpg.init(keyGenParameterSpec); + secretKey = kpg.generateKey(); + } + + Cipher cipher = getCipherInstance(); + cipher.init(Cipher.ENCRYPT_MODE, secretKey); + byte[] iv = cipher.getIV(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + outputStream.write(iv.length); + outputStream.write(iv); + byte[] encryptionResult = cipher.doFinal( + stringCipherWrapper.get().getBytes("UTF-8")); + outputStream.write(encryptionResult); + return outputStream.toByteArray(); + } catch (KeyStoreException | IOException | CertificateException | NoSuchAlgorithmException + | UnrecoverableKeyException | NoSuchProviderException | NoSuchPaddingException + | BadPaddingException | IllegalBlockSizeException | InvalidKeyException + | InvalidAlgorithmParameterException e) { + Log.e(TAG, e.toString()); + } + return new byte[0]; + } + + private KeyStore getKeyStore() + throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException { + KeyStore keyStore = KeyStore.getInstance(ANDROID_KEY_STORE); + keyStore.load(null); + return keyStore; + } + + private Cipher getCipherInstance() + throws NoSuchAlgorithmException, NoSuchPaddingException { + return Cipher.getInstance( + KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_GCM + "/" + + KeyProperties.ENCRYPTION_PADDING_NONE); + } +} diff --git a/src/com/android/car/dialer/ui/search/ContactDetails.java b/src/com/android/car/dialer/storage/CipherWrapper.java index ccec3a20..fe99fa57 100644 --- a/src/com/android/car/dialer/ui/search/ContactDetails.java +++ b/src/com/android/car/dialer/storage/CipherWrapper.java @@ -14,21 +14,24 @@ * limitations under the License. */ -package com.android.car.dialer.ui.search; +package com.android.car.dialer.storage; -import android.net.Uri; +import androidx.annotation.NonNull; /** - * A struct that holds the details for a contact search result. + * A wrapper which is used by {@link androidx.room.TypeConverter} to encrypt and decrypt. By using + * this wrapper, we can use {@link androidx.room.TypeConverter} to do encryption before writing and + * do decryption after reading. */ -class ContactDetails { - final String displayName; - final Uri photoUri; - final Uri lookupUri; +class CipherWrapper<T> { + private final T mObject; - ContactDetails(String displayName, String photoUri, Uri lookupUri) { - this.displayName = displayName; - this.photoUri = photoUri == null ? null : Uri.parse(photoUri); - this.lookupUri = lookupUri; + CipherWrapper(@NonNull T object) { + mObject = object; + } + + @NonNull + T get() { + return mObject; } } diff --git a/src/com/android/car/dialer/storage/FavoriteNumberDao.java b/src/com/android/car/dialer/storage/FavoriteNumberDao.java new file mode 100644 index 00000000..395ec25f --- /dev/null +++ b/src/com/android/car/dialer/storage/FavoriteNumberDao.java @@ -0,0 +1,61 @@ +/* + * 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.storage; + +import androidx.lifecycle.LiveData; +import androidx.room.Dao; +import androidx.room.Delete; +import androidx.room.Insert; +import androidx.room.Query; +import androidx.room.Update; + +import java.util.List; + +/** Data access object for the {@link FavoriteNumberEntity} that interacts with the database. */ +@Dao +public interface FavoriteNumberDao { + /** Insert a new entry to database. */ + @Insert + void insert(FavoriteNumberEntity favoriteNumber); + + /** Insert multiple favorite number entries. */ + @Insert + void insertAll(List<FavoriteNumberEntity> favoriteNumbers); + + /** Get all the favorite number entries. */ + @Query("SELECT * FROM favorite_number_entity") + LiveData<List<FavoriteNumberEntity>> loadAll(); + + /** + * Update the given favorite number entry. Does nothing if the entry does not exist in database. + */ + @Update + void update(FavoriteNumberEntity favoriteNumber); + + /** Update multiple favorite number entries. */ + @Update + void updateAll(List<FavoriteNumberEntity> favoriteNumbers); + + /** Delete the favorite number entry from database. */ + @Delete + void delete(FavoriteNumberEntity favoriteNumbers); + + /** Delete all the favorite numbers whose account name do not match any of the devices. */ + @Query("DELETE FROM favorite_number_entity WHERE mAccountName IS NOT NULL" + + " AND mAccountName NOT IN (:pairedDeviceAddresses)") + void cleanup(List<String> pairedDeviceAddresses); +} diff --git a/src/com/android/car/dialer/storage/FavoriteNumberDatabase.java b/src/com/android/car/dialer/storage/FavoriteNumberDatabase.java new file mode 100644 index 00000000..473b1aa2 --- /dev/null +++ b/src/com/android/car/dialer/storage/FavoriteNumberDatabase.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.storage; + +import android.content.Context; + +import androidx.room.Database; +import androidx.room.Room; +import androidx.room.RoomDatabase; +import androidx.room.TypeConverters; + +/** Defines the database for the {@link FavoriteNumberEntity}s. */ +@Database(entities = {FavoriteNumberEntity.class}, exportSchema = false, version = 1) +@TypeConverters(CipherConverter.class) +public abstract class FavoriteNumberDatabase extends RoomDatabase { + + /** Returns the data access object to interact with the favorite number database. */ + public abstract FavoriteNumberDao favoriteNumberDao(); + + private static volatile FavoriteNumberDatabase sFavoriteNumberDatabase; + + static FavoriteNumberDatabase getDatabase(final Context context) { + if (sFavoriteNumberDatabase == null) { + synchronized (FavoriteNumberDatabase.class) { + if (sFavoriteNumberDatabase == null) { + sFavoriteNumberDatabase = Room.databaseBuilder(context.getApplicationContext(), + FavoriteNumberDatabase.class, "favorite_number_database").build(); + } + } + } + return sFavoriteNumberDatabase; + } +} diff --git a/src/com/android/car/dialer/storage/FavoriteNumberEntity.java b/src/com/android/car/dialer/storage/FavoriteNumberEntity.java new file mode 100644 index 00000000..0c125348 --- /dev/null +++ b/src/com/android/car/dialer/storage/FavoriteNumberEntity.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.storage; + +import androidx.annotation.Nullable; +import androidx.room.Entity; +import androidx.room.PrimaryKey; + +/** Favorite number entity */ +@Entity(tableName = "favorite_number_entity") +public class FavoriteNumberEntity { + @PrimaryKey(autoGenerate = true) + private int mIndex; + + private String mContactLookupKey; + + /** Needed to refresh the contact lookup uri. */ + private long mContactId; + + private CipherWrapper<String> mPhoneNumber; + + private String mAccountName; + + private String mAccountType; + + public void setIndex(int index) { + mIndex = index; + } + + public int getIndex() { + return mIndex; + } + + public void setContactLookupKey(String contactLookupKey) { + mContactLookupKey = contactLookupKey; + } + + public String getContactLookupKey() { + return mContactLookupKey; + } + + public void setContactId(long contactId) { + mContactId = contactId; + } + + public long getContactId() { + return mContactId; + } + + public void setPhoneNumber(@Nullable CipherWrapper<String> phoneNumber) { + mPhoneNumber = phoneNumber; + } + + @Nullable + public CipherWrapper<String> getPhoneNumber() { + return mPhoneNumber; + } + + public void setAccountName(String accountName) { + mAccountName = accountName; + } + + public String getAccountName() { + return mAccountName; + } + + public void setAccountType(String accountType) { + mAccountType = accountType; + } + + public String getAccountType() { + return mAccountType; + } +} diff --git a/src/com/android/car/dialer/storage/FavoriteNumberRepository.java b/src/com/android/car/dialer/storage/FavoriteNumberRepository.java new file mode 100644 index 00000000..3762edea --- /dev/null +++ b/src/com/android/car/dialer/storage/FavoriteNumberRepository.java @@ -0,0 +1,268 @@ +/* + * 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.storage; + +import android.bluetooth.BluetoothDevice; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.provider.ContactsContract; +import android.text.TextUtils; + +import androidx.annotation.WorkerThread; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MediatorLiveData; +import androidx.lifecycle.MutableLiveData; + +import com.android.car.dialer.log.L; +import com.android.car.telephony.common.Contact; +import com.android.car.telephony.common.I18nPhoneNumberWrapper; +import com.android.car.telephony.common.InMemoryPhoneBook; +import com.android.car.telephony.common.PhoneNumber; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +/** + * Repository for favorite numbers.It supports the operation to convert the favorite entities to + * {@link Contact}s and add or delete entry. + */ +public class FavoriteNumberRepository { + private static final String TAG = "CD.FavRepository"; + private static ExecutorService sSerializedExecutor; + + static { + sSerializedExecutor = Executors.newSingleThreadExecutor(); + } + + private static volatile FavoriteNumberRepository sFavoriteNumberRepository; + + /** Returns the single instance of the {@link FavoriteNumberRepository}. */ + public static FavoriteNumberRepository getRepository(final Context context) { + if (sFavoriteNumberRepository == null) { + synchronized (FavoriteNumberRepository.class) { + if (sFavoriteNumberRepository == null) { + sFavoriteNumberRepository = new FavoriteNumberRepository(context); + } + } + } + return sFavoriteNumberRepository; + } + + private final Context mContext; + private final FavoriteNumberDao mFavoriteNumberDao; + private final LiveData<List<FavoriteNumberEntity>> mFavoriteNumbers; + private final LiveData<List<Contact>> mFavoriteContacts; + private Future<?> mConvertAllRunnableFuture; + + private FavoriteNumberRepository(Context context) { + mContext = context.getApplicationContext(); + + FavoriteNumberDatabase db = FavoriteNumberDatabase.getDatabase(mContext); + mFavoriteNumberDao = db.favoriteNumberDao(); + mFavoriteNumbers = mFavoriteNumberDao.loadAll(); + + mFavoriteContacts = new FavoriteContactLiveData(mContext); + } + + /** Returns the favorite number list. */ + public LiveData<List<FavoriteNumberEntity>> getFavoriteNumbers() { + return mFavoriteNumbers; + } + + /** Returns the favorite contact list. */ + public LiveData<List<Contact>> getFavoriteContacts() { + return mFavoriteContacts; + } + + /** Add a phone number to favorite. */ + public void addToFavorite(Contact contact, PhoneNumber phoneNumber) { + FavoriteNumberEntity favoriteNumber = new FavoriteNumberEntity(); + favoriteNumber.setContactId(contact.getId()); + favoriteNumber.setContactLookupKey(contact.getLookupKey()); + favoriteNumber.setPhoneNumber(new CipherWrapper<>( + phoneNumber.getRawNumber())); + favoriteNumber.setAccountName(phoneNumber.getAccountName()); + favoriteNumber.setAccountType(phoneNumber.getAccountType()); + sSerializedExecutor.execute(() -> mFavoriteNumberDao.insert(favoriteNumber)); + } + + /** Remove a phone number from favorite. */ + public void removeFromFavorite(Contact contact, PhoneNumber phoneNumber) { + List<FavoriteNumberEntity> favoriteNumbers = mFavoriteNumbers.getValue(); + if (favoriteNumbers == null) { + return; + } + for (FavoriteNumberEntity favoriteNumberEntity : favoriteNumbers) { + if (matches(favoriteNumberEntity, contact, phoneNumber)) { + sSerializedExecutor.execute(() -> mFavoriteNumberDao.delete(favoriteNumberEntity)); + } + } + } + + /** Remove favorite entries for devices that has been unpaired. */ + public void cleanup(Set<BluetoothDevice> pairedDevices) { + L.d(TAG, "remove entries for unpaired devices except %s", pairedDevices); + sSerializedExecutor.execute(() -> { + List<String> pairedDeviceAddresses = new ArrayList<>(); + for (BluetoothDevice device : pairedDevices) { + pairedDeviceAddresses.add(device.getAddress()); + } + mFavoriteNumberDao.cleanup(pairedDeviceAddresses); + }); + } + + /** + * Convert the {@link FavoriteNumberEntity}s to {@link Contact}s and update contact id and + * contact lookup key for all the entities that are out of date. + */ + private void convertToContacts(Context context, final MutableLiveData<List<Contact>> results) { + if (mConvertAllRunnableFuture != null) { + mConvertAllRunnableFuture.cancel(false); + } + + mConvertAllRunnableFuture = sSerializedExecutor.submit(() -> { + if (mFavoriteNumbers.getValue() == null) { + results.postValue(Collections.emptyList()); + return; + } + + ContentResolver cr = context.getContentResolver(); + List<FavoriteNumberEntity> outOfDateList = new ArrayList<>(); + List<Contact> favoriteContacts = new ArrayList<>(); + List<FavoriteNumberEntity> favoriteNumbers = mFavoriteNumbers.getValue(); + for (FavoriteNumberEntity favoriteNumber : favoriteNumbers) { + Contact contact = lookupContact(cr, favoriteNumber); + if (contact != null) { + favoriteContacts.add(contact); + if (favoriteNumber.getContactId() != contact.getId() + || !TextUtils.equals(favoriteNumber.getContactLookupKey(), + contact.getLookupKey())) { + favoriteNumber.setContactLookupKey(contact.getLookupKey()); + favoriteNumber.setContactId(contact.getId()); + outOfDateList.add(favoriteNumber); + } + } + } + results.postValue(favoriteContacts); + if (!outOfDateList.isEmpty()) { + mFavoriteNumberDao.updateAll(outOfDateList); + } + }); + } + + @WorkerThread + private Contact lookupContact(ContentResolver cr, FavoriteNumberEntity favoriteNumber) { + Uri lookupUri = ContactsContract.Contacts.getLookupUri( + favoriteNumber.getContactId(), favoriteNumber.getContactLookupKey()); + Uri refreshedUri = ContactsContract.Contacts.lookupContact( + mContext.getContentResolver(), lookupUri); + if (refreshedUri == null) { + return null; + } + long contactId = ContentUris.parseId(refreshedUri); + + try (Cursor cursor = cr.query( + ContactsContract.CommonDataKinds.Phone.CONTENT_URI, + /* projection= */null, + /* selection= */ ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = ?", + new String[]{String.valueOf(contactId)}, + /* orderBy= */null)) { + if (cursor != null) { + if (cursor.moveToFirst()) { + Contact contact = Contact.fromCursor(mContext, cursor); + contact.getNumbers().clear(); + Contact inMemoryContact = InMemoryPhoneBook.get().lookupContactByKey( + contact.getLookupKey()); + for (PhoneNumber inMemoryPhoneNumber : inMemoryContact.getNumbers()) { + if (numberMatches(favoriteNumber, inMemoryPhoneNumber)) { + contact.getNumbers().add(inMemoryPhoneNumber); + } + } + if (!contact.getNumbers().isEmpty()) { + return contact; + } + } + } + } + return null; + } + + private boolean matches(FavoriteNumberEntity favoriteNumber, Contact contact, + PhoneNumber phoneNumber) { + if (TextUtils.equals(favoriteNumber.getContactLookupKey(), contact.getLookupKey())) { + return numberMatches(favoriteNumber, phoneNumber); + } + + return false; + } + + private boolean numberMatches(FavoriteNumberEntity favoriteNumber, PhoneNumber phoneNumber) { + if (favoriteNumber.getPhoneNumber() == null) { + return false; + } + + if (!TextUtils.equals(favoriteNumber.getAccountName(), phoneNumber.getAccountName()) + || !TextUtils.equals(favoriteNumber.getAccountType(), + phoneNumber.getAccountType())) { + return false; + } + + I18nPhoneNumberWrapper i18nPhoneNumberWrapper = I18nPhoneNumberWrapper.Factory.INSTANCE.get( + mContext, favoriteNumber.getPhoneNumber().get()); + return i18nPhoneNumberWrapper.equals(phoneNumber.getI18nPhoneNumberWrapper()); + } + + private class FavoriteContactLiveData extends MediatorLiveData<List<Contact>> { + private FavoriteContactLiveData(Context context) { + super(); + addSource(InMemoryPhoneBook.get().getContactsLiveData(), + contacts -> convertToContacts(context, this)); + addSource(mFavoriteNumbers, favorites -> convertToContacts(context, this)); + observeForever(favoriteContacts -> L.d(TAG, "%d favorite contacts loaded.", + favoriteContacts.size())); + } + + @Override + public void setValue(List<Contact> contacts) { + // Clean up the old favorite bit and update the new favorite bit. + List<Contact> currentList = getValue(); + if (currentList != null) { + for (Contact contact : currentList) { + for (PhoneNumber phoneNumber : contact.getNumbers()) { + phoneNumber.setIsFavorite(false); + } + } + } + + for (Contact contact : contacts) { + for (PhoneNumber phoneNumber : contact.getNumbers()) { + phoneNumber.setIsFavorite(true); + } + } + + super.setValue(contacts); + } + } +} diff --git a/src/com/android/car/dialer/ui/TelecomActivity.java b/src/com/android/car/dialer/ui/TelecomActivity.java index 32737d11..f97c8c58 100644 --- a/src/com/android/car/dialer/ui/TelecomActivity.java +++ b/src/com/android/car/dialer/ui/TelecomActivity.java @@ -318,7 +318,7 @@ public class TelecomActivity extends FragmentActivity implements return -1; } getSupportFragmentManager().executePendingTransactions(); - while (getSupportFragmentManager().getBackStackEntryCount() > 1) { + while (isBackNavigationAvailable()) { getSupportFragmentManager().popBackStackImmediate(); } @@ -362,6 +362,12 @@ public class TelecomActivity extends FragmentActivity implements : R.style.RootToolbarDisplayOptions, android.R.attr.displayOptions); getActionBar().setDisplayOptions(displayOptions); + + Fragment topFragment = getSupportFragmentManager().findFragmentById( + R.id.content_fragment_container); + if (topFragment instanceof DialerBaseFragment) { + ((DialerBaseFragment) topFragment).setupActionBar(getActionBar()); + } } @Override @@ -374,6 +380,18 @@ public class TelecomActivity extends FragmentActivity implements } @Override + public void onBackPressed() { + // By default onBackPressed will pop all the fragments off the backstack and then finish + // the activity. We want to finish the activity while there is still one fragment on the + // backstack, because we use onBackStackChanged() to set up our fragments. + if (isBackNavigationAvailable()) { + super.onBackPressed(); + } else { + finishAfterTransition(); + } + } + + @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.main_menu, menu); diff --git a/src/com/android/car/dialer/ui/activecall/InCallActivity.java b/src/com/android/car/dialer/ui/activecall/InCallActivity.java index e374564b..7b8215c6 100644 --- a/src/com/android/car/dialer/ui/activecall/InCallActivity.java +++ b/src/com/android/car/dialer/ui/activecall/InCallActivity.java @@ -31,6 +31,7 @@ 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 com.android.car.dialer.notification.InCallNotificationController; import java.util.List; @@ -41,6 +42,7 @@ public class InCallActivity extends FragmentActivity { private Fragment mIncomingCallFragment; private MutableLiveData<Boolean> mShowIncomingCall; + private LiveData<Call> mIncomingCallLiveData; @Override protected void onCreate(Bundle savedInstanceState) { @@ -56,16 +58,26 @@ public class InCallActivity extends FragmentActivity { mShowIncomingCall = new MutableLiveData(); InCallViewModel inCallViewModel = ViewModelProviders.of(this).get(InCallViewModel.class); - LiveData<Call> incomingCallLiveData = LiveDataFunctions.iff(mShowIncomingCall, + mIncomingCallLiveData = LiveDataFunctions.iff(mShowIncomingCall, inCallViewModel.getIncomingCall()); - incomingCallLiveData.observe(this, this::updateIncomingCallVisibility); - LiveDataFunctions.pair(inCallViewModel.getOngoingCallList(), incomingCallLiveData).observe( + mIncomingCallLiveData.observe(this, this::updateIncomingCallVisibility); + LiveDataFunctions.pair(inCallViewModel.getOngoingCallList(), mIncomingCallLiveData).observe( this, this::maybeFinishActivity); handleIntent(); } @Override + protected void onStop() { + super.onStop(); + L.d(TAG, "onStop"); + if (mShowIncomingCall.getValue()) { + InCallNotificationController.get() + .showInCallNotification(mIncomingCallLiveData.getValue()); + } + } + + @Override protected void onNewIntent(Intent i) { super.onNewIntent(i); L.d(TAG, "onNewIntent"); diff --git a/src/com/android/car/dialer/ui/activecall/InCallFragment.java b/src/com/android/car/dialer/ui/activecall/InCallFragment.java index c14a7a74..9a265dd7 100644 --- a/src/com/android/car/dialer/ui/activecall/InCallFragment.java +++ b/src/com/android/car/dialer/ui/activecall/InCallFragment.java @@ -18,7 +18,6 @@ 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; @@ -46,6 +45,8 @@ import com.bumptech.glide.request.RequestOptions; import com.bumptech.glide.request.target.SimpleTarget; import com.bumptech.glide.request.transition.Transition; +import java.util.concurrent.CompletableFuture; + /** A fragment that displays information about a call with actions. */ public abstract class InCallFragment extends Fragment { private static final String TAG = "CD.InCallFragment"; @@ -56,6 +57,15 @@ public abstract class InCallFragment extends Fragment { private TextView mNameView; private ImageView mAvatarView; private BackgroundImageView mBackgroundImage; + private LetterTileDrawable mDefaultAvatar; + private CompletableFuture<Void> mPhoneNumberInfoFuture; + private String mCurrentNumber; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mDefaultAvatar = TelecomUtils.createLetterTile(getContext(), null); + } /** * Shared UI elements between ongoing call and incoming call page: {@link BackgroundImageView} @@ -81,47 +91,64 @@ public abstract class InCallFragment extends Fragment { } 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 += " "; + if (mCurrentNumber != null && mCurrentNumber.equals(number)) { + return; } - 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); + mCurrentNumber = number; + + if (mPhoneNumberInfoFuture != null) { + mPhoneNumberInfoFuture.cancel(true); } - 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); - } - }); + mNameView.setText(TelecomUtils.getFormattedNumber(getContext(), number)); + mPhoneNumberView.setVisibility(View.GONE); + mAvatarView.setImageDrawable(mDefaultAvatar); + + mPhoneNumberInfoFuture = TelecomUtils.getPhoneNumberInfo(getContext(), number) + .thenAcceptAsync((info) -> { + if (getContext() == null) { + return; + } + + mNameView.setText(info.getDisplayName()); + + String phoneNumberLabel = info.getTypeLabel(); + if (!phoneNumberLabel.isEmpty()) { + phoneNumberLabel += " "; + } + phoneNumberLabel += TelecomUtils.getFormattedNumber(getContext(), number); + if (!TextUtils.isEmpty(phoneNumberLabel) + && !phoneNumberLabel.equals(info.getDisplayName())) { + mPhoneNumberView.setText(phoneNumberLabel); + mPhoneNumberView.setVisibility(View.VISIBLE); + } else { + mPhoneNumberView.setVisibility(View.GONE); + } + + LetterTileDrawable letterTile = TelecomUtils.createLetterTile( + getContext(), info.getDisplayName()); + + Glide.with(getContext()) + .asBitmap() + .load(info.getAvatarUri()) + .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 + // 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); + } + }); + }, getContext().getMainExecutor()); } /** Presents the call state and call duration. */ @@ -142,4 +169,12 @@ public abstract class InCallFragment extends Fragment { callStateAndConnectTime.first)); } } + + @Override + public void onStop() { + super.onStop(); + if (mPhoneNumberInfoFuture != null) { + mPhoneNumberInfoFuture.cancel(true); + } + } } diff --git a/src/com/android/car/dialer/ui/activecall/InCallViewModel.java b/src/com/android/car/dialer/ui/activecall/InCallViewModel.java index f5f8704e..bcb9753d 100644 --- a/src/com/android/car/dialer/ui/activecall/InCallViewModel.java +++ b/src/com/android/car/dialer/ui/activecall/InCallViewModel.java @@ -78,6 +78,9 @@ public class InCallViewModel extends AndroidViewModel implements public void onServiceConnected(ComponentName name, IBinder binder) { L.d(TAG, "onServiceConnected: %s, service: %s", name, binder); mInCallService = ((InCallServiceImpl.LocalBinder) binder).getService(); + for (Call call : mInCallService.getCalls()) { + call.registerCallback(mCallStateChangedCallback); + } updateCallList(); mInCallService.addActiveCallListChangedCallback(InCallViewModel.this); } @@ -235,6 +238,9 @@ public class InCallViewModel extends AndroidViewModel implements protected void onCleared() { mContext.unbindService(mInCallServiceConnection); if (mInCallService != null) { + for (Call call : mInCallService.getCalls()) { + call.unregisterCallback(mCallStateChangedCallback); + } mInCallService.removeActiveCallListChangedCallback(this); } mInCallService = null; diff --git a/src/com/android/car/dialer/ui/activecall/OnHoldCallUserProfileFragment.java b/src/com/android/car/dialer/ui/activecall/OnHoldCallUserProfileFragment.java index 7740392a..e0b7c313 100644 --- a/src/com/android/car/dialer/ui/activecall/OnHoldCallUserProfileFragment.java +++ b/src/com/android/car/dialer/ui/activecall/OnHoldCallUserProfileFragment.java @@ -16,7 +16,6 @@ package com.android.car.dialer.ui.activecall; -import android.net.Uri; import android.os.Bundle; import android.telecom.Call; import android.view.LayoutInflater; @@ -27,16 +26,18 @@ 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.apps.common.LetterTileDrawable; 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; +import java.util.concurrent.CompletableFuture; + /** * A fragment that displays information about onhold call. */ @@ -47,6 +48,14 @@ public class OnHoldCallUserProfileFragment extends Fragment { private ImageView mSwapCallsButton; private LiveData<Call> mPrimaryCallLiveData; private LiveData<Call> mSecondaryCallLiveData; + private CompletableFuture<Void> mPhoneNumberInfoFuture; + private LetterTileDrawable mDefaultAvatar; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mDefaultAvatar = TelecomUtils.createLetterTile(getContext(), null); + } @Nullable @Override @@ -75,13 +84,19 @@ public class OnHoldCallUserProfileFragment extends Fragment { return; } - String number = callDetail.getNumber(); - Pair<String, Uri> displayNameAndAvatarUri = TelecomUtils.getDisplayNameAndAvatarUri( - getContext(), number); + if (mPhoneNumberInfoFuture != null) { + mPhoneNumberInfoFuture.cancel(true); + } - mTitle.setText(displayNameAndAvatarUri.first); - TelecomUtils.setContactBitmapAsync(getContext(), mAvatarView, - displayNameAndAvatarUri.second, displayNameAndAvatarUri.first); + String number = callDetail.getNumber(); + mTitle.setText(TelecomUtils.getFormattedNumber(getContext(), number)); + mAvatarView.setImageDrawable(mDefaultAvatar); + mPhoneNumberInfoFuture = TelecomUtils.getPhoneNumberInfo(getContext(), number) + .thenAcceptAsync((info) -> { + mTitle.setText(info.getDisplayName()); + TelecomUtils.setContactBitmapAsync(getContext(), mAvatarView, + info.getAvatarUri(), info.getDisplayName()); + }, getContext().getMainExecutor()); } private void swapCalls() { @@ -95,4 +110,12 @@ public class OnHoldCallUserProfileFragment extends Fragment { mPrimaryCallLiveData.getValue().hold(); } } + + @Override + public void onStop() { + super.onStop(); + if (mPhoneNumberInfoFuture != null) { + mPhoneNumberInfoFuture.cancel(true); + } + } } diff --git a/src/com/android/car/dialer/ui/calllog/CallHistoryFragment.java b/src/com/android/car/dialer/ui/calllog/CallHistoryFragment.java index c68682cf..4cbf82fa 100644 --- a/src/com/android/car/dialer/ui/calllog/CallHistoryFragment.java +++ b/src/com/android/car/dialer/ui/calllog/CallHistoryFragment.java @@ -32,20 +32,27 @@ public class CallHistoryFragment extends DialerListBaseFragment implements CallLogAdapter.OnShowContactDetailListener { private static final String CONTACT_DETAIL_FRAGMENT_TAG = "CONTACT_DETAIL_FRAGMENT_TAG"; + private CallLogAdapter mCallLogAdapter; + 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); + // Don't recreate the adapter if we already have one, so that the list items + // will display immediately upon the view being recreated. If they're not displayed + // immediately, we won't remember our scroll position. + if (mCallLogAdapter == null) { + mCallLogAdapter = new CallLogAdapter( + getContext(), /* onShowContactDetailListener= */this); + } + getRecyclerView().setAdapter(mCallLogAdapter); CallHistoryViewModel viewModel = ViewModelProviders.of(this).get( CallHistoryViewModel.class); - viewModel.getCallHistory().observe(this, callLogAdapter::setUiCallLogs); + viewModel.getCallHistory().observe(this, mCallLogAdapter::setUiCallLogs); } @Override diff --git a/src/com/android/car/dialer/ui/calllog/CallHistoryViewModel.java b/src/com/android/car/dialer/ui/calllog/CallHistoryViewModel.java index 5dc4dc48..bef716bb 100644 --- a/src/com/android/car/dialer/ui/calllog/CallHistoryViewModel.java +++ b/src/com/android/car/dialer/ui/calllog/CallHistoryViewModel.java @@ -18,13 +18,14 @@ 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; @@ -46,7 +47,7 @@ public class CallHistoryViewModel extends AndroidViewModel { /** * Returns the live data for call history list. */ - public LiveData<List<UiCallLog>> getCallHistory() { + public LiveData<List<Object>> 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 index 41e7e415..71427c50 100644 --- a/src/com/android/car/dialer/ui/calllog/CallLogAdapter.java +++ b/src/com/android/car/dialer/ui/calllog/CallLogAdapter.java @@ -20,11 +20,13 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import androidx.annotation.IntDef; 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.HeaderViewHolder; import com.android.car.dialer.ui.common.entity.UiCallLog; import com.android.car.telephony.common.Contact; @@ -32,15 +34,28 @@ import java.util.ArrayList; import java.util.List; /** Adapter for call history list. */ -public class CallLogAdapter extends RecyclerView.Adapter<CallLogViewHolder> { +public class CallLogAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { private static final String TAG = "CD.CallLogAdapter"; + /** IntDef for the different groups of calllog lists separated by time periods. */ + @IntDef({ + EntryType.TYPE_HEADER, + EntryType.TYPE_CALLLOG, + }) + private @interface EntryType { + /** Entry typre is header. */ + int TYPE_HEADER = 1; + + /** Entry type is calllog. */ + int TYPE_CALLLOG = 2; + } + public interface OnShowContactDetailListener { void onShowContactDetail(Contact contact); } - private List<UiCallLog> mUiCallLogs = new ArrayList<>(); + private List<Object> mUiCallLogs = new ArrayList<>(); private Context mContext; private CallLogAdapter.OnShowContactDetailListener mOnShowContactDetailListener; @@ -50,7 +65,10 @@ public class CallLogAdapter extends RecyclerView.Adapter<CallLogViewHolder> { mOnShowContactDetailListener = onShowContactDetailListener; } - public void setUiCallLogs(@NonNull List<UiCallLog> uiCallLogs) { + /** + * Sets calllogs. + */ + public void setUiCallLogs(@NonNull List<Object> uiCallLogs) { L.d(TAG, "setUiCallLogs: %d", uiCallLogs.size()); mUiCallLogs.clear(); mUiCallLogs.addAll(uiCallLogs); @@ -59,20 +77,42 @@ public class CallLogAdapter extends RecyclerView.Adapter<CallLogViewHolder> { @NonNull @Override - public CallLogViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + if (viewType == EntryType.TYPE_CALLLOG) { + View rootView = LayoutInflater.from(mContext) + .inflate(R.layout.call_history_list_item, parent, false); + return new CallLogViewHolder(rootView, mOnShowContactDetailListener); + } + View rootView = LayoutInflater.from(mContext) - .inflate(R.layout.call_history_list_item, parent, false); - return new CallLogViewHolder(rootView, mOnShowContactDetailListener); + .inflate(R.layout.header_item, parent, false); + return new HeaderViewHolder(rootView); } @Override - public void onBindViewHolder(@NonNull CallLogViewHolder holder, int position) { - holder.onBind(mUiCallLogs.get(position)); + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { + if (holder instanceof CallLogViewHolder) { + ((CallLogViewHolder) holder).onBind((UiCallLog) mUiCallLogs.get(position)); + } else { + ((HeaderViewHolder) holder).setHeaderTitle((String) mUiCallLogs.get(position)); + } } @Override - public void onViewRecycled(@NonNull CallLogViewHolder holder) { - holder.onRecycle(); + @EntryType + public int getItemViewType(int position) { + if (mUiCallLogs.get(position) instanceof UiCallLog) { + return EntryType.TYPE_CALLLOG; + } else { + return EntryType.TYPE_HEADER; + } + } + + @Override + public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) { + if (holder instanceof CallLogViewHolder) { + ((CallLogViewHolder) holder).onRecycle(); + } } @Override @@ -80,4 +120,3 @@ public class CallLogAdapter extends RecyclerView.Adapter<CallLogViewHolder> { return mUiCallLogs.size(); } } - diff --git a/src/com/android/car/dialer/ui/common/DialerBaseFragment.java b/src/com/android/car/dialer/ui/common/DialerBaseFragment.java index 2eb276f9..d4a0ee93 100644 --- a/src/com/android/car/dialer/ui/common/DialerBaseFragment.java +++ b/src/com/android/car/dialer/ui/common/DialerBaseFragment.java @@ -18,9 +18,7 @@ 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; @@ -46,50 +44,13 @@ public abstract class DialerBaseFragment extends Fragment { 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) { + public 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(); diff --git a/src/com/android/car/dialer/ui/common/FavoritePhoneNumberListAdapter.java b/src/com/android/car/dialer/ui/common/FavoritePhoneNumberListAdapter.java new file mode 100644 index 00000000..cd039cf4 --- /dev/null +++ b/src/com/android/car/dialer/ui/common/FavoritePhoneNumberListAdapter.java @@ -0,0 +1,129 @@ +/* + * 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.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.recyclerview.widget.RecyclerView; + +import com.android.car.dialer.R; +import com.android.car.telephony.common.Contact; +import com.android.car.telephony.common.PhoneNumber; + +import java.util.ArrayList; +import java.util.List; + +/** + * {@link RecyclerView.Adapter} that presents the {@link PhoneNumber} and its type as two line list + * item with stars to indicate favorite state or user selection to add to favorite. Currently + * favorite phone number is set to disabled so user can not take any action for an existing favorite + * phone number. + */ +public class FavoritePhoneNumberListAdapter extends + RecyclerView.Adapter<FavoritePhoneNumberListAdapter.PhoneNumberViewHolder> { + private final Context mContext; + private final FavoritePhoneNumberPresenter mFavoritePhoneNumberPresenter; + private final List<PhoneNumber> mPhoneNumbers; + private Contact mContact; + + /** + * A presenter that presents the favorite state for phone number and provides the click + * listener. + */ + public interface FavoritePhoneNumberPresenter { + /** Provides the click listener for the given phone number and its present view. */ + void onItemClicked(PhoneNumber phoneNumber, View itemView); + } + + public FavoritePhoneNumberListAdapter(Context context, + FavoritePhoneNumberPresenter favoritePhoneNumberPresenter) { + mContext = context; + mFavoritePhoneNumberPresenter = favoritePhoneNumberPresenter; + mPhoneNumbers = new ArrayList<>(); + } + + /** Sets the phone numbers to display */ + public void setPhoneNumbers(Contact contact, List<PhoneNumber> phoneNumbers) { + mPhoneNumbers.clear(); + mContact = contact; + mPhoneNumbers.addAll(phoneNumbers); + + notifyDataSetChanged(); + } + + @Override + public PhoneNumberViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View itemView = LayoutInflater.from(mContext).inflate( + R.layout.add_favorite_number_list_item, parent, false); + return new PhoneNumberViewHolder(itemView, mFavoritePhoneNumberPresenter); + } + + @Override + public void onBindViewHolder(PhoneNumberViewHolder holder, int position) { + holder.bind(mPhoneNumbers.get(position)); + } + + @Override + public int getItemCount() { + return mPhoneNumbers.size(); + } + + public Contact getContact() { + return mContact; + } + + static class PhoneNumberViewHolder extends RecyclerView.ViewHolder { + + private final FavoritePhoneNumberPresenter mFavoritePhoneNumberPresenter; + private final TextView mPhoneNumberView; + private final TextView mPhoneNumberDescriptionView; + + PhoneNumberViewHolder(View itemView, + FavoritePhoneNumberPresenter favoritePhoneNumberPresenter) { + super(itemView); + mFavoritePhoneNumberPresenter = favoritePhoneNumberPresenter; + mPhoneNumberView = itemView.findViewById(R.id.phone_number); + mPhoneNumberDescriptionView = itemView.findViewById(R.id.phone_number_description); + } + + void bind(PhoneNumber phoneNumber) { + mPhoneNumberView.setText(phoneNumber.getRawNumber()); + CharSequence readableLabel = phoneNumber.getReadableLabel(itemView.getResources()); + if (phoneNumber.isPrimary()) { + mPhoneNumberDescriptionView.setText( + itemView.getResources().getString(R.string.primary_number_description, + readableLabel)); + } else { + mPhoneNumberDescriptionView.setText(readableLabel); + } + + if (phoneNumber.isFavorite()) { + itemView.setActivated(true); + itemView.setEnabled(false); + } else { + itemView.setActivated(false); + itemView.setEnabled(true); + itemView.setOnClickListener( + view -> mFavoritePhoneNumberPresenter.onItemClicked(phoneNumber, itemView)); + } + } + } +} diff --git a/src/com/android/car/dialer/ui/common/UiCallLogLiveData.java b/src/com/android/car/dialer/ui/common/UiCallLogLiveData.java index ac326dee..9a5e8467 100644 --- a/src/com/android/car/dialer/ui/common/UiCallLogLiveData.java +++ b/src/com/android/car/dialer/ui/common/UiCallLogLiveData.java @@ -19,31 +19,36 @@ 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.android.car.telephony.common.TelecomUtils; + import com.google.common.base.Joiner; import com.google.common.base.Splitter; import java.util.ArrayList; +import java.util.Calendar; 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. + * Represents a list of {@link UiCallLog}s and label {@link String}s for UI representation. + * This live data gets 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>> { +public class UiCallLogLiveData extends MediatorLiveData<List<Object>> { private static final String TAG = "CD.UiCallLogLiveData"; private static final String TYPE_AND_RELATIVE_TIME_JOINER = ", "; @@ -55,8 +60,15 @@ public class UiCallLogLiveData extends MediatorLiveData<List<UiCallLog>> { LiveData<List<Contact>> contactListLiveData) { mContext = context; addSource(callHistoryLiveData, this::onCallHistoryChanged); - addSource(contactListLiveData, - (contacts) -> onCallHistoryChanged(callHistoryLiveData.getValue())); + addSource(contactListLiveData, (contacts) -> { + // Don't call onCallHistoryChanged() before the call history is loaded. + // Otherwise, we'll set our value to an empty list instead of just being + // uninitialized while loading. This will cause us to lose our scroll position. + List<PhoneCallLog> callLogs = callHistoryLiveData.getValue(); + if (callLogs != null) { + onCallHistoryChanged(callLogs); + } + }); addSource(heartBeatLiveData, (trigger) -> updateRelativeTime()); } @@ -66,32 +78,35 @@ public class UiCallLogLiveData extends MediatorLiveData<List<UiCallLog>> { private void updateRelativeTime() { boolean hasChanged = false; - List<UiCallLog> uiCallLogs = getValue(); + List<Object> 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; + for (Object object : uiCallLogs) { + if (object instanceof UiCallLog) { + UiCallLog uiCallLog = (UiCallLog) object; + 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; + } } } @@ -100,14 +115,21 @@ public class UiCallLogLiveData extends MediatorLiveData<List<UiCallLog>> { } } - private List<UiCallLog> convert(List<PhoneCallLog> phoneCallLogs) { + private List<Object> convert(List<PhoneCallLog> phoneCallLogs) { if (phoneCallLogs == null) { return Collections.emptyList(); } - List<UiCallLog> uiCallLogs = new ArrayList<>(); + List<Object> uiCallLogs = new ArrayList<>(); + String preHeader = null; InMemoryPhoneBook inMemoryPhoneBook = InMemoryPhoneBook.get(); for (PhoneCallLog phoneCallLog : phoneCallLogs) { + String header = getHeader(phoneCallLog.getLastCallEndTimestamp()); + if (preHeader == null || (!header.equals(preHeader))) { + uiCallLogs.add(header); + } + preHeader = header; + String number = phoneCallLog.getPhoneNumberString(); String relativeTime = getRelativeTime(phoneCallLog.getLastCallEndTimestamp()); if (TelecomUtils.isVoicemailNumber(mContext, number)) { @@ -138,6 +160,9 @@ public class UiCallLogLiveData extends MediatorLiveData<List<UiCallLog>> { uiCallLogs.add(uiCallLog); } + L.i(TAG, "phoneCallLog size: %d, uiCallLog size: %d", + phoneCallLogs.size(), uiCallLogs.size()); + return uiCallLogs; } @@ -160,4 +185,22 @@ public class UiCallLogLiveData extends MediatorLiveData<List<UiCallLog>> { private CharSequence getType(@Nullable PhoneNumber phoneNumber) { return phoneNumber != null ? phoneNumber.getReadableLabel(mContext.getResources()) : ""; } + + private String getHeader(long calllogTime) { + // Calllog times are acquired before getting currentTime, so calllogTime is always + // less than currentTime + if (DateUtils.isToday(calllogTime)) { + return mContext.getResources().getString(R.string.call_log_header_today); + } + + Calendar callLogCalender = Calendar.getInstance(); + callLogCalender.setTimeInMillis(calllogTime); + callLogCalender.add(Calendar.DAY_OF_YEAR, 1); + + if (DateUtils.isToday(callLogCalender.getTimeInMillis())) { + return mContext.getResources().getString(R.string.call_log_header_yesterday); + } + + return mContext.getResources().getString(R.string.call_log_header_older); + } } diff --git a/src/com/android/car/dialer/ui/common/entity/HeaderViewHolder.java b/src/com/android/car/dialer/ui/common/entity/HeaderViewHolder.java new file mode 100644 index 00000000..1c5bed1b --- /dev/null +++ b/src/com/android/car/dialer/ui/common/entity/HeaderViewHolder.java @@ -0,0 +1,45 @@ +/* + * 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.entity; + +import android.view.View; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.android.car.dialer.R; + +/** + * {@link RecyclerView.ViewHolder} for headers to display divider. + */ +public class HeaderViewHolder extends RecyclerView.ViewHolder { + + private TextView mHeaderTitle; + + public HeaderViewHolder(@NonNull View itemView) { + super(itemView); + mHeaderTitle = itemView.findViewById(R.id.title); + } + + /** + * Sets header title. + */ + public void setHeaderTitle(String headerTitle) { + mHeaderTitle.setText(headerTitle); + } +} diff --git a/src/com/android/car/dialer/ui/contact/ContactDefaultNumberActionProvider.java b/src/com/android/car/dialer/ui/contact/ContactDefaultNumberActionProvider.java deleted file mode 100644 index d56c70ca..00000000 --- a/src/com/android/car/dialer/ui/contact/ContactDefaultNumberActionProvider.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * 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 index e1b0f6ed..ad3337e9 100644 --- a/src/com/android/car/dialer/ui/contact/ContactDetailsAdapter.java +++ b/src/com/android/car/dialer/ui/contact/ContactDetailsAdapter.java @@ -17,7 +17,6 @@ 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; @@ -32,31 +31,37 @@ 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>(); + interface PhoneNumberPresenter { + void onClick(Contact contact, PhoneNumber phoneNumber); + } - public ContactDetailsAdapter(@NonNull Context context, @Nullable Contact contact) { + private final Context mContext; + private final PhoneNumberPresenter mPhoneNumberPresenter; + private final ArrayList<Object> mItems = new ArrayList<>(); + private Contact mContact; + + ContactDetailsAdapter( + @NonNull Context context, + @Nullable Contact contact, + @NonNull PhoneNumberPresenter phoneNumberPresenter) { super(); mContext = context; + mPhoneNumberPresenter = phoneNumberPresenter; setContact(contact); } void setContact(Contact contact) { L.d(TAG, "setContact %s", contact); + mContact = contact; mItems.clear(); if (shouldShowHeader()) { mItems.add(contact); @@ -103,7 +108,7 @@ class ContactDetailsAdapter extends RecyclerView.Adapter<ContactDetailsViewHolde View view = LayoutInflater.from(parent.getContext()).inflate(layoutResId, parent, false); - return new ContactDetailsViewHolder(view); + return new ContactDetailsViewHolder(view, mPhoneNumberPresenter); } @Override @@ -113,10 +118,10 @@ class ContactDetailsAdapter extends RecyclerView.Adapter<ContactDetailsViewHolde viewHolder.bind(mContext, (Contact) mItems.get(position)); break; case ID_CONTENT: - viewHolder.bind(mContext, (PhoneNumber) mItems.get(position)); + viewHolder.bind(mContext, mContact, (PhoneNumber) mItems.get(position)); break; default: - Log.e(TAG, "Unknown view type " + viewHolder.getItemViewType()); + L.e(TAG, "Unknown view type %d ", 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 index 2d6f25ac..420975a6 100644 --- a/src/com/android/car/dialer/ui/contact/ContactDetailsFragment.java +++ b/src/com/android/car/dialer/ui/contact/ContactDetailsFragment.java @@ -17,11 +17,8 @@ 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; @@ -35,6 +32,7 @@ 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.PhoneNumber; import com.android.car.telephony.common.TelecomUtils; /** @@ -42,30 +40,19 @@ import com.android.car.telephony.common.TelecomUtils; * 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 { +public class ContactDetailsFragment extends DialerListBaseFragment implements + ContactDetailsAdapter.PhoneNumberPresenter { 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; - } + private ContactDetailsViewModel mContactDetailsViewModel; /** Creates a new ContactDetailsFragment using a {@link Contact}. */ public static ContactDetailsFragment newInstance(Contact contact) { @@ -82,27 +69,12 @@ public class ContactDetailsFragment extends DialerListBaseFragment { 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( + mContactDetailsViewModel = 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); + mContactDetailsLiveData = mContactDetailsViewModel.getContactDetails(mContact); } @Override @@ -114,14 +86,17 @@ public class ContactDetailsFragment extends DialerListBaseFragment { @Override public void onViewCreated(View view, Bundle savedInstanceState) { ContactDetailsAdapter contactDetailsAdapter = new ContactDetailsAdapter(getContext(), - mContact); + mContact, this); getRecyclerView().setAdapter(contactDetailsAdapter); - mContactDetailsLiveData.observe(this, contactDetailsAdapter::setContact); + mContactDetailsLiveData.observe(this, contact -> { + mContact = contact; + onContactChanged(contact); + contactDetailsAdapter.setContact(contact); + }); } private void onContactChanged(Contact contact) { getArguments().clear(); - if (mAvatarView != null) { mAvatarView.setOutlineProvider(ContactAvatarOutputlineProvider.get()); TelecomUtils.setContactBitmapAsync(getContext(), mAvatarView, contact, null); @@ -137,7 +112,7 @@ public class ContactDetailsFragment extends DialerListBaseFragment { } @Override - protected void setupActionBar(@NonNull ActionBar actionBar) { + public void setupActionBar(@NonNull ActionBar actionBar) { actionBar.setCustomView(R.layout.contact_details_action_bar); actionBar.setTitle(null); @@ -170,4 +145,14 @@ public class ContactDetailsFragment extends DialerListBaseFragment { super.onSaveInstanceState(outState); outState.putParcelable(KEY_CONTACT_ENTITY, mContactDetailsLiveData.getValue()); } + + @Override + public void onClick(Contact contact, PhoneNumber phoneNumber) { + boolean isFavorite = phoneNumber.isFavorite(); + if (isFavorite) { + mContactDetailsViewModel.removeFromFavorite(contact, phoneNumber); + } else { + mContactDetailsViewModel.addToFavorite(contact, phoneNumber); + } + } } diff --git a/src/com/android/car/dialer/ui/contact/ContactDetailsViewHolder.java b/src/com/android/car/dialer/ui/contact/ContactDetailsViewHolder.java index 6f1eb35a..55ebf931 100644 --- a/src/com/android/car/dialer/ui/contact/ContactDetailsViewHolder.java +++ b/src/com/android/car/dialer/ui/contact/ContactDetailsViewHolder.java @@ -20,7 +20,6 @@ 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; @@ -51,7 +50,12 @@ class ContactDetailsViewHolder extends RecyclerView.ViewHolder { @Nullable private final View mFavoriteActionView; - ContactDetailsViewHolder(View v) { + @NonNull + private final ContactDetailsAdapter.PhoneNumberPresenter mPhoneNumberPresenter; + + ContactDetailsViewHolder( + View v, + @NonNull ContactDetailsAdapter.PhoneNumberPresenter phoneNumberPresenter) { super(v); mCallActionView = v.findViewById(R.id.call_action_id); mFavoriteActionView = v.findViewById(R.id.contact_details_favorite_button); @@ -61,6 +65,8 @@ class ContactDetailsViewHolder extends RecyclerView.ViewHolder { if (mAvatar != null) { mAvatar.setOutlineProvider(ContactAvatarOutputlineProvider.get()); } + + mPhoneNumberPresenter = phoneNumberPresenter; } public void bind(Context context, Contact contact) { @@ -74,7 +80,7 @@ class ContactDetailsViewHolder extends RecyclerView.ViewHolder { mTitle.setText(contact.getDisplayName()); } - public void bind(Context context, PhoneNumber phoneNumber) { + public void bind(Context context, Contact contact, PhoneNumber phoneNumber) { mTitle.setText(phoneNumber.getRawNumber()); @@ -82,13 +88,16 @@ class ContactDetailsViewHolder extends RecyclerView.ViewHolder { CharSequence readableLabel = phoneNumber.getReadableLabel(context.getResources()); if (phoneNumber.isPrimary()) { mText.setText(context.getString(R.string.primary_number_description, readableLabel)); + mText.setTextAppearance(R.style.TextAppearance_DefaultNumberLabel); } else { mText.setText(readableLabel); + mText.setTextAppearance(R.style.TextAppearance_ContactDetailsListSubtitle); } mCallActionView.setOnClickListener(v -> placeCall(phoneNumber)); + mFavoriteActionView.setActivated(phoneNumber.isFavorite()); mFavoriteActionView.setOnClickListener(v -> { - Toast.makeText(context, "Not yet implemented", Toast.LENGTH_LONG).show(); + mPhoneNumberPresenter.onClick(contact, phoneNumber); mFavoriteActionView.setActivated(!mFavoriteActionView.isActivated()); }); } diff --git a/src/com/android/car/dialer/ui/contact/ContactDetailsViewModel.java b/src/com/android/car/dialer/ui/contact/ContactDetailsViewModel.java index c48fda7a..8cbf6b5c 100644 --- a/src/com/android/car/dialer/ui/contact/ContactDetailsViewModel.java +++ b/src/com/android/car/dialer/ui/contact/ContactDetailsViewModel.java @@ -17,6 +17,9 @@ package com.android.car.dialer.ui.contact; import android.app.Application; +import android.content.ContentUris; +import android.content.Context; +import android.database.Cursor; import android.net.Uri; import android.provider.ContactsContract; @@ -24,33 +27,149 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; +import androidx.lifecycle.MediatorLiveData; import androidx.lifecycle.MutableLiveData; -import com.android.car.dialer.livedata.ContactDetailsLiveData; +import com.android.car.dialer.storage.FavoriteNumberRepository; +import com.android.car.dialer.widget.WorkerExecutor; import com.android.car.telephony.common.Contact; +import com.android.car.telephony.common.InMemoryPhoneBook; +import com.android.car.telephony.common.PhoneNumber; + +import java.util.List; +import java.util.concurrent.Future; /** View model for the contact details page. */ public class ContactDetailsViewModel extends AndroidViewModel { + private final FavoriteNumberRepository mFavoriteNumberRepository; public ContactDetailsViewModel(@NonNull Application application) { super(application); + mFavoriteNumberRepository = FavoriteNumberRepository.getRepository(application); } /** - * Builds the {@link LiveData} for the contact entity described by the given look up uri. + * Builds the {@link LiveData} for the given contact which will update upon contact change and + * favorite repository change. * - * @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. + * @param contact The contact entry. It might be out of date and should update when the {@link + * InMemoryPhoneBook} changes. It always uses the in memory instance to get the + * favorite state for phone numbers. */ - public LiveData<Contact> getContactDetails(@Nullable Uri contactLookupUri) { - if (contactLookupUri == null) { + public LiveData<Contact> getContactDetails(@Nullable Contact contact) { + if (contact == null) { MutableLiveData<Contact> deletedContactDetailsLiveData = new MutableLiveData<>(); deletedContactDetailsLiveData.setValue(null); return deletedContactDetailsLiveData; } - return new ContactDetailsLiveData(getApplication(), contactLookupUri); + return new ContactDetailsLiveData(getApplication(), contact); + } + + /** + * Adds the phone number to favorite. + * + * @param contact The contact the phone number belongs to. + * @param phoneNumber The phone number to add to favorite. + */ + public void addToFavorite(Contact contact, PhoneNumber phoneNumber) { + mFavoriteNumberRepository.addToFavorite(contact, phoneNumber); + } + + /** + * Removes the phone number from favorite. + * + * @param contact The contact the phone number belongs to. + * @param phoneNumber The phone number to remove from favorite. + */ + public void removeFromFavorite(Contact contact, PhoneNumber phoneNumber) { + mFavoriteNumberRepository.removeFromFavorite(contact, phoneNumber); + } + + private class ContactDetailsLiveData extends MediatorLiveData<Contact> { + private final WorkerExecutor mWorkerExecutor; + private final Context mContext; + private Contact mContact; + private Future<?> mRunnableFuture; + + private ContactDetailsLiveData(Context context, Contact contact) { + mContext = context; + mWorkerExecutor = WorkerExecutor.getInstance(); + mContact = contact; + addSource(InMemoryPhoneBook.get().getContactsLiveData(), this::onContactListChanged); + addSource(mFavoriteNumberRepository.getFavoriteContacts(), + this::onFavoriteContactsChanged); + } + + private void onContactListChanged(List<Contact> contacts) { + if (mContact == null) { + return; + } + + Contact inMemoryContact = InMemoryPhoneBook.get().lookupContactByKey( + mContact.getLookupKey()); + if (inMemoryContact != null) { + setValue(inMemoryContact); + return; + } + + if (mRunnableFuture != null) { + mRunnableFuture.cancel(false); + } + mRunnableFuture = mWorkerExecutor.getSingleThreadExecutor().submit( + () -> { + Uri refreshedContactLookupUri = ContactsContract.Contacts.getLookupUri( + mContext.getContentResolver(), mContact.getLookupUri()); + if (refreshedContactLookupUri == null) { + postValue(null); + return; + } + long contactId = ContentUris.parseId(refreshedContactLookupUri); + try (Cursor cursor = mContext.getContentResolver().query( + ContactsContract.CommonDataKinds.Phone.CONTENT_URI, + /* projection= */null, + /* selection= */ + ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = ? ", + new String[]{String.valueOf(contactId)}, + /* orderBy= */null)) { + if (cursor == null) { + postValue(null); + return; + } + + if (cursor.moveToFirst()) { + String lookupKey = cursor.getString(cursor.getColumnIndex( + ContactsContract.CommonDataKinds.Phone.LOOKUP_KEY)); + Contact contact = InMemoryPhoneBook.get().lookupContactByKey( + lookupKey); + postValue(contact); + } + } + } + ); + } + + private void onFavoriteContactsChanged(List<Contact> favoriteContacts) { + if (mContact == null) { + return; + } + Contact inMemoryContact = InMemoryPhoneBook.get().lookupContactByKey( + mContact.getLookupKey()); + setValue(inMemoryContact); + } + + @Override + public void setValue(Contact contact) { + mContact = contact; + super.setValue(contact); + } + + @Override + protected void onInactive() { + super.onInactive(); + if (mRunnableFuture != null) { + mRunnableFuture.cancel(true); + } + } } } diff --git a/src/com/android/car/dialer/ui/contact/ContactListAdapter.java b/src/com/android/car/dialer/ui/contact/ContactListAdapter.java index 9c9be76e..cdc1ccd4 100644 --- a/src/com/android/car/dialer/ui/contact/ContactListAdapter.java +++ b/src/com/android/car/dialer/ui/contact/ContactListAdapter.java @@ -1,4 +1,3 @@ - /* * Copyright (C) 2018 The Android Open Source Project * @@ -17,6 +16,8 @@ package com.android.car.dialer.ui.contact; import android.content.Context; +import android.text.TextUtils; +import android.util.Pair; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -42,16 +43,22 @@ public class ContactListAdapter extends RecyclerView.Adapter<ContactListViewHold private final List<Contact> mContactList = new ArrayList<>(); private final OnShowContactDetailListener mOnShowContactDetailListener; + private Integer mSortMethod; + public ContactListAdapter(Context context, OnShowContactDetailListener onShowContactDetailListener) { mContext = context; mOnShowContactDetailListener = onShowContactDetailListener; } - public void setContactList(List<Contact> contactList) { + /** + * Sets {@link #mContactList} based on live data. + */ + public void setContactList(Pair<Integer, List<Contact>> contactListPair) { mContactList.clear(); - if (contactList != null) { - mContactList.addAll(contactList); + if (contactListPair != null) { + mContactList.addAll(contactListPair.second); + mSortMethod = contactListPair.first; } notifyDataSetChanged(); } @@ -67,11 +74,27 @@ public class ContactListAdapter extends RecyclerView.Adapter<ContactListViewHold @Override public void onBindViewHolder(@NonNull ContactListViewHolder holder, int position) { Contact contact = mContactList.get(position); - holder.onBind(contact); + String header = getHeader(contact); + + boolean showHeader = position == 0 + || (!header.equals(getHeader(mContactList.get(position - 1)))); + holder.onBind(contact, showHeader, header); } @Override public int getItemCount() { return mContactList.size(); } + + private String getHeader(Contact contact) { + String label; + if (mSortMethod.equals(ContactListViewModel.SORT_BY_LAST_NAME)) { + label = contact.getPhonebookLabelAlt(); + } else { + label = contact.getPhonebookLabel(); + } + + return !TextUtils.isEmpty(label) ? label + : mContext.getString(R.string.header_for_type_other); + } } diff --git a/src/com/android/car/dialer/ui/contact/ContactListFragment.java b/src/com/android/car/dialer/ui/contact/ContactListFragment.java index 05155bb2..bf752c94 100644 --- a/src/com/android/car/dialer/ui/contact/ContactListFragment.java +++ b/src/com/android/car/dialer/ui/contact/ContactListFragment.java @@ -40,8 +40,13 @@ public class ContactListFragment extends DialerListBaseFragment implements @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - mContactListAdapter = new ContactListAdapter( - getContext(), /* onShowContactDetailListener= */this); + // Don't recreate the adapter if we already have one, so that the list items + // will display immediately upon the view being recreated. If they're not displayed + // immediately, we won't remember our scroll position. + if (mContactListAdapter == null) { + mContactListAdapter = new ContactListAdapter( + getContext(), /* onShowContactDetailListener= */this); + } getRecyclerView().setAdapter(mContactListAdapter); ContactListViewModel contactListViewModel = ViewModelProviders.of(this).get( diff --git a/src/com/android/car/dialer/ui/contact/ContactListViewHolder.java b/src/com/android/car/dialer/ui/contact/ContactListViewHolder.java index 701f5ec1..02b96026 100644 --- a/src/com/android/car/dialer/ui/contact/ContactListViewHolder.java +++ b/src/com/android/car/dialer/ui/contact/ContactListViewHolder.java @@ -40,6 +40,7 @@ import java.util.List; */ public class ContactListViewHolder extends RecyclerView.ViewHolder { private final ContactListAdapter.OnShowContactDetailListener mOnShowContactDetailListener; + private final TextView mHeaderView; private final ImageView mAvatarView; private final TextView mTitleView; private final TextView mTextView; @@ -50,6 +51,7 @@ public class ContactListViewHolder extends RecyclerView.ViewHolder { ContactListAdapter.OnShowContactDetailListener onShowContactDetailListener) { super(itemView); mOnShowContactDetailListener = onShowContactDetailListener; + mHeaderView = itemView.findViewById(R.id.header); mAvatarView = itemView.findViewById(R.id.icon); mAvatarView.setOutlineProvider(ContactAvatarOutputlineProvider.get()); mTitleView = itemView.findViewById(R.id.title); @@ -58,8 +60,17 @@ public class ContactListViewHolder extends RecyclerView.ViewHolder { mCallActionView = itemView.findViewById(R.id.call_action_id); } - public void onBind(Contact contact) { + /** + * Binds the view holder with relevant data. + */ + public void onBind(Contact contact, boolean showHeader, String header) { TelecomUtils.setContactBitmapAsync(mAvatarView.getContext(), mAvatarView, contact, null); + if (showHeader) { + mHeaderView.setVisibility(View.VISIBLE); + mHeaderView.setText(header); + } else { + mHeaderView.setVisibility(View.GONE); + } mTitleView.setText(contact.getDisplayName()); setLabelText(contact); mShowContactDetailView.setOnClickListener( diff --git a/src/com/android/car/dialer/ui/contact/ContactListViewModel.java b/src/com/android/car/dialer/ui/contact/ContactListViewModel.java index d1938313..90e4517c 100644 --- a/src/com/android/car/dialer/ui/contact/ContactListViewModel.java +++ b/src/com/android/car/dialer/ui/contact/ContactListViewModel.java @@ -18,6 +18,7 @@ package com.android.car.dialer.ui.contact; import android.app.Application; import android.content.Context; +import android.util.Pair; import androidx.annotation.NonNull; import androidx.lifecycle.AndroidViewModel; @@ -40,8 +41,11 @@ import java.util.concurrent.Future; */ public class ContactListViewModel extends AndroidViewModel { + public static final int SORT_BY_FIRST_NAME = 1; + public static final int SORT_BY_LAST_NAME = 2; + private final Context mContext; - private final LiveData<List<Contact>> mSortedContactListLiveData; + private final LiveData<Pair<Integer, List<Contact>>> mSortedContactListLiveData; public ContactListViewModel(@NonNull Application application) { super(application); @@ -58,11 +62,12 @@ public class ContactListViewModel extends AndroidViewModel { /** * Returns a live data which represents a list of all contacts. */ - public LiveData<List<Contact>> getAllContacts() { + public LiveData<Pair<Integer, List<Contact>>> getAllContacts() { return mSortedContactListLiveData; } - private static class SortedContactListLiveData extends MediatorLiveData<List<Contact>> { + private static class SortedContactListLiveData + extends MediatorLiveData<Pair<Integer, List<Contact>>> { private final LiveData<List<Contact>> mContactListLiveData; private final SharedPreferencesLiveData mPreferencesLiveData; @@ -114,12 +119,15 @@ public class ContactListViewModel extends AndroidViewModel { List<Contact> contactList = mContactListLiveData.getValue(); Comparator<Contact> comparator; + Integer sortMethod; if (mPreferencesLiveData.getValue() == null || mPreferencesLiveData.getValue().getString(key, defaultValue) .equals(defaultValue)) { comparator = mFirstNameComparator; + sortMethod = SORT_BY_FIRST_NAME; } else { comparator = mLastNameComparator; + sortMethod = SORT_BY_LAST_NAME; } // SingleThreadPoolExecutor is used here to avoid multiple threads sorting the list @@ -130,7 +138,7 @@ public class ContactListViewModel extends AndroidViewModel { Runnable runnable = () -> { Collections.sort(contactList, comparator); - postValue(contactList); + postValue(new Pair<>(sortMethod, contactList)); }; mRunnableFuture = WorkerExecutor.getInstance().getSingleThreadExecutor().submit( runnable); diff --git a/src/com/android/car/dialer/ui/dialpad/DialpadFragment.java b/src/com/android/car/dialer/ui/dialpad/DialpadFragment.java index 1adcc5ee..e0a2f5a2 100644 --- a/src/com/android/car/dialer/ui/dialpad/DialpadFragment.java +++ b/src/com/android/car/dialer/ui/dialpad/DialpadFragment.java @@ -105,15 +105,14 @@ public class DialpadFragment extends AbstractDialpadFragment { @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); + mMode = getArguments().getInt(DIALPAD_MODE_KEY); + L.d(TAG, "onCreate mode: %s", mMode); 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); @@ -160,7 +159,7 @@ public class DialpadFragment extends AbstractDialpadFragment { } @Override - protected void setupActionBar(ActionBar actionBar) { + public 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. diff --git a/src/com/android/car/dialer/ui/dialpad/InCallDialpadFragment.java b/src/com/android/car/dialer/ui/dialpad/InCallDialpadFragment.java index 37868069..77e606a5 100644 --- a/src/com/android/car/dialer/ui/dialpad/InCallDialpadFragment.java +++ b/src/com/android/car/dialer/ui/dialpad/InCallDialpadFragment.java @@ -108,7 +108,7 @@ public class InCallDialpadFragment extends AbstractDialpadFragment { } @Override - protected void setupActionBar(ActionBar actionBar) { + public void setupActionBar(ActionBar actionBar) { // No-op } diff --git a/src/com/android/car/dialer/ui/favorite/AddFavoriteFragment.java b/src/com/android/car/dialer/ui/favorite/AddFavoriteFragment.java new file mode 100644 index 00000000..1ae42bc0 --- /dev/null +++ b/src/com/android/car/dialer/ui/favorite/AddFavoriteFragment.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.favorite; + +import android.app.AlertDialog; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; + +import androidx.lifecycle.ViewModelProviders; +import androidx.recyclerview.widget.RecyclerView; + +import com.android.car.dialer.R; +import com.android.car.dialer.ui.common.FavoritePhoneNumberListAdapter; +import com.android.car.dialer.ui.search.ContactResultsFragment; +import com.android.car.telephony.common.Contact; +import com.android.car.telephony.common.PhoneNumber; + +import java.util.HashSet; +import java.util.Set; + +/** A fragment that allows the user to search for and select favorite phone numbers */ +public class AddFavoriteFragment extends ContactResultsFragment { + + /** Creates a new instance of AddFavoriteFragment */ + public static AddFavoriteFragment newInstance() { + return new AddFavoriteFragment(); + } + + private AlertDialog mCurrentDialog; + private FavoritePhoneNumberListAdapter mDialogAdapter; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + FavoriteViewModel favoriteViewModel = ViewModelProviders.of(getActivity()).get( + FavoriteViewModel.class); + Set<PhoneNumber> selectedNumbers = new HashSet<>(); + + mDialogAdapter = new FavoritePhoneNumberListAdapter(getContext(), + (phoneNumber, itemView) -> { + boolean isActivated = itemView.isActivated(); + itemView.setActivated(!isActivated); + if (isActivated) { + selectedNumbers.remove(phoneNumber); + } else { + selectedNumbers.add(phoneNumber); + } + } + ); + + View dialogView = LayoutInflater.from(getContext()).inflate( + R.layout.add_to_favorite_dialog, null, false); + RecyclerView recyclerView = dialogView.findViewById(R.id.list); + recyclerView.setAdapter(mDialogAdapter); + + mCurrentDialog = new AlertDialog.Builder(getContext()) + .setTitle(R.string.select_number_dialog_title) + .setView(dialogView) + .setNegativeButton(R.string.cancel_add_favorites_dialog, null) + .setPositiveButton(R.string.confirm_add_favorites_dialog, + (d, which) -> { + for (PhoneNumber number : selectedNumbers) { + favoriteViewModel.addToFavorite(mDialogAdapter.getContact(), + number); + } + selectedNumbers.clear(); + getFragmentManager().popBackStackImmediate(); + }) + .create(); + } + + @Override + public void onShowContactDetail(Contact contact) { + if (contact == null) { + mCurrentDialog.dismiss(); + return; + } + + mDialogAdapter.setPhoneNumbers(contact, contact.getNumbers()); + mCurrentDialog.show(); + } +} diff --git a/src/com/android/car/dialer/ui/favorite/FavoriteAdapter.java b/src/com/android/car/dialer/ui/favorite/FavoriteAdapter.java index ae051217..da7ebe51 100644 --- a/src/com/android/car/dialer/ui/favorite/FavoriteAdapter.java +++ b/src/com/android/car/dialer/ui/favorite/FavoriteAdapter.java @@ -16,11 +16,9 @@ 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; @@ -37,9 +35,18 @@ import java.util.List; */ public class FavoriteAdapter extends RecyclerView.Adapter<FavoriteContactViewHolder> { private static final String TAG = "CD.FavoriteAdapter"; + private static final int TYPE_CONTACT = 0; + private static final int TYPE_ADD_FAVORITE = 1; + + /** Listener interface for when the add favorite button is clicked */ + public interface OnAddFavoriteClickedListener { + /** Called when the add favorite button is clicked */ + void onAddFavoriteClicked(); + } private List<Contact> mFavoriteContacts = Collections.emptyList(); private OnItemClickedListener<Contact> mListener; + private OnAddFavoriteClickedListener mAddFavoriteListener; /** Sets the favorite contact list. */ public void setFavoriteContacts(List<Contact> favoriteContacts) { @@ -54,25 +61,38 @@ public class FavoriteAdapter extends RecyclerView.Adapter<FavoriteContactViewHol } @Override + public int getItemViewType(int position) { + return position < mFavoriteContacts.size() + ? TYPE_CONTACT + : TYPE_ADD_FAVORITE; + } + + @Override public FavoriteContactViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.favorite_contact_list_item, parent, false); + View view; + if (viewType == TYPE_CONTACT) { + view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.favorite_contact_list_item, parent, false); + } else { + view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.add_favorite_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 { + if (getItemViewType(position) == TYPE_CONTACT) { Contact contact = mFavoriteContacts.get(position); - viewHolder.onBind(context, contact); + viewHolder.onBind(contact); viewHolder.itemView.setOnClickListener((v) -> onItemViewClicked(contact)); + } else { + viewHolder.itemView.setOnClickListener((v) -> { + if (mAddFavoriteListener != null) { + mAddFavoriteListener.onAddFavoriteClicked(); + } + }); } } @@ -88,4 +108,12 @@ public class FavoriteAdapter extends RecyclerView.Adapter<FavoriteContactViewHol public void setOnListItemClickedListener(OnItemClickedListener<Contact> listener) { mListener = listener; } + + /** + * Sets a {@link OnAddFavoriteClickedListener listener} which will be called when the + * "Add favorite" button is clicked. + */ + public void setOnAddFavoriteClickedListener(OnAddFavoriteClickedListener listener) { + mAddFavoriteListener = listener; + } } diff --git a/src/com/android/car/dialer/ui/favorite/FavoriteContactViewHolder.java b/src/com/android/car/dialer/ui/favorite/FavoriteContactViewHolder.java index 1853cc43..93d36e2c 100644 --- a/src/com/android/car/dialer/ui/favorite/FavoriteContactViewHolder.java +++ b/src/com/android/car/dialer/ui/favorite/FavoriteContactViewHolder.java @@ -48,7 +48,9 @@ class FavoriteContactViewHolder extends RecyclerView.ViewHolder { FavoriteContactViewHolder(View v) { super(v); mIcon = v.findViewById(R.id.icon); - mIcon.setOutlineProvider(ContactAvatarOutputlineProvider.get()); + if (mIcon != null) { + mIcon.setOutlineProvider(ContactAvatarOutputlineProvider.get()); + } mTitle = v.findViewById(R.id.title); mText = v.findViewById(R.id.text); } @@ -56,7 +58,8 @@ class FavoriteContactViewHolder extends RecyclerView.ViewHolder { /** * Binds view with favorite contact. */ - public void onBind(Context context, @Nonnull Contact contact) { + public void onBind(@Nonnull Contact contact) { + Context context = itemView.getContext(); String displayName = contact.getDisplayName(); mTitle.setText(displayName); @@ -82,13 +85,4 @@ class FavoriteContactViewHolder extends RecyclerView.ViewHolder { 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 index 7e3573de..61b3ff46 100644 --- a/src/com/android/car/dialer/ui/favorite/FavoriteFragment.java +++ b/src/com/android/car/dialer/ui/favorite/FavoriteFragment.java @@ -20,7 +20,6 @@ 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; @@ -60,7 +59,7 @@ public class FavoriteFragment extends DialerBaseFragment { }); emptyPage.findViewById(R.id.add_favorite_button).setOnClickListener(v -> - Toast.makeText(getContext(), "Not yet implemented", Toast.LENGTH_LONG).show()); + pushContentFragment(AddFavoriteFragment.newInstance(), null)); return view; } diff --git a/src/com/android/car/dialer/ui/favorite/FavoriteListFragment.java b/src/com/android/car/dialer/ui/favorite/FavoriteListFragment.java index 41583727..80e5e2ce 100644 --- a/src/com/android/car/dialer/ui/favorite/FavoriteListFragment.java +++ b/src/com/android/car/dialer/ui/favorite/FavoriteListFragment.java @@ -50,6 +50,7 @@ public class FavoriteListFragment extends DialerListBaseFragment { getRecyclerView().setItemAnimator(null); FavoriteAdapter adapter = new FavoriteAdapter(); + adapter.setOnAddFavoriteClickedListener(this::onAddFavoriteClicked); FavoriteViewModel favoriteViewModel = ViewModelProviders.of(getActivity()).get( FavoriteViewModel.class); @@ -73,6 +74,10 @@ public class FavoriteListFragment extends DialerListBaseFragment { UiCallManager.get().placeCall(phoneNumber.getRawNumber())); } + private void onAddFavoriteClicked() { + pushContentFragment(AddFavoriteFragment.newInstance(), null); + } + private class ItemSpacingDecoration extends RecyclerView.ItemDecoration { @Override @@ -83,17 +88,14 @@ public class FavoriteListFragment extends DialerListBaseFragment { int numColumns = resources.getInteger(R.integer.favorite_fragment_grid_column); int leftPadding = resources.getDimensionPixelOffset(R.dimen.favorite_card_space_horizontal); - int topPadding = + int verticalPadding = 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); + outRect.set(leftPadding, verticalPadding, 0, verticalPadding); } } } diff --git a/src/com/android/car/dialer/ui/favorite/FavoriteViewModel.java b/src/com/android/car/dialer/ui/favorite/FavoriteViewModel.java index f58f56ff..84f4bb31 100644 --- a/src/com/android/car/dialer/ui/favorite/FavoriteViewModel.java +++ b/src/com/android/car/dialer/ui/favorite/FavoriteViewModel.java @@ -17,10 +17,13 @@ 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.dialer.storage.FavoriteNumberRepository; import com.android.car.telephony.common.Contact; +import com.android.car.telephony.common.PhoneNumber; import java.util.List; @@ -28,15 +31,25 @@ import java.util.List; * View model for {@link FavoriteFragment}. */ public class FavoriteViewModel extends AndroidViewModel { - private LiveData<List<Contact>> mFavoriteContactsLiveData; + private FavoriteNumberRepository mFavoriteNumberRepository; public FavoriteViewModel(Application application) { super(application); - mFavoriteContactsLiveData = FavoriteContactLiveData.newInstance(application); + mFavoriteNumberRepository = FavoriteNumberRepository.getRepository(application); } /** Returns favorite contact list live data. */ public LiveData<List<Contact>> getFavoriteContacts() { - return mFavoriteContactsLiveData; + return mFavoriteNumberRepository.getFavoriteContacts(); + } + + /** + * Adds the phone number to favorite. + * + * @param contact The contact the phone number belongs to. + * @param phoneNumber The phone number to add to favorite. + */ + public void addToFavorite(Contact contact, PhoneNumber phoneNumber) { + mFavoriteNumberRepository.addToFavorite(contact, phoneNumber); } } diff --git a/src/com/android/car/dialer/ui/menu/MenuActionProvider.java b/src/com/android/car/dialer/ui/menu/MenuActionProvider.java index 108d4277..10386614 100644 --- a/src/com/android/car/dialer/ui/menu/MenuActionProvider.java +++ b/src/com/android/car/dialer/ui/menu/MenuActionProvider.java @@ -49,7 +49,7 @@ public class MenuActionProvider extends ActionProvider { @Override public View onCreateActionView(MenuItem forItem) { View actionView = LayoutInflater.from(mContext).inflate(R.layout.menu_action_view, null); - actionView.setTooltip(forItem.getTitle()); + actionView.setContentDescription(forItem.getTitle()); ImageView icon = actionView.findViewById(R.id.menu_icon); icon.setImageDrawable(forItem.getIcon()); if (forItem.getIconTintMode() != null) { diff --git a/src/com/android/car/dialer/ui/search/ContactResultViewHolder.java b/src/com/android/car/dialer/ui/search/ContactResultViewHolder.java index a7071000..121be99a 100644 --- a/src/com/android/car/dialer/ui/search/ContactResultViewHolder.java +++ b/src/com/android/car/dialer/ui/search/ContactResultViewHolder.java @@ -25,6 +25,7 @@ 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.Contact; import com.android.car.telephony.common.TelecomUtils; /** @@ -51,15 +52,14 @@ public class ContactResultViewHolder extends RecyclerView.ViewHolder { /** * Populates the view that is represented by this ViewHolder with the information in the - * provided {@link ContactDetails}. + * provided {@link Contact}. */ - public void bind(ContactDetails details) { - mContactCard.setOnClickListener(v -> { - mOnShowContactDetailListener.onShowContactDetail(details.lookupUri); - }); + public void bind(Contact contact) { + mContactCard.setOnClickListener( + v -> mOnShowContactDetailListener.onShowContactDetail(contact)); - mContactName.setText(details.displayName); - TelecomUtils.setContactBitmapAsync(mContext, mContactPicture, details.photoUri, - details.displayName); + mContactName.setText(contact.getDisplayName()); + TelecomUtils.setContactBitmapAsync(mContext, mContactPicture, contact.getAvatarUri(), + contact.getDisplayName()); } } diff --git a/src/com/android/car/dialer/ui/search/ContactResultsAdapter.java b/src/com/android/car/dialer/ui/search/ContactResultsAdapter.java index 639ca9f8..7b1ac1f0 100644 --- a/src/com/android/car/dialer/ui/search/ContactResultsAdapter.java +++ b/src/com/android/car/dialer/ui/search/ContactResultsAdapter.java @@ -17,7 +17,6 @@ package com.android.car.dialer.ui.search; import android.database.Cursor; -import android.net.Uri; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -25,6 +24,7 @@ import android.view.ViewGroup; 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; @@ -36,10 +36,10 @@ import java.util.List; public class ContactResultsAdapter extends RecyclerView.Adapter<ContactResultViewHolder> { interface OnShowContactDetailListener { - void onShowContactDetail(Uri contactLookupUri); + void onShowContactDetail(Contact contact); } - private final List<ContactDetails> mContacts = new ArrayList<>(); + private final List<Contact> mContacts = new ArrayList<>(); private final OnShowContactDetailListener mOnShowContactDetailListener; public ContactResultsAdapter(OnShowContactDetailListener onShowContactDetailListener) { @@ -58,7 +58,7 @@ 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(List<ContactDetails> data) { + public void setData(List<Contact> data) { mContacts.clear(); mContacts.addAll(data); notifyDataSetChanged(); diff --git a/src/com/android/car/dialer/ui/search/ContactResultsFragment.java b/src/com/android/car/dialer/ui/search/ContactResultsFragment.java index 56b72930..a53768b2 100644 --- a/src/com/android/car/dialer/ui/search/ContactResultsFragment.java +++ b/src/com/android/car/dialer/ui/search/ContactResultsFragment.java @@ -17,7 +17,8 @@ package com.android.car.dialer.ui.search; import android.app.ActionBar; -import android.net.Uri; +import android.app.SearchManager; +import android.content.Context; import android.os.Bundle; import android.text.TextUtils; import android.view.Menu; @@ -35,6 +36,7 @@ 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; +import com.android.car.telephony.common.Contact; /** * A fragment that will take a search query, look up contacts that match and display those @@ -69,8 +71,6 @@ public class ContactResultsFragment extends DialerListBaseFragment implements private RecyclerView.OnScrollListener mOnScrollChangeListener; private SearchView mSearchView; - private boolean mKeyboardShown = false; - @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -89,15 +89,6 @@ public class ContactResultsFragment extends DialerListBaseFragment implements } 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 @@ -131,29 +122,25 @@ public class ContactResultsFragment extends DialerListBaseFragment implements } @Override - protected void setupActionBar(@NonNull ActionBar actionBar) { + public 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 + // 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); + SearchManager searchManager = + (SearchManager) getContext().getSystemService(Context.SEARCH_SERVICE); + searchView.setSearchableInfo( + searchManager.getSearchableInfo(getActivity().getComponentName())); // 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. + // an icon, but doing so also focuses on it and shows the keyboard. searchView.setIconified(false); - if (mKeyboardShown) { - searchView.clearFocus(); - } else { - mKeyboardShown = true; - } - searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String query) { @@ -177,8 +164,8 @@ public class ContactResultsFragment extends DialerListBaseFragment implements } @Override - public void onPause() { - super.onPause(); + public void onStop() { + super.onStop(); mSearchView.clearFocus(); } @@ -206,8 +193,8 @@ public class ContactResultsFragment extends DialerListBaseFragment implements } @Override - public void onShowContactDetail(Uri contactLookupUri) { - Fragment contactDetailsFragment = ContactDetailsFragment.newInstance(contactLookupUri); + public void onShowContactDetail(Contact contact) { + Fragment contactDetailsFragment = ContactDetailsFragment.newInstance(contact); 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 index 8dc979fb..07921988 100644 --- a/src/com/android/car/dialer/ui/search/ContactResultsViewModel.java +++ b/src/com/android/car/dialer/ui/search/ContactResultsViewModel.java @@ -17,6 +17,7 @@ package com.android.car.dialer.ui.search; import android.app.Application; +import android.content.ContentResolver; import android.database.Cursor; import android.net.Uri; import android.provider.ContactsContract; @@ -26,8 +27,11 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; +import androidx.lifecycle.MediatorLiveData; import androidx.lifecycle.MutableLiveData; +import com.android.car.telephony.common.Contact; +import com.android.car.telephony.common.InMemoryPhoneBook; import com.android.car.telephony.common.ObservableAsyncQuery; import com.android.car.telephony.common.QueryParam; @@ -39,77 +43,99 @@ import java.util.List; 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 + ContactsContract.Contacts.LOOKUP_KEY }; - private final SearchQueryParamProvider mSearchQueryParamProvider; - private final ObservableAsyncQuery mObservableAsyncQuery; - private final MutableLiveData<List<ContactDetails>> mContactSearchResultsLiveData; - private String mSearchQuery; + private final ContactResultsLiveData mContactSearchResultsLiveData; + private final MutableLiveData<String> mSearchQueryLiveData; public ContactResultsViewModel(@NonNull Application application) { super(application); - mSearchQueryParamProvider = new SearchQueryParamProvider(); - mContactSearchResultsLiveData = new MutableLiveData<>(); - mObservableAsyncQuery = new ObservableAsyncQuery(mSearchQueryParamProvider, - application.getContentResolver(), this::onQueryFinished); + mSearchQueryLiveData = new MutableLiveData<>(); + mContactSearchResultsLiveData = new ContactResultsLiveData(application.getContentResolver(), + mSearchQueryLiveData); } void setSearchQuery(String searchQuery) { - if (TextUtils.equals(mSearchQuery, searchQuery)) { + if (TextUtils.equals(mSearchQueryLiveData.getValue(), searchQuery)) { return; } - mSearchQuery = searchQuery; - if (TextUtils.isEmpty(searchQuery)) { - mContactSearchResultsLiveData.setValue(Collections.emptyList()); - } else { - mObservableAsyncQuery.startQuery(); - } + mSearchQueryLiveData.setValue(searchQuery); } - LiveData<List<ContactDetails>> getContactSearchResults() { + LiveData<List<Contact>> getContactSearchResults() { return mContactSearchResultsLiveData; } String getSearchQuery() { - return mSearchQuery; + return mSearchQueryLiveData.getValue(); } - private void onQueryFinished(@Nullable Cursor cursor) { - if (cursor == null) { - mContactSearchResultsLiveData.setValue(Collections.emptyList()); - return; + private static class ContactResultsLiveData extends MediatorLiveData<List<Contact>> { + private final SearchQueryParamProvider mSearchQueryParamProvider; + private final ObservableAsyncQuery mObservableAsyncQuery; + + ContactResultsLiveData(ContentResolver contentResolver, + LiveData<String> searchQueryLiveData) { + mSearchQueryParamProvider = new SearchQueryParamProvider(searchQueryLiveData); + mObservableAsyncQuery = new ObservableAsyncQuery(mSearchQueryParamProvider, + contentResolver, this::onQueryFinished); + + addSource(InMemoryPhoneBook.get().getContactsLiveData(), this::onContactsChange); + addSource(searchQueryLiveData, this::onSearchQueryChanged); } - 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); + private void onContactsChange(List<Contact> contactList) { + if (contactList == null || contactList.isEmpty()) { + mObservableAsyncQuery.stopQuery(); + setValue(Collections.emptyList()); + } else { + mObservableAsyncQuery.startQuery(); + } + } - Uri lookupUri = ContactsContract.Contacts.getLookupUri( - cursor.getLong(idColIdx), cursor.getString(lookupColIdx)); + private void onSearchQueryChanged(String searchQuery) { + if (TextUtils.isEmpty(searchQuery)) { + mObservableAsyncQuery.stopQuery(); + setValue(Collections.emptyList()); + } else { + mObservableAsyncQuery.startQuery(); + } + } - contactDetails.add(new ContactDetails( - cursor.getString(nameColIdx), - cursor.getString(photoUriColIdx), - lookupUri)); + private void onQueryFinished(@Nullable Cursor cursor) { + if (cursor == null) { + setValue(Collections.emptyList()); + return; + } + + List<Contact> contacts = new ArrayList<>(); + while (cursor.moveToNext()) { + int lookupColIdx = cursor.getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY); + Contact contact = InMemoryPhoneBook.get().lookupContactByKey( + cursor.getString(lookupColIdx)); + if (contact != null) { + contacts.add(contact); + } + } + setValue(contacts); + cursor.close(); } - mContactSearchResultsLiveData.setValue(contactDetails); - cursor.close(); } - private class SearchQueryParamProvider implements QueryParam.Provider { + private static class SearchQueryParamProvider implements QueryParam.Provider { + private final LiveData<String> mSearchQueryLiveData; + + private SearchQueryParamProvider(LiveData<String> searchQueryLiveData) { + mSearchQueryLiveData = searchQueryLiveData; + } @Nullable @Override public QueryParam getQueryParam() { Uri lookupUri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_FILTER_URI, - Uri.encode(mSearchQuery)); + Uri.encode(mSearchQueryLiveData.getValue())); return new QueryParam(lookupUri, CONTACT_DETAILS_PROJECTION, ContactsContract.Contacts.HAS_PHONE_NUMBER + "!=0", /* selectionArgs= */null, /* orderBy= */null); diff --git a/tests/robotests/res/values-h610dp/styles.xml b/tests/robotests/res/values-h610dp/styles.xml new file mode 100644 index 00000000..5a4eaa16 --- /dev/null +++ b/tests/robotests/res/values-h610dp/styles.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. + --> + +<resources> + <!-- Subheader override to make test pass. Need to use qualifier to h610dp to make it work --> + <style name="SubheaderText"> + <item name="android:textAppearance">@style/TextAppearance.Body3</item> + </style> +</resources> diff --git a/tests/robotests/src/com/android/car/dialer/livedata/BluetoothPairListLiveDataTest.java b/tests/robotests/src/com/android/car/dialer/livedata/BluetoothPairListLiveDataTest.java index a91bff04..db880117 100644 --- a/tests/robotests/src/com/android/car/dialer/livedata/BluetoothPairListLiveDataTest.java +++ b/tests/robotests/src/com/android/car/dialer/livedata/BluetoothPairListLiveDataTest.java @@ -121,12 +121,13 @@ public class BluetoothPairListLiveDataTest { @Test public void testOnInactiveUnregister() { mBluetoothPairListLiveData.observe(mMockLifecycleOwner, - (value) -> mMockObserver.onChanged(value)); - mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START); + value -> mMockObserver.onChanged(value)); int preNumber = mReceiverVerifier.getReceiverNumber(); + mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START); mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY); - mReceiverVerifier.verifyReceiverUnregistered(INTENT_ACTION, preNumber); + + assertThat(mReceiverVerifier.getReceiverNumber()).isEqualTo(preNumber); } private void verifyBondedDevices(Set bondedDevices) { 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 index 63383620..d249e4b4 100644 --- a/tests/robotests/src/com/android/car/dialer/ui/calllog/CallHistoryFragmentTest.java +++ b/tests/robotests/src/com/android/car/dialer/ui/calllog/CallHistoryFragmentTest.java @@ -26,6 +26,7 @@ import android.view.View; import android.widget.TextView; import androidx.lifecycle.MutableLiveData; +import androidx.recyclerview.widget.RecyclerView; import com.android.car.apps.common.widget.PagedRecyclerView; import com.android.car.dialer.CarDialerRobolectricTestRunner; @@ -34,6 +35,7 @@ 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.HeaderViewHolder; import com.android.car.dialer.ui.common.entity.UiCallLog; import com.android.car.dialer.widget.CallTypeIconsView; import com.android.car.telephony.common.InMemoryPhoneBook; @@ -53,16 +55,19 @@ import org.robolectric.annotation.Config; import java.util.Arrays; import java.util.List; -@Config(shadows = {ShadowAndroidViewModelFactory.class}) +@Config(shadows = {ShadowAndroidViewModelFactory.class}, qualifiers = "h610dp") @RunWith(CarDialerRobolectricTestRunner.class) public class CallHistoryFragmentTest { + private static final String HEADER = "TODAY"; 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 static final long TIME_STAMP_1 = System.currentTimeMillis(); + private static final long TIME_STAMP_2 = System.currentTimeMillis() - 10000; - private CallLogViewHolder mViewHolder; + private CallHistoryFragment mCallHistoryFragment; + private RecyclerView.ViewHolder mCalllogViewHolder; + private RecyclerView.ViewHolder mHeaderViewHolder; @Mock private UiCallManager mMockUiCallManager; @Mock @@ -84,20 +89,22 @@ public class CallHistoryFragmentTest { 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)); + MutableLiveData<List<Object>> callLog = new MutableLiveData<>(); + callLog.setValue(Arrays.asList(HEADER, uiCallLog)); ShadowAndroidViewModelFactory.add(CallHistoryViewModel.class, mMockCallHistoryViewModel); when(mMockCallHistoryViewModel.getCallHistory()).thenReturn(callLog); - CallHistoryFragment callHistoryFragment = CallHistoryFragment.newInstance(); + mCallHistoryFragment = CallHistoryFragment.newInstance(); FragmentTestActivity mFragmentTestActivity = Robolectric.buildActivity( FragmentTestActivity.class).create().resume().get(); - mFragmentTestActivity.setFragment(callHistoryFragment); + mFragmentTestActivity.setFragment(mCallHistoryFragment); - PagedRecyclerView recyclerView = callHistoryFragment.getView().findViewById(R.id.list_view); + PagedRecyclerView recyclerView = mCallHistoryFragment.getView() + .findViewById(R.id.list_view); // set up layout for recyclerView recyclerView.layoutBothForTesting(0, 0, 100, 1000); - mViewHolder = (CallLogViewHolder) recyclerView.findViewHolderForLayoutPosition(0); + mHeaderViewHolder = recyclerView.findViewHolderForLayoutPosition(0); + mCalllogViewHolder = recyclerView.findViewHolderForLayoutPosition(1); } @After @@ -106,10 +113,21 @@ public class CallHistoryFragmentTest { } @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( + public void testHeaderViewHolder() { + assertThat(mHeaderViewHolder instanceof HeaderViewHolder).isTrue(); + + TextView title = ((HeaderViewHolder) mHeaderViewHolder).itemView.findViewById(R.id.title); + assertThat(title.getText()).isEqualTo(HEADER); + } + + @Test + public void testCalllogViewHolder() { + assertThat(mCalllogViewHolder instanceof CallLogViewHolder).isTrue(); + + CallLogViewHolder viewHolder = (CallLogViewHolder) mCalllogViewHolder; + TextView titleView = viewHolder.itemView.findViewById(R.id.title); + TextView textView = viewHolder.itemView.findViewById(R.id.text); + CallTypeIconsView callTypeIconsView = viewHolder.itemView.findViewById( R.id.call_type_icons); assertThat(titleView.getText()).isEqualTo(UI_CALLOG_TITLE); @@ -122,7 +140,8 @@ public class CallHistoryFragmentTest { @Test public void testClick_placeCall() { - View callButton = mViewHolder.itemView.findViewById(R.id.call_action_id); + View callButton = ((CallLogViewHolder) mCalllogViewHolder).itemView + .findViewById(R.id.call_action_id); assertThat(callButton.hasOnClickListeners()).isTrue(); callButton.performClick(); 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 index c41f2a65..02598a5c 100644 --- a/tests/robotests/src/com/android/car/dialer/ui/contact/ContactDetailsFragmentTest.java +++ b/tests/robotests/src/com/android/car/dialer/ui/contact/ContactDetailsFragmentTest.java @@ -22,7 +22,6 @@ 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; @@ -35,8 +34,10 @@ 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.InMemoryPhoneBook; import com.android.car.telephony.common.PhoneNumber; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -44,6 +45,7 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.robolectric.Robolectric; +import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; import java.util.Arrays; @@ -60,8 +62,6 @@ public class ContactDetailsFragmentTest { @Mock private ContactDetailsViewModel mMockContactDetailsViewModel; @Mock - private Uri mMockContactLookupUri; - @Mock private Contact mMockContact; @Mock private PhoneNumber mMockPhoneNumber1; @@ -74,6 +74,8 @@ public class ContactDetailsFragmentTest { public void setUp() { MockitoAnnotations.initMocks(this); + InMemoryPhoneBook.init(RuntimeEnvironment.application); + when(mMockContact.getDisplayName()).thenReturn(DISPLAY_NAME); when(mMockPhoneNumber1.getRawNumber()).thenReturn(RAW_NUMBERS[0]); when(mMockPhoneNumber2.getRawNumber()).thenReturn(RAW_NUMBERS[1]); @@ -86,13 +88,17 @@ public class ContactDetailsFragmentTest { contactDetails.setValue(mMockContact); ShadowAndroidViewModelFactory.add(ContactDetailsViewModel.class, mMockContactDetailsViewModel); - when(mMockContactDetailsViewModel.getContactDetails(mMockContactLookupUri)).thenReturn( + when(mMockContactDetailsViewModel.getContactDetails(mMockContact)).thenReturn( contactDetails); } + @After + public void tearDown() { + InMemoryPhoneBook.tearDown(); + } + @Test public void testCreateWithContact() { - when(mMockContact.getLookupUri()).thenReturn(mMockContactLookupUri); mContactDetailsFragment = ContactDetailsFragment.newInstance(mMockContact); setUpFragment(); 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 index a6227730..d8c87c18 100644 --- a/tests/robotests/src/com/android/car/dialer/ui/contact/ContactListFragmentTest.java +++ b/tests/robotests/src/com/android/car/dialer/ui/contact/ContactListFragmentTest.java @@ -24,6 +24,7 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.util.Pair; import android.view.View; import androidx.fragment.app.Fragment; @@ -36,6 +37,7 @@ 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.dialer.ui.favorite.FavoriteViewModel; import com.android.car.telephony.common.Contact; import com.android.car.telephony.common.PhoneNumber; @@ -52,7 +54,7 @@ import org.robolectric.shadows.ShadowAlertDialog; import java.util.Arrays; import java.util.List; -@Config(shadows = {ShadowAndroidViewModelFactory.class}) +@Config(shadows = {ShadowAndroidViewModelFactory.class}, qualifiers = "h610dp") @RunWith(CarDialerRobolectricTestRunner.class) public class ContactListFragmentTest { private static final String RAW_NUMBNER = "6502530000"; @@ -67,6 +69,8 @@ public class ContactListFragmentTest { @Mock private ContactDetailsViewModel mMockContactDetailsViewModel; @Mock + private FavoriteViewModel mMockFavoriteViewModel; + @Mock private Contact mMockContact1; @Mock private Contact mMockContact2; @@ -79,16 +83,19 @@ public class ContactListFragmentTest { public void setUp() { MockitoAnnotations.initMocks(this); - MutableLiveData<List<Contact>> contactList = new MutableLiveData<>(); - contactList.setValue(Arrays.asList(mMockContact1, mMockContact2, mMockContact3)); + MutableLiveData<Pair<Integer, List<Contact>>> contactList = new MutableLiveData<>(); + contactList.setValue(new Pair<>(ContactListViewModel.SORT_BY_LAST_NAME, + 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); + + ShadowAndroidViewModelFactory.add(FavoriteViewModel.class, mMockFavoriteViewModel); + when(mMockFavoriteViewModel.getFavoriteContacts()).thenReturn(new MutableLiveData<>()); } @Test 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 index 982991da..5ec96e0b 100644 --- a/tests/robotests/src/com/android/car/dialer/ui/contact/ContactListViewHolderTest.java +++ b/tests/robotests/src/com/android/car/dialer/ui/contact/ContactListViewHolderTest.java @@ -43,10 +43,12 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowAlertDialog; import java.util.Arrays; +@Config(qualifiers = "h610dp") @RunWith(CarDialerRobolectricTestRunner.class) public class ContactListViewHolderTest { private static final String DISPLAY_NAME = "Display Name"; @@ -79,7 +81,7 @@ public class ContactListViewHolderTest { @Test public void testDisplayName() { when(mMockContact.getDisplayName()).thenReturn(DISPLAY_NAME); - mContactListViewHolder.onBind(mMockContact); + mContactListViewHolder.onBind(mMockContact, false, ""); assertThat(((TextView) mItemView.findViewById(R.id.title)).getText()).isEqualTo( DISPLAY_NAME); @@ -90,7 +92,7 @@ public class ContactListViewHolderTest { 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); + mContactListViewHolder.onBind(mMockContact, false, ""); assertThat(((TextView) mItemView.findViewById(R.id.text)).getText()).isEqualTo(LABEL_1); } @@ -100,7 +102,7 @@ public class ContactListViewHolderTest { 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); + mContactListViewHolder.onBind(mMockContact, false, ""); assertThat(((TextView) mItemView.findViewById(R.id.text)).getText()).isEqualTo( mContext.getResources().getText( @@ -114,7 +116,7 @@ public class ContactListViewHolderTest { PhoneNumber phoneNumber = mock(PhoneNumber.class); when(phoneNumber.getLabel()).thenReturn(null); when(mMockContact.getNumbers()).thenReturn(Arrays.asList(phoneNumber)); - mContactListViewHolder.onBind(mMockContact); + mContactListViewHolder.onBind(mMockContact, false, ""); assertThat(((TextView) mItemView.findViewById(R.id.text)).getText()).isEqualTo(""); } @@ -127,7 +129,7 @@ public class ContactListViewHolderTest { false, 0, null, null, 0); when(mMockContact.getNumbers()).thenReturn(Arrays.asList(phoneNumber1, phoneNumber2)); when(mMockContact.hasPrimaryPhoneNumber()).thenReturn(false); - mContactListViewHolder.onBind(mMockContact); + mContactListViewHolder.onBind(mMockContact, false, ""); assertThat(((TextView) mItemView.findViewById(R.id.text)).getText()).isEqualTo( mContext.getString(R.string.type_multiple)); @@ -142,7 +144,7 @@ public class ContactListViewHolderTest { when(mMockContact.getNumbers()).thenReturn(Arrays.asList(phoneNumber1, phoneNumber2)); when(mMockContact.hasPrimaryPhoneNumber()).thenReturn(true); when(mMockContact.getPrimaryPhoneNumber()).thenReturn(phoneNumber2); - mContactListViewHolder.onBind(mMockContact); + mContactListViewHolder.onBind(mMockContact, false, ""); assertThat(((TextView) mItemView.findViewById(R.id.text)).getText()).isEqualTo( mContext.getString(R.string.primary_number_description, LABEL_2)); @@ -157,7 +159,7 @@ public class ContactListViewHolderTest { when(mMockContact.getNumbers()).thenReturn(Arrays.asList(phoneNumber1, phoneNumber2)); when(mMockContact.hasPrimaryPhoneNumber()).thenReturn(true); when(mMockContact.getPrimaryPhoneNumber()).thenReturn(phoneNumber2); - mContactListViewHolder.onBind(mMockContact); + mContactListViewHolder.onBind(mMockContact, false, ""); assertThat(((TextView) mItemView.findViewById(R.id.text)).getText()).isEqualTo( mContext.getString(R.string.primary_number_description, @@ -176,7 +178,7 @@ public class ContactListViewHolderTest { when(mMockContact.getNumbers()).thenReturn(Arrays.asList(phoneNumber1, phoneNumber2)); when(mMockContact.hasPrimaryPhoneNumber()).thenReturn(true); when(mMockContact.getPrimaryPhoneNumber()).thenReturn(phoneNumber2); - mContactListViewHolder.onBind(mMockContact); + mContactListViewHolder.onBind(mMockContact, false, ""); assertThat(((TextView) mItemView.findViewById(R.id.text)).getText()).isEqualTo( mContext.getString(R.string.primary_number_description, "null")); @@ -188,7 +190,7 @@ public class ContactListViewHolderTest { 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); + mContactListViewHolder.onBind(mMockContact, false, ""); View callActionView = mItemView.findViewById(R.id.call_action_id); assertThat(callActionView.hasOnClickListeners()).isTrue(); @@ -209,7 +211,7 @@ public class ContactListViewHolderTest { false, 0, null, null, 0); when(mMockContact.getNumbers()).thenReturn(Arrays.asList(phoneNumber1, phoneNumber2)); when(mMockContact.hasPrimaryPhoneNumber()).thenReturn(false); - mContactListViewHolder.onBind(mMockContact); + mContactListViewHolder.onBind(mMockContact, false, ""); assertThat(ShadowAlertDialog.getLatestAlertDialog()).isNull(); View callActionView = mItemView.findViewById(R.id.call_action_id); @@ -229,7 +231,7 @@ public class ContactListViewHolderTest { when(mMockContact.getNumbers()).thenReturn(Arrays.asList(phoneNumber1, phoneNumber2)); when(mMockContact.hasPrimaryPhoneNumber()).thenReturn(true); when(mMockContact.getPrimaryPhoneNumber()).thenReturn(phoneNumber2); - mContactListViewHolder.onBind(mMockContact); + mContactListViewHolder.onBind(mMockContact, false, ""); View callActionView = mItemView.findViewById(R.id.call_action_id); assertThat(callActionView.hasOnClickListeners()).isTrue(); @@ -243,7 +245,7 @@ public class ContactListViewHolderTest { @Test public void testClickShowContactDetailView_showContactDetail() { - mContactListViewHolder.onBind(mMockContact); + mContactListViewHolder.onBind(mMockContact, false, ""); View showContactDetailActionView = mItemView.findViewById(R.id.show_contact_detail_id); assertThat(showContactDetailActionView.hasOnClickListeners()).isTrue(); 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 index 2e9f8087..3e349b12 100644 --- a/tests/robotests/src/com/android/car/dialer/ui/search/ContactResultsFragmentTest.java +++ b/tests/robotests/src/com/android/car/dialer/ui/search/ContactResultsFragmentTest.java @@ -19,10 +19,8 @@ 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; @@ -38,13 +36,16 @@ 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 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.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.Robolectric; +import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; import java.util.Arrays; @@ -60,23 +61,35 @@ public class ContactResultsFragmentTest { private ContactResultsFragment mContactResultsFragment; private FragmentTestActivity mFragmentTestActivity; private PagedRecyclerView mListView; - private MutableLiveData<List<ContactDetails>> mContactSearchResultsLiveData; + private MutableLiveData<List<Contact>> mContactSearchResultsLiveData; @Mock private ContactResultsViewModel mMockContactResultsViewModel; @Mock private ContactDetailsViewModel mMockContactDetailsViewModel; @Mock private Contact mMockContact; + @Mock + private Contact mContact1, mContact2, mContact3; @Before public void setUp() { MockitoAnnotations.initMocks(this); + InMemoryPhoneBook.init(RuntimeEnvironment.application); mContactSearchResultsLiveData = new MutableLiveData<>(); when(mMockContactResultsViewModel.getContactSearchResults()) .thenReturn(mContactSearchResultsLiveData); ShadowAndroidViewModelFactory.add( ContactResultsViewModel.class, mMockContactResultsViewModel); + + when(mContact1.getDisplayName()).thenReturn(DISPLAY_NAMES[0]); + when(mContact2.getDisplayName()).thenReturn(DISPLAY_NAMES[1]); + when(mContact3.getDisplayName()).thenReturn(DISPLAY_NAMES[2]); + } + + @After + public void tearDown() { + InMemoryPhoneBook.tearDown(); } @Test @@ -89,11 +102,8 @@ public class ContactResultsFragmentTest { @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)); + Arrays.asList(mContact1, mContact2, mContact3)); mContactResultsFragment = ContactResultsFragment.newInstance(INITIAL_SEARCH_QUERY); setUpFragment(); @@ -105,11 +115,8 @@ public class ContactResultsFragmentTest { @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)); + Arrays.asList(mContact1, mContact2, mContact3)); MutableLiveData<Contact> contactDetailLiveData = new MutableLiveData<>(); contactDetailLiveData.setValue(mMockContact); |