summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorandroid-build-team Robot <android-build-team-robot@google.com>2019-08-15 04:38:48 +0000
committerandroid-build-team Robot <android-build-team-robot@google.com>2019-08-15 04:38:48 +0000
commit3224f9d299a7bcfbca10c767625ea79fc828c095 (patch)
treeb84ede97ae4e25e24f41b48ec0f72ce0bab9a5fe
parentd09ff8acda8ba990c6a124595e6d7505efa77d40 (diff)
parentecf167812fc2d94aaee73d3128b003e75ff61131 (diff)
downloadplatform_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
-rw-r--r--Android.mk48
-rw-r--r--AndroidManifest.xml8
-rw-r--r--res/drawable/ic_add_favorite.xml2
-rw-r--r--res/drawable/ic_bluetooth_disable.xml24
-rw-r--r--res/layout-h456dp/keypad_dividers.xml68
-rw-r--r--res/layout-port/dialpad_fragment.xml9
-rw-r--r--res/layout-port/user_profile_large.xml8
-rw-r--r--res/layout/add_favorite_list_item.xml29
-rw-r--r--res/layout/add_favorite_number_list_item.xml57
-rw-r--r--res/layout/add_to_favorite_dialog.xml24
-rw-r--r--res/layout/contact_details_number.xml4
-rw-r--r--res/layout/contact_list_item.xml88
-rw-r--r--res/layout/contact_user_profile.xml102
-rw-r--r--res/layout/dialpad_info.xml7
-rw-r--r--res/layout/favorite_fragment.xml5
-rw-r--r--res/layout/header_item.xml24
-rw-r--r--res/layout/keypad.xml50
-rw-r--r--res/layout/keypad_dividers.xml19
-rw-r--r--res/layout/no_hfp.xml10
-rw-r--r--res/layout/user_profile_large.xml7
-rw-r--r--res/menu/contact_edit.xml24
-rw-r--r--res/values-bg/strings.xml2
-rw-r--r--res/values-en-rAU/strings.xml2
-rw-r--r--res/values-en-rCA/strings.xml2
-rw-r--r--res/values-en-rGB/strings.xml2
-rw-r--r--res/values-en-rIN/strings.xml2
-rw-r--r--res/values-en-rXC/strings.xml2
-rw-r--r--res/values-h456dp/dimens.xml23
-rw-r--r--res/values-h610dp/dimens.xml6
-rw-r--r--res/values-in/strings.xml2
-rw-r--r--res/values-it/strings.xml2
-rw-r--r--res/values-ja/strings.xml2
-rw-r--r--res/values-mn/strings.xml2
-rw-r--r--res/values-ms/strings.xml2
-rw-r--r--res/values-my/strings.xml2
-rw-r--r--res/values-nb/strings.xml2
-rw-r--r--res/values-port/dimens.xml2
-rw-r--r--res/values-si/strings.xml2
-rw-r--r--res/values-sl/strings.xml2
-rw-r--r--res/values-sw/strings.xml2
-rw-r--r--res/values-tr/strings.xml2
-rw-r--r--res/values-w1280dp-land/dimens.xml18
-rw-r--r--res/values/arrays.xml6
-rw-r--r--res/values/colors.xml1
-rw-r--r--res/values/dimens.xml19
-rw-r--r--res/values/strings.xml24
-rw-r--r--res/values/styles.xml26
-rw-r--r--res/values/themes.xml2
-rw-r--r--src/com/android/car/dialer/livedata/BluetoothPairListLiveData.java1
-rw-r--r--src/com/android/car/dialer/livedata/ContactDetailsLiveData.java98
-rw-r--r--src/com/android/car/dialer/livedata/FavoriteContactLiveData.java80
-rw-r--r--src/com/android/car/dialer/livedata/SharedPreferencesLiveData.java5
-rw-r--r--src/com/android/car/dialer/notification/InCallNotificationController.java68
-rw-r--r--src/com/android/car/dialer/notification/MissedCallNotificationController.java64
-rw-r--r--src/com/android/car/dialer/notification/NotificationUtils.java58
-rw-r--r--src/com/android/car/dialer/storage/BluetoothBondedListReceiver.java49
-rw-r--r--src/com/android/car/dialer/storage/CipherConverter.java162
-rw-r--r--src/com/android/car/dialer/storage/CipherWrapper.java (renamed from src/com/android/car/dialer/ui/search/ContactDetails.java)25
-rw-r--r--src/com/android/car/dialer/storage/FavoriteNumberDao.java61
-rw-r--r--src/com/android/car/dialer/storage/FavoriteNumberDatabase.java47
-rw-r--r--src/com/android/car/dialer/storage/FavoriteNumberEntity.java88
-rw-r--r--src/com/android/car/dialer/storage/FavoriteNumberRepository.java268
-rw-r--r--src/com/android/car/dialer/ui/TelecomActivity.java20
-rw-r--r--src/com/android/car/dialer/ui/activecall/InCallActivity.java18
-rw-r--r--src/com/android/car/dialer/ui/activecall/InCallFragment.java113
-rw-r--r--src/com/android/car/dialer/ui/activecall/InCallViewModel.java6
-rw-r--r--src/com/android/car/dialer/ui/activecall/OnHoldCallUserProfileFragment.java39
-rw-r--r--src/com/android/car/dialer/ui/calllog/CallHistoryFragment.java15
-rw-r--r--src/com/android/car/dialer/ui/calllog/CallHistoryViewModel.java5
-rw-r--r--src/com/android/car/dialer/ui/calllog/CallLogAdapter.java61
-rw-r--r--src/com/android/car/dialer/ui/common/DialerBaseFragment.java41
-rw-r--r--src/com/android/car/dialer/ui/common/FavoritePhoneNumberListAdapter.java129
-rw-r--r--src/com/android/car/dialer/ui/common/UiCallLogLiveData.java105
-rw-r--r--src/com/android/car/dialer/ui/common/entity/HeaderViewHolder.java45
-rw-r--r--src/com/android/car/dialer/ui/contact/ContactDefaultNumberActionProvider.java95
-rw-r--r--src/com/android/car/dialer/ui/contact/ContactDetailsAdapter.java29
-rw-r--r--src/com/android/car/dialer/ui/contact/ContactDetailsFragment.java61
-rw-r--r--src/com/android/car/dialer/ui/contact/ContactDetailsViewHolder.java17
-rw-r--r--src/com/android/car/dialer/ui/contact/ContactDetailsViewModel.java137
-rw-r--r--src/com/android/car/dialer/ui/contact/ContactListAdapter.java33
-rw-r--r--src/com/android/car/dialer/ui/contact/ContactListFragment.java9
-rw-r--r--src/com/android/car/dialer/ui/contact/ContactListViewHolder.java13
-rw-r--r--src/com/android/car/dialer/ui/contact/ContactListViewModel.java16
-rw-r--r--src/com/android/car/dialer/ui/dialpad/DialpadFragment.java7
-rw-r--r--src/com/android/car/dialer/ui/dialpad/InCallDialpadFragment.java2
-rw-r--r--src/com/android/car/dialer/ui/favorite/AddFavoriteFragment.java98
-rw-r--r--src/com/android/car/dialer/ui/favorite/FavoriteAdapter.java52
-rw-r--r--src/com/android/car/dialer/ui/favorite/FavoriteContactViewHolder.java16
-rw-r--r--src/com/android/car/dialer/ui/favorite/FavoriteFragment.java3
-rw-r--r--src/com/android/car/dialer/ui/favorite/FavoriteListFragment.java12
-rw-r--r--src/com/android/car/dialer/ui/favorite/FavoriteViewModel.java21
-rw-r--r--src/com/android/car/dialer/ui/menu/MenuActionProvider.java2
-rw-r--r--src/com/android/car/dialer/ui/search/ContactResultViewHolder.java16
-rw-r--r--src/com/android/car/dialer/ui/search/ContactResultsAdapter.java8
-rw-r--r--src/com/android/car/dialer/ui/search/ContactResultsFragment.java41
-rw-r--r--src/com/android/car/dialer/ui/search/ContactResultsViewModel.java106
-rw-r--r--tests/robotests/res/values-h610dp/styles.xml23
-rw-r--r--tests/robotests/src/com/android/car/dialer/livedata/BluetoothPairListLiveDataTest.java7
-rw-r--r--tests/robotests/src/com/android/car/dialer/ui/calllog/CallHistoryFragmentTest.java49
-rw-r--r--tests/robotests/src/com/android/car/dialer/ui/contact/ContactDetailsFragmentTest.java16
-rw-r--r--tests/robotests/src/com/android/car/dialer/ui/contact/ContactListFragmentTest.java15
-rw-r--r--tests/robotests/src/com/android/car/dialer/ui/contact/ContactListViewHolderTest.java26
-rw-r--r--tests/robotests/src/com/android/car/dialer/ui/search/ContactResultsFragmentTest.java29
103 files changed, 2458 insertions, 950 deletions
diff --git a/Android.mk b/Android.mk
index 2d8691c8..f1e12e7b 100644
--- a/Android.mk
+++ b/Android.mk
@@ -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">&#8230;</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);