diff options
24 files changed, 880 insertions, 134 deletions
diff --git a/res/drawable/ic_merge.xml b/res/drawable/ic_merge.xml new file mode 100644 index 00000000..443fed62 --- /dev/null +++ b/res/drawable/ic_merge.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2020 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="@dimen/primary_icon_size" + android:height="@dimen/primary_icon_size" + android:viewportWidth="24.0" + android:viewportHeight="24.0" + android:tint="@color/icon_tint_state_list"> + <path + android:pathData="M18.41,18.59L17,20l-3.41,-3.41L15,15.18l3.41,3.41zM16.5,7.5L12,3 7.5,7.5l1.41,1.41L11,6.83v6.35l-5.41,5.41L7,20l6,-6V6.83l2.09,2.09L16.5,7.5z" + android:fillColor="#000000"/> +</vector> diff --git a/res/drawable/ic_swap_calls.xml b/res/drawable/ic_swap_calls.xml deleted file mode 100644 index 098ca973..00000000 --- a/res/drawable/ic_swap_calls.xml +++ /dev/null @@ -1,27 +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. ---> -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="@dimen/primary_icon_size" - android:height="@dimen/primary_icon_size" - android:viewportWidth="24.0" - android:viewportHeight="24.0"> - - <path - android:pathData="M0 0h24v24H0z" /> - <path - android:fillColor="#fffafafa" - android:pathData="M18 4l-4 4h3v7c0 1.1-.9 2-2 2s-2-.9-2-2V8c0-2.21-1.79-4-4-4S5 5.79 5 8v7H2l4 4 4-4H7V8c0-1.1.9-2 2-2s2 .9 2 2v7c0 2.21 1.79 4 4 4s4-1.79 4-4V8h3l-4-4z"/> -</vector> diff --git a/res/layout/conference_call_user_list.xml b/res/layout/conference_call_user_list.xml new file mode 100644 index 00000000..018a7269 --- /dev/null +++ b/res/layout/conference_call_user_list.xml @@ -0,0 +1,66 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2020 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT 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" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent"> + + <LinearLayout + android:id="@+id/conference_header" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="35dp" + android:layout_marginBottom="35dp" + android:layout_marginLeft="135dp" + app:layout_constraintBottom_toTopOf="@+id/recycler_view" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent"> + + <TextView + android:id="@+id/conference_title" + android:textAppearance="@style/TextAppearance.InCallState" + android:layout_width="wrap_content" + android:layout_height="wrap_content"/> + + <!-- 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/call_duration" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textAppearance="@style/TextAppearance.InCallState" + android:singleLine="true"/> + </LinearLayout> + + <com.android.car.ui.recyclerview.CarUiRecyclerView + android:id="@+id/recycler_view" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_marginTop="30dp" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintTop_toBottomOf="@+id/conference_header" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent"/> + +</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file diff --git a/res/layout/in_call_activity.xml b/res/layout/in_call_activity.xml index c7a0684b..23dc0fed 100644 --- a/res/layout/in_call_activity.xml +++ b/res/layout/in_call_activity.xml @@ -28,4 +28,10 @@ android:id="@+id/ongoing_call_fragment" android:layout_width="match_parent" android:layout_height="match_parent"/> + + <fragment + android:name="com.android.car.dialer.ui.activecall.OngoingConfCallFragment" + android:id="@+id/ongoing_conf_call_fragment" + android:layout_width="match_parent" + android:layout_height="match_parent"/> </FrameLayout> diff --git a/res/layout/on_going_call_controller_bar_fragment.xml b/res/layout/on_going_call_controller_bar_fragment.xml index c9902c20..8061b712 100644 --- a/res/layout/on_going_call_controller_bar_fragment.xml +++ b/res/layout/on_going_call_controller_bar_fragment.xml @@ -52,14 +52,13 @@ limitations under the License. app:layout_constraintEnd_toStartOf="@+id/voice_channel_view" app:layout_constraintTop_toTopOf="parent"/> - <LinearLayout + <FrameLayout android:id="@+id/voice_channel_view" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:orientation="vertical" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toEndOf="@+id/end_call_button" - app:layout_constraintEnd_toStartOf="@+id/pause_button" + app:layout_constraintEnd_toStartOf="@+id/button_wrapper" app:layout_constraintTop_toTopOf="parent"> <ImageView @@ -76,17 +75,32 @@ limitations under the License. android:layout_height="wrap_content" android:visibility="gone"/> - </LinearLayout> + </FrameLayout> - <ImageView - android:id="@+id/pause_button" - android:layout_width="@dimen/in_call_button_size" - android:layout_height="@dimen/in_call_button_size" - android:background="@drawable/dialer_ripple_background" - android:scaleType="center" - android:src="@drawable/ic_pause_activatable" + <LinearLayout + android:id="@+id/button_wrapper" + android:layout_width="wrap_content" + android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toEndOf="@+id/voice_channel_view" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toTopOf="parent"/> + app:layout_constraintTop_toTopOf="parent"> + + <ImageView + android:id="@+id/merge_button" + android:layout_width="@dimen/in_call_button_size" + android:layout_height="@dimen/in_call_button_size" + android:background="@drawable/dialer_ripple_background" + android:scaleType="center" + android:src="@drawable/ic_merge"/> + + <ImageView + android:id="@+id/pause_button" + android:layout_width="@dimen/in_call_button_size" + android:layout_height="@dimen/in_call_button_size" + android:background="@drawable/dialer_ripple_background" + android:scaleType="center" + android:src="@drawable/ic_pause_activatable"/> + </LinearLayout> + </androidx.constraintlayout.widget.ConstraintLayout> diff --git a/res/layout/ongoing_call_fragment.xml b/res/layout/ongoing_call_fragment.xml index 007531b1..02b28180 100644 --- a/res/layout/ongoing_call_fragment.xml +++ b/res/layout/ongoing_call_fragment.xml @@ -45,15 +45,19 @@ limitations under the License. app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toTopOf="@+id/ongoing_call_control_bar"/> - <include - layout="@layout/user_profile_large" - android:id="@+id/user_profile_container" + <LinearLayout android:layout_width="match_parent" android:layout_height="0dp" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toTopOf="@+id/ongoing_call_control_bar" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent"/> + app:layout_constraintStart_toStartOf="parent"> + + <include + layout="@layout/user_profile_large" + android:id="@+id/user_profile_container"/> + + </LinearLayout> <fragment android:id="@+id/onhold_user_profile" diff --git a/res/layout/ongoing_conf_call_fragment.xml b/res/layout/ongoing_conf_call_fragment.xml new file mode 100644 index 00000000..2fdf0d5a --- /dev/null +++ b/res/layout/ongoing_conf_call_fragment.xml @@ -0,0 +1,83 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2020 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT 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"> + + <!-- This ConstraintLayout is to make a full-screen transparent background --> + <!-- so that the ripple effects in the controller bar buttons work. --> + <!-- If you put the transparent background on the root element of --> + <!-- in_call_fragment, the BackgroundImageView will cover up the ripples. --> + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + android:background="@android:color/transparent"> + + <fragment + android:name="com.android.car.dialer.ui.dialpad.InCallDialpadFragment" + android:id="@+id/incall_dialpad_fragment" + android:layout_width="match_parent" + android:layout_height="0dp" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toTopOf="@+id/ongoing_call_control_bar"/> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="0dp" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toTopOf="@+id/ongoing_call_control_bar" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent"> + + <include + layout="@layout/conference_call_user_list" + android:id="@+id/conference_profiles"/> + + </LinearLayout> + + <fragment + android:id="@+id/onhold_user_profile" + android:name="com.android.car.dialer.ui.activecall.OnHoldCallUserProfileFragment" + android:layout_width="match_parent" + android:layout_height="@dimen/onhold_user_info_height" + android:layout_marginTop="@dimen/onhold_profile_margin_y" + android:layout_marginStart="@dimen/onhold_profile_margin_x" + android:layout_marginEnd="@dimen/onhold_profile_margin_x" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent"/> + + <fragment + android:id="@+id/ongoing_call_control_bar" + android:name="com.android.car.dialer.ui.activecall.OnGoingCallControllerBarFragment" + android:layout_width="match_parent" + android:layout_height="@dimen/in_call_controller_bar_height" + android:layout_marginStart="@dimen/in_call_controller_bar_margin" + android:layout_marginEnd="@dimen/in_call_controller_bar_margin" + android:layout_marginBottom="@dimen/in_call_controller_bar_margin_bottom" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent"/> + </androidx.constraintlayout.widget.ConstraintLayout> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/res/layout/onhold_user_profile.xml b/res/layout/onhold_user_profile.xml index 22afc237..235a013c 100644 --- a/res/layout/onhold_user_profile.xml +++ b/res/layout/onhold_user_profile.xml @@ -34,8 +34,8 @@ <ImageView android:id="@+id/icon" - android:layout_width="@dimen/avatar_icon_size" - android:layout_height="@dimen/avatar_icon_size" + android:layout_width="@dimen/small_avatar_icon_size" + android:layout_height="@dimen/small_avatar_icon_size" android:scaleType="centerCrop" android:layout_marginStart="@dimen/onhold_profile_avatar_margin" app:layout_constraintTop_toTopOf="parent" @@ -44,37 +44,65 @@ <TextView android:id="@+id/title" - android:layout_width="0dp" + android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/onhold_profile_status_margin" 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_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="@id/guideline" - app:layout_constraintEnd_toStartOf="@+id/swap_call_icon"/> + app:layout_constraintEnd_toStartOf="@+id/time"/> + + <Chronometer + android:id="@id/time" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/onhold_profile_status_margin" + android:textAppearance="?android:attr/textAppearanceLarge" + android:textColor="@color/onhold_time_color" + android:singleLine="true" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toEndOf="@id/title" + app:layout_constraintEnd_toStartOf="@+id/separator"/> <TextView - android:id="@id/text" - android:layout_width="0dp" + android:id="@id/separator" + android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="@string/onhold_call_label" - android:textAppearance="?android:attr/textAppearanceSmall" + android:layout_marginStart="@dimen/onhold_profile_status_margin" + android:layout_marginEnd="@dimen/onhold_profile_status_margin" + android:textAppearance="?android:attr/textAppearanceLarge" android:singleLine="true" - app:layout_constraintTop_toBottomOf="@id/title" + android:text="@string/onhold_call_separator" + app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintStart_toStartOf="@id/guideline" - app:layout_constraintEnd_toStartOf="@+id/swap_call_icon"/> + app:layout_constraintStart_toEndOf="@id/time" + app:layout_constraintEnd_toStartOf="@+id/onhold_label"/> - <ImageView - android:id="@+id/swap_call_icon" + <TextView + android:id="@id/onhold_label" android:layout_width="0dp" - android:layout_height="match_parent" - android:src="@drawable/ic_swap_calls" - android:scaleType="center" - android:tint="@color/secondary_icon_color" - android:paddingLeft="@dimen/swap_call_button_margin" - android:paddingRight="@dimen/swap_call_button_margin" + android:layout_height="wrap_content" + android:textAppearance="?android:attr/textAppearanceLarge" + android:singleLine="true" + android:text="@string/call_state_hold" + android:textColor="@color/onhold_label_color" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toEndOf="@id/separator" + app:layout_constraintEnd_toStartOf="@+id/swap_call"/> + + <TextView + android:id="@id/swap_call" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/onhold_profile_avatar_margin" + android:text="@string/swap_call_label" + android:textAppearance="?android:attr/textAppearanceLarge" + android:singleLine="true" + android:textColor="?android:attr/colorAccent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent"/> diff --git a/res/layout/user_profile_list_item.xml b/res/layout/user_profile_list_item.xml new file mode 100644 index 00000000..d9fbeaf3 --- /dev/null +++ b/res/layout/user_profile_list_item.xml @@ -0,0 +1,51 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2020 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="@dimen/user_profile_list_item_height" + android:gravity="start|center_vertical" + android:orientation="horizontal" + android:paddingStart="@dimen/in_call_user_profile_list_margin" + android:paddingEnd="@dimen/in_call_user_profile_list_margin"> + <ImageView + android:id="@+id/user_profile_avatar" + android:layout_width="@dimen/in_call_avatar_icon_size_small" + android:layout_height="@dimen/in_call_avatar_icon_size_small" + android:scaleType="fitCenter"/> + + <LinearLayout + android:layout_height="wrap_content" + android:layout_width="fill_parent" + android:orientation="vertical" + android:paddingStart="@dimen/in_call_margin_between_avatar_and_text"> + <TextView + android:id="@+id/user_profile_title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textAppearance="@style/TextAppearance.Display3" + android:singleLine="true"/> + <TextView + android:id="@+id/user_profile_phone_number" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textAppearance="@style/TextAppearance.Body3" + android:singleLine="true" + android:layout_marginTop="@dimen/in_call_phone_number_margin_top"/> + + </LinearLayout> +</LinearLayout> diff --git a/res/values-w1280dp-land/dimens.xml b/res/values-w1280dp-land/dimens.xml index cddf4f66..8790808e 100644 --- a/res/values-w1280dp-land/dimens.xml +++ b/res/values-w1280dp-land/dimens.xml @@ -15,5 +15,5 @@ --> <resources> <dimen name="dialpad_info_title_text_size_max">36sp</dimen> - <dimen name="onhold_profile_margin_x">112dp</dimen> + <dimen name="onhold_profile_margin_x">165dp</dimen> </resources> diff --git a/res/values/colors.xml b/res/values/colors.xml index 49d2e522..1ae967e0 100644 --- a/res/values/colors.xml +++ b/res/values/colors.xml @@ -18,7 +18,9 @@ <color name="phone_call">@*android:color/car_green_700</color> <color name="phone_end_call">@*android:color/car_red_500a</color> <color name="onhold_call_background">@*android:color/car_grey_868</color> + <color name="onhold_label_color">#FFA000</color> <color name="audio_output_accent">@*android:color/car_accent</color> + <color name="onhold_time_color">#B8FFFFFF</color> <!-- Dialpad page --> <color name="call_button_outline">@*android:color/car_green_500</color> diff --git a/res/values/dimens.xml b/res/values/dimens.xml index 16d2ce11..858c0651 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -57,17 +57,20 @@ <dimen name="in_call_controller_bar_margin">@*android:dimen/car_padding_5</dimen> <dimen name="in_call_controller_bar_margin_bottom">0dp</dimen> <dimen name="in_call_avatar_icon_size">196dp</dimen> + <dimen name="in_call_avatar_icon_size_small">98dp</dimen> <dimen name="in_call_phone_number_margin_top">@*android:dimen/car_padding_2</dimen> <dimen name="in_call_state_margin_top">@*android:dimen/car_padding_2</dimen> <dimen name="in_call_margin_between_avatar_and_text">48dp</dimen> <dimen name="in_call_user_profile_margin">@*android:dimen/car_margin</dimen> - <dimen name="onhold_user_info_height">@dimen/list_item_height</dimen> + <dimen name="in_call_user_profile_list_margin">20dp</dimen> + <dimen name="onhold_user_info_height">90dp</dimen> <dimen name="onhold_profile_margin_x">@dimen/list_item_padding</dimen> <dimen name="onhold_profile_margin_y">@*android:dimen/car_padding_3</dimen> <dimen name="onhold_profile_corner_radius">8dp</dimen> <dimen name="onhold_profile_avatar_margin">@*android:dimen/car_keyline_1</dimen> <dimen name="onhold_profile_guideline">@dimen/list_item_guideline</dimen> - <dimen name="swap_call_button_margin">@*android:dimen/car_keyline_1</dimen> + <dimen name="onhold_profile_status_margin">6dp</dimen> + <dimen name="user_profile_list_item_height">150dp</dimen> <!-- Ringing call dimensions --> <dimen name="ringing_call_button_touch_target_size">@dimen/touch_target_size</dimen> @@ -134,6 +137,7 @@ <dimen name="vertical_divider_inset">@*android:dimen/car_padding_2</dimen> <dimen name="vertical_divider_width">2dp</dimen> <dimen name="primary_icon_size">@*android:dimen/car_primary_icon_size</dimen> + <dimen name="small_avatar_icon_size">56dp</dimen> <dimen name="avatar_icon_size">76dp</dimen> <dimen name="large_avatar_icon_size">96dp</dimen> <dimen name="primary_icon_enclosing_circle_size">64dp</dimen> diff --git a/res/values/strings.xml b/res/values/strings.xml index a890cd17..936ef2e5 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -179,7 +179,8 @@ <!-- Onhold User Profile Info --> <!-- Text to show the call is onhold [CHAR LIMIT=40]--> - <string name="onhold_call_label">On Hold</string> + <string name="onhold_call_separator">•</string> + <string name="swap_call_label">Switch calls</string> <!-- Contact list headers --> <!-- Contact list label for contact names starting with special characters --> @@ -209,4 +210,10 @@ <!-- Audio route selection dialog. Placeholder resources for overriding --> <string name="audio_route_dialog_title">Output call audio to:</string> <string name="audio_route_dialog_subtitle"></string> + + <!-- Ongoing Conference Call --> + <!-- Title of conference page --> + <string name="ongoing_conf_title">Conference</string> + <string name="ongoing_conf_title_format">%1$s (%2$d) -\u0020</string> + </resources> diff --git a/src/com/android/car/dialer/livedata/CallDetailLiveData.java b/src/com/android/car/dialer/livedata/CallDetailLiveData.java index 4d818016..d0ad0b3d 100644 --- a/src/com/android/car/dialer/livedata/CallDetailLiveData.java +++ b/src/com/android/car/dialer/livedata/CallDetailLiveData.java @@ -19,7 +19,7 @@ package com.android.car.dialer.livedata; import android.telecom.Call; import android.telecom.InCallService; -import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.lifecycle.LiveData; import com.android.car.telephony.common.CallDetail; @@ -31,23 +31,23 @@ import java.util.List; */ public class CallDetailLiveData extends LiveData<CallDetail> { - private final Call mTelecomCall; - - public CallDetailLiveData(@NonNull Call telecomCall) { - mTelecomCall = telecomCall; - } + private Call mTelecomCall; @Override protected void onActive() { super.onActive(); setTelecomCallDetail(mTelecomCall); - mTelecomCall.registerCallback(mCallback); + if (mTelecomCall != null) { + mTelecomCall.registerCallback(mCallback); + } } @Override protected void onInactive() { super.onInactive(); - mTelecomCall.unregisterCallback(mCallback); + if (mTelecomCall != null) { + mTelecomCall.unregisterCallback(mCallback); + } } private Call.Callback mCallback = new Call.Callback() { @@ -88,7 +88,19 @@ public class CallDetailLiveData extends LiveData<CallDetail> { } }; - private void setTelecomCallDetail(Call telecomCall) { - setValue(CallDetail.fromTelecomCallDetail(telecomCall.getDetails())); + /** + * Sets the {@link Call} of which this live data sources. + */ + public void setTelecomCall(Call telecomCall) { + mTelecomCall = telecomCall; + setTelecomCallDetail(mTelecomCall); + if (mTelecomCall != null) { + mTelecomCall.registerCallback(mCallback); + } + } + + private void setTelecomCallDetail(@Nullable Call telecomCall) { + setValue(telecomCall != null ? CallDetail.fromTelecomCallDetail(telecomCall.getDetails()) + : null); } } diff --git a/src/com/android/car/dialer/ui/activecall/ConferenceProfileAdapter.java b/src/com/android/car/dialer/ui/activecall/ConferenceProfileAdapter.java new file mode 100644 index 00000000..224b6eda --- /dev/null +++ b/src/com/android/car/dialer/ui/activecall/ConferenceProfileAdapter.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.car.dialer.ui.activecall; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.android.car.dialer.R; +import com.android.car.telephony.common.CallDetail; + +import java.util.ArrayList; +import java.util.List; + +/** + * Adapter for holding user profiles of a conference. + */ +public class ConferenceProfileAdapter extends RecyclerView.Adapter<ConferenceProfileViewHolder> { + + private Context mContext; + private List<CallDetail> mConferenceList = new ArrayList<>(); + + public ConferenceProfileAdapter(Context context) { + mContext = context; + } + + /** + * Sets {@link #mConferenceList} based on live data. + */ + public void setConferenceList(List<CallDetail> list) { + mConferenceList.clear(); + if (list != null) { + mConferenceList.addAll(list); + } + notifyDataSetChanged(); + } + + @NonNull + @Override + public ConferenceProfileViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) { + View view = LayoutInflater.from(mContext).inflate(R.layout.user_profile_list_item, + viewGroup, false); + return new ConferenceProfileViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ConferenceProfileViewHolder conferenceProfileViewHolder, + int i) { + conferenceProfileViewHolder.bind(mConferenceList.get(i)); + } + + @Override + public int getItemCount() { + return mConferenceList.size(); + } +} diff --git a/src/com/android/car/dialer/ui/activecall/ConferenceProfileViewHolder.java b/src/com/android/car/dialer/ui/activecall/ConferenceProfileViewHolder.java new file mode 100644 index 00000000..7d020aa6 --- /dev/null +++ b/src/com/android/car/dialer/ui/activecall/ConferenceProfileViewHolder.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.car.dialer.ui.activecall; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.text.TextUtils; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.recyclerview.widget.RecyclerView; + +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 com.bumptech.glide.Glide; +import com.bumptech.glide.request.RequestOptions; +import com.bumptech.glide.request.target.SimpleTarget; +import com.bumptech.glide.request.transition.Transition; + +/** + * View holder for a user profile of a conference + */ +public class ConferenceProfileViewHolder extends RecyclerView.ViewHolder { + + private ImageView mAvatar; + private TextView mTitle; + private TextView mNumber; + private Context mContext; + + ConferenceProfileViewHolder(View v) { + super(v); + + mAvatar = v.findViewById(R.id.user_profile_avatar); + mAvatar.setOutlineProvider(ContactAvatarOutputlineProvider.get()); + mTitle = v.findViewById(R.id.user_profile_title); + mNumber = v.findViewById(R.id.user_profile_phone_number); + mContext = v.getContext(); + } + + /** + * Binds call details to the profile views + */ + public void bind(CallDetail callDetail) { + String number = callDetail.getNumber(); + TelecomUtils.getPhoneNumberInfo(mContext, number) + .thenAcceptAsync((info) -> { + if (mContext == null) { + return; + } + + mAvatar.setImageDrawable(TelecomUtils.createLetterTile(mContext, null, null)); + mTitle.setText(info.getDisplayName()); + + String phoneNumberLabel = info.getTypeLabel(); + if (!phoneNumberLabel.isEmpty()) { + phoneNumberLabel += " "; + } + phoneNumberLabel += TelecomUtils.getFormattedNumber(mContext, number); + if (!TextUtils.isEmpty(phoneNumberLabel) + && !phoneNumberLabel.equals(info.getDisplayName())) { + mNumber.setText(phoneNumberLabel); + } else { + mNumber.setText(null); + } + + LetterTileDrawable letterTile = TelecomUtils.createLetterTile( + mContext, info.getInitials(), info.getDisplayName()); + + Glide.with(mContext) + .load(info.getAvatarUri()) + .apply(new RequestOptions().centerCrop().error(letterTile)) + .into(new SimpleTarget<Drawable>() { + @Override + public void onResourceReady(Drawable resource, + Transition<? super Drawable> glideAnimation) { + mAvatar.setImageDrawable(resource); + } + + @Override + public void onLoadFailed(Drawable errorDrawable) { + mAvatar.setImageDrawable(letterTile); + } + }); + + }, mContext.getMainExecutor()); + } +} diff --git a/src/com/android/car/dialer/ui/activecall/InCallActivity.java b/src/com/android/car/dialer/ui/activecall/InCallActivity.java index af438a18..bf22b279 100644 --- a/src/com/android/car/dialer/ui/activecall/InCallActivity.java +++ b/src/com/android/car/dialer/ui/activecall/InCallActivity.java @@ -23,6 +23,7 @@ import android.telecom.Call; import androidx.core.util.Pair; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.FragmentTransaction; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModelProviders; @@ -32,13 +33,13 @@ 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; +import com.android.car.telephony.common.CallDetail; /** Activity for ongoing call and incoming call. */ public class InCallActivity extends FragmentActivity { private static final String TAG = "CD.InCallActivity"; private Fragment mOngoingCallFragment; + private Fragment mOngoingConfCallFragment; private Fragment mIncomingCallFragment; private InCallViewModel mInCallViewModel; @@ -54,14 +55,23 @@ public class InCallActivity extends FragmentActivity { mOngoingCallFragment = getSupportFragmentManager().findFragmentById( R.id.ongoing_call_fragment); + mOngoingConfCallFragment = getSupportFragmentManager().findFragmentById( + R.id.ongoing_conf_call_fragment); mIncomingCallFragment = getSupportFragmentManager().findFragmentById( R.id.incoming_call_fragment); + // Initially hide all fragments to prevent animation flicker + getSupportFragmentManager().beginTransaction() + .hide(mIncomingCallFragment) + .hide(mOngoingCallFragment) + .hide(mOngoingConfCallFragment) + .commit(); + mShowIncomingCall = new MutableLiveData<>(); mInCallViewModel = ViewModelProviders.of(this).get(InCallViewModel.class); mIncomingCallLiveData = LiveDataFunctions.iff(mShowIncomingCall, mInCallViewModel.getIncomingCall()); - LiveDataFunctions.pair(mInCallViewModel.getOngoingCallList(), + LiveDataFunctions.pair(mInCallViewModel.getPrimaryCallDetail(), mIncomingCallLiveData).observe(this, this::updateVisibility); handleIntent(); @@ -85,14 +95,32 @@ public class InCallActivity extends FragmentActivity { handleIntent(); } - private void updateVisibility(Pair<List<Call>, Call> callList) { - if ((callList.first == null || callList.first.isEmpty()) && callList.second == null) { + private void updateVisibility(Pair<CallDetail, Call> callList) { + CallDetail detail = callList.first; + Call incomingCall = callList.second; + + if (detail == null && incomingCall == null) { L.d(TAG, "No call to show. Finish InCallActivity"); finish(); return; } - updateIncomingCallVisibility(callList.second); + FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); + + if (incomingCall == null) { + ft.show(detail.isConference() ? mOngoingConfCallFragment : mOngoingCallFragment) + .hide(detail.isConference() ? mOngoingCallFragment : mOngoingConfCallFragment) + .hide(mIncomingCallFragment); + + mShowIncomingCall.setValue(false); + setIntent(null); + } else { + ft.show(mIncomingCallFragment) + .hide(mOngoingCallFragment) + .hide(mOngoingConfCallFragment); + } + + ft.setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out).commit(); } private void handleIntent() { @@ -107,24 +135,4 @@ public class InCallActivity extends FragmentActivity { mShowIncomingCall.setValue(false); } } - - private void updateIncomingCallVisibility(Call incomingCall) { - if (incomingCall == null) { - getSupportFragmentManager() - .beginTransaction() - .show(mOngoingCallFragment) - .hide(mIncomingCallFragment) - .setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out) - .commit(); - mShowIncomingCall.setValue(false); - setIntent(null); - } else { - getSupportFragmentManager() - .beginTransaction() - .show(mIncomingCallFragment) - .hide(mOngoingCallFragment) - .setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out) - .commit(); - } - } } diff --git a/src/com/android/car/dialer/ui/activecall/InCallFragment.java b/src/com/android/car/dialer/ui/activecall/InCallFragment.java index 0f74255d..42408584 100644 --- a/src/com/android/car/dialer/ui/activecall/InCallFragment.java +++ b/src/com/android/car/dialer/ui/activecall/InCallFragment.java @@ -164,9 +164,8 @@ public abstract class InCallFragment extends Fragment { mUserProfileCallStateText.start(); } else { mUserProfileCallStateText.stop(); - mUserProfileCallStateText.setText( - TelecomUtils.callStateToUiString(getContext(), - callStateAndConnectTime.first)); + mUserProfileCallStateText.setText(TelecomUtils.callStateToUiString(getContext(), + callStateAndConnectTime.first)); } } diff --git a/src/com/android/car/dialer/ui/activecall/InCallViewModel.java b/src/com/android/car/dialer/ui/activecall/InCallViewModel.java index 39210fa2..57bb39e9 100644 --- a/src/com/android/car/dialer/ui/activecall/InCallViewModel.java +++ b/src/com/android/car/dialer/ui/activecall/InCallViewModel.java @@ -59,20 +59,24 @@ public class InCallViewModel extends AndroidViewModel implements private final MutableLiveData<List<Call>> mCallListLiveData; private final MutableLiveData<List<Call>> mOngoingCallListLiveData; + private final MutableLiveData<List<Call>> mConferenceCallListLiveData; + private final LiveData<List<CallDetail>> mConferenceCallDetailListLiveData; private final Comparator<Call> mCallComparator; private final MutableLiveData<Call> mIncomingCallLiveData; - private final LiveData<CallDetail> mCallDetailLiveData; + private final CallDetailLiveData mCallDetailLiveData; private final LiveData<Integer> mCallStateLiveData; private final LiveData<Call> mPrimaryCallLiveData; private final LiveData<Call> mSecondaryCallLiveData; - private final LiveData<CallDetail> mSecondaryCallDetailLiveData; + private final CallDetailLiveData mSecondaryCallDetailLiveData; + private final LiveData<Pair<Call, Call>> mOngoingCallPairLiveData; private final LiveData<Integer> mAudioRouteLiveData; private MutableLiveData<CallAudioState> mCallAudioStateLiveData; private final MutableLiveData<Boolean> mDialpadIsOpen; private final ShowOnholdCallLiveData mShowOnholdCall; private LiveData<Long> mCallConnectTimeLiveData; + private LiveData<Long> mSecondaryCallConnectTimeLiveData; private LiveData<Pair<Integer, Long>> mCallStateAndConnectTimeLiveData; private final Context mContext; @@ -109,12 +113,25 @@ public class InCallViewModel extends AndroidViewModel implements // Sets value to trigger incoming call and active call list to update. mCallListLiveData.setValue(mCallListLiveData.getValue()); } + + @Override + public void onParentChanged(Call call, Call parent) { + L.d(TAG, "onParentChanged %s", call); + updateCallList(); + } + + @Override + public void onChildrenChanged(Call call, List<Call> children) { + L.d(TAG, "onChildrenChanged %s", call); + updateCallList(); + } }; public InCallViewModel(@NonNull Application application) { super(application); mContext = application.getApplicationContext(); + mConferenceCallListLiveData = new MutableLiveData<>(); mIncomingCallLiveData = new MutableLiveData<>(); mOngoingCallListLiveData = new MutableLiveData<>(); mCallAudioStateLiveData = new MutableLiveData<>(); @@ -126,16 +143,37 @@ public class InCallViewModel extends AndroidViewModel implements List<Call> activeCallList = filter(callList, call -> call != null && call.getState() != Call.STATE_RINGING); activeCallList.sort(mCallComparator); - mOngoingCallListLiveData.setValue(activeCallList); + List<Call> conferenceList = filter(activeCallList, + call -> call.getParent() != null); + List<Call> ongoingCallList = filter(activeCallList, + call -> call.getParent() == null); + mConferenceCallListLiveData.setValue(conferenceList); + mOngoingCallListLiveData.setValue(ongoingCallList); mIncomingCallLiveData.setValue(firstMatch(callList, call -> call != null && call.getState() == Call.STATE_RINGING)); + + L.d(TAG, "size:" + activeCallList.size() + " activeList" + activeCallList); + L.d(TAG, "conf:%s" + conferenceList, conferenceList.size()); + L.d(TAG, "ongoing:%s" + ongoingCallList, ongoingCallList.size()); } }; - mPrimaryCallLiveData = Transformations.map(mOngoingCallListLiveData, - input -> input.isEmpty() ? null : input.get(0)); - mCallDetailLiveData = Transformations.switchMap(mPrimaryCallLiveData, - input -> input != null ? new CallDetailLiveData(input) : null); + mConferenceCallDetailListLiveData = Transformations.map(mConferenceCallListLiveData, + callList -> { + List<CallDetail> detailList = new ArrayList<>(); + for (Call call : callList) { + detailList.add(CallDetail.fromTelecomCallDetail(call.getDetails())); + } + return detailList; + }); + + mCallDetailLiveData = new CallDetailLiveData(); + mPrimaryCallLiveData = Transformations.map(mOngoingCallListLiveData, input -> { + Call call = input.isEmpty() ? null : input.get(0); + mCallDetailLiveData.setTelecomCall(call); + return call; + }); + mCallStateLiveData = Transformations.switchMap(mPrimaryCallLiveData, input -> input != null ? new CallStateLiveData(input) : null); mCallConnectTimeLiveData = Transformations.map(mCallDetailLiveData, (details) -> { @@ -147,11 +185,23 @@ public class InCallViewModel extends AndroidViewModel implements mCallStateAndConnectTimeLiveData = LiveDataFunctions.pair(mCallStateLiveData, mCallConnectTimeLiveData); - mSecondaryCallLiveData = Transformations.map(mOngoingCallListLiveData, - callList -> (callList != null && callList.size() > 1) ? callList.get(1) : null); + mSecondaryCallDetailLiveData = new CallDetailLiveData(); + mSecondaryCallLiveData = Transformations.map(mOngoingCallListLiveData, callList -> { + Call call = (callList != null && callList.size() > 1) ? callList.get(1) : null; + mSecondaryCallDetailLiveData.setTelecomCall(call); + return call; + }); + + mSecondaryCallConnectTimeLiveData = Transformations.map(mSecondaryCallDetailLiveData, + details -> { + if (details == null) { + return 0L; + } + return details.getConnectTimeMillis(); + }); - mSecondaryCallDetailLiveData = Transformations.switchMap(mSecondaryCallLiveData, - input -> input != null ? new CallDetailLiveData(input) : null); + mOngoingCallPairLiveData = LiveDataFunctions.pair(mPrimaryCallLiveData, + mSecondaryCallLiveData); mAudioRouteLiveData = new AudioRouteLiveData(mContext); @@ -166,6 +216,22 @@ public class InCallViewModel extends AndroidViewModel implements mContext.bindService(intent, mInCallServiceConnection, Context.BIND_AUTO_CREATE); } + /** Merge primary and secondary calls into a conference */ + public void mergeConference() { + Call call = mPrimaryCallLiveData.getValue(); + Call otherCall = mSecondaryCallLiveData.getValue(); + + if (call == null || otherCall == null) { + return; + } + call.conference(otherCall); + } + + /** Returns the live data which monitors conference calls */ + public LiveData<List<CallDetail>> getConferenceCallDetailList() { + return mConferenceCallDetailListLiveData; + } + /** Returns the live data which monitors all the calls. */ public LiveData<List<Call>> getAllCallList() { return mCallListLiveData; @@ -229,6 +295,20 @@ public class InCallViewModel extends AndroidViewModel implements } /** + * Returns the live data which monitors the secondary call connect time. + */ + public LiveData<Long> getSecondaryCallConnectTime() { + return mSecondaryCallConnectTimeLiveData; + } + + /** + * Returns the live data that monitors the primary and secondary calls. + */ + public LiveData<Pair<Call, Call>> getOngoingCallPair() { + return mOngoingCallPairLiveData; + } + + /** * Returns current audio route. */ public LiveData<Integer> getAudioRoute() { diff --git a/src/com/android/car/dialer/ui/activecall/OnGoingCallControllerBarFragment.java b/src/com/android/car/dialer/ui/activecall/OnGoingCallControllerBarFragment.java index 19fa3c94..f5350f08 100644 --- a/src/com/android/car/dialer/ui/activecall/OnGoingCallControllerBarFragment.java +++ b/src/com/android/car/dialer/ui/activecall/OnGoingCallControllerBarFragment.java @@ -33,6 +33,7 @@ import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; +import androidx.core.util.Pair; import androidx.fragment.app.Fragment; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; @@ -54,7 +55,7 @@ import java.util.List; /** A Fragment of the bar which controls on going call. */ public class OnGoingCallControllerBarFragment extends Fragment { - private static String TAG = "CDialer.OngoingCallCtlFrg"; + private static final String TAG = "CD.OngoingCallCtlFrg"; private static final ImmutableMap<Integer, AudioRouteInfo> AUDIO_ROUTES = ImmutableMap.<Integer, AudioRouteInfo>builder() @@ -76,6 +77,8 @@ public class OnGoingCallControllerBarFragment extends Fragment { R.drawable.ic_audio_route_speaker_activatable)) .build(); + private InCallViewModel mInCallViewModel; + private AlertDialog mAudioRouteSelectionDialog; private List<CarUiListItem> mAudioRouteListItems; private List<Integer> mAvailableRoutes; @@ -84,8 +87,11 @@ public class OnGoingCallControllerBarFragment extends Fragment { private View mAudioRouteView; private ImageView mAudioRouteButton; private TextView mAudioRouteText; + private View mMergeButton; private View mPauseButton; private LiveData<Call> mPrimaryCallLiveData; + private LiveData<List<Call>> mOngoingCallListLiveData; + private LiveData<Pair<Call, Call>> mOngoingCallPairLiveData; private MutableLiveData<Boolean> mDialpadState; private LiveData<List<Call>> mCallListLiveData; private int mPrimaryCallState; @@ -135,18 +141,31 @@ public class OnGoingCallControllerBarFragment extends Fragment { mAudioRouteSelectionDialog = audioRouteSelectionDialogBuilder.create(); - InCallViewModel inCallViewModel = ViewModelProviders.of(getActivity()).get( - InCallViewModel.class); + mInCallViewModel = ViewModelProviders.of(getActivity()).get(InCallViewModel.class); - inCallViewModel.getPrimaryCallState().observe(this, this::setCallState); - mPrimaryCallLiveData = inCallViewModel.getPrimaryCall(); - inCallViewModel.getAudioRoute().observe(this, this::updateViewBasedOnAudioRoute); + mInCallViewModel.getPrimaryCallState().observe(this, this::setCallState); + mPrimaryCallLiveData = mInCallViewModel.getPrimaryCall(); + mOngoingCallPairLiveData = mInCallViewModel.getOngoingCallPair(); - mDialpadState = inCallViewModel.getDialpadOpenState(); - mCallAudioState = inCallViewModel.getCallAudioState(); + mOngoingCallListLiveData = mInCallViewModel.getOngoingCallList(); + mInCallViewModel.getAudioRoute().observe(this, this::updateViewBasedOnAudioRoute); + mDialpadState = mInCallViewModel.getDialpadOpenState(); + mCallAudioState = mInCallViewModel.getCallAudioState(); - mCallListLiveData = inCallViewModel.getAllCallList(); + mCallListLiveData = mInCallViewModel.getAllCallList(); mCallListLiveData.observe(this, v -> updatePauseButtonEnabledState()); + + mOngoingCallPairLiveData.observe(this, pair -> { + boolean isPrimaryCallConference = pair.first != null + && pair.first.getDetails().hasProperty(Call.Details.PROPERTY_CONFERENCE); + if (!isPrimaryCallConference && pair.second != null) { + mPauseButton.setVisibility(View.GONE); + mMergeButton.setVisibility(View.VISIBLE); + } else { + mPauseButton.setVisibility(View.VISIBLE); + mMergeButton.setVisibility(View.GONE); + } + }); } @Nullable @@ -190,6 +209,11 @@ public class OnGoingCallControllerBarFragment extends Fragment { mAudioRouteSelectionDialog.setOnDismissListener( (dialog) -> mAudioRouteView.setActivated(false)); + mMergeButton = fragmentView.findViewById(R.id.merge_button); + mMergeButton.setOnClickListener((v) -> { + mInCallViewModel.mergeConference(); + }); + mPauseButton = fragmentView.findViewById(R.id.pause_button); mPauseButton.setOnClickListener((v) -> { if (mPrimaryCallState == Call.STATE_ACTIVE) { @@ -200,6 +224,7 @@ public class OnGoingCallControllerBarFragment extends Fragment { L.i(TAG, "Pause button is clicked while call in %s state", mPrimaryCallState); } }); + updatePauseButtonEnabledState(); return fragmentView; @@ -242,8 +267,8 @@ public class OnGoingCallControllerBarFragment extends Fragment { } private void updatePauseButtonEnabledState() { - boolean hasOnlyOneCall = mCallListLiveData.getValue() != null - && mCallListLiveData.getValue().size() == 1; + boolean hasOnlyOneCall = mOngoingCallListLiveData.getValue() != null + && mOngoingCallListLiveData.getValue().size() == 1; boolean shouldEnablePauseButton = hasOnlyOneCall && (mPrimaryCallState == Call.STATE_HOLDING || mPrimaryCallState == Call.STATE_ACTIVE); diff --git a/src/com/android/car/dialer/ui/activecall/OnHoldCallUserProfileFragment.java b/src/com/android/car/dialer/ui/activecall/OnHoldCallUserProfileFragment.java index d86f804e..673cf260 100644 --- a/src/com/android/car/dialer/ui/activecall/OnHoldCallUserProfileFragment.java +++ b/src/com/android/car/dialer/ui/activecall/OnHoldCallUserProfileFragment.java @@ -17,10 +17,12 @@ package com.android.car.dialer.ui.activecall; import android.os.Bundle; +import android.os.SystemClock; import android.telecom.Call; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.Chronometer; import android.widget.ImageView; import android.widget.TextView; @@ -47,9 +49,9 @@ public class OnHoldCallUserProfileFragment extends Fragment { private ImageView mAvatarView; private View mSwapCallsView; private LiveData<Call> mPrimaryCallLiveData; - private LiveData<Call> mSecondaryCallLiveData; private CompletableFuture<Void> mPhoneNumberInfoFuture; private LetterTileDrawable mDefaultAvatar; + private Chronometer mTimeTextView; @Override public void onCreate(Bundle savedInstanceState) { @@ -74,23 +76,43 @@ public class OnHoldCallUserProfileFragment extends Fragment { InCallViewModel.class); inCallViewModel.getSecondaryCallDetail().observe(this, this::updateProfile); mPrimaryCallLiveData = inCallViewModel.getPrimaryCall(); - mSecondaryCallLiveData = inCallViewModel.getSecondaryCall(); + + mTimeTextView = fragmentView.findViewById(R.id.time); + inCallViewModel.getSecondaryCallConnectTime().observe(this, this::updateConnectTime); return fragmentView; } + /** Presents the onhold call duration. */ + protected void updateConnectTime(Long connectTime) { + if (connectTime == null) { + mTimeTextView.stop(); + mTimeTextView.setText(""); + return; + } + mTimeTextView.setBase(connectTime + - System.currentTimeMillis() + SystemClock.elapsedRealtime()); + mTimeTextView.start(); + } + private void updateProfile(@Nullable CallDetail callDetail) { if (callDetail == null) { return; } + mAvatarView.setImageDrawable(mDefaultAvatar); + if (mPhoneNumberInfoFuture != null) { mPhoneNumberInfoFuture.cancel(true); } + if (callDetail.isConference()) { + mTitle.setText(getString(R.string.ongoing_conf_title)); + return; + } + String number = callDetail.getNumber(); mTitle.setText(TelecomUtils.getFormattedNumber(getContext(), number)); - mAvatarView.setImageDrawable(mDefaultAvatar); mPhoneNumberInfoFuture = TelecomUtils.getPhoneNumberInfo(getContext(), number) .thenAcceptAsync((info) -> { diff --git a/src/com/android/car/dialer/ui/activecall/OngoingCallFragment.java b/src/com/android/car/dialer/ui/activecall/OngoingCallFragment.java index c0ffaf2d..6318eb5c 100644 --- a/src/com/android/car/dialer/ui/activecall/OngoingCallFragment.java +++ b/src/com/android/car/dialer/ui/activecall/OngoingCallFragment.java @@ -58,7 +58,6 @@ public class OngoingCallFragment extends InCallFragment { inCallViewModel.getCallStateAndConnectTime().observe(this, this::updateCallDescription); mDialpadState = inCallViewModel.getDialpadOpenState(); - mDialpadState.setValue(savedInstanceState == null ? false : !mDialpadFragment.isHidden()); mDialpadState.observe(this, isDialpadOpen -> { if (isDialpadOpen) { onOpenDialpad(); diff --git a/src/com/android/car/dialer/ui/activecall/OngoingConfCallFragment.java b/src/com/android/car/dialer/ui/activecall/OngoingConfCallFragment.java new file mode 100644 index 00000000..663449fc --- /dev/null +++ b/src/com/android/car/dialer/ui/activecall/OngoingConfCallFragment.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.car.dialer.ui.activecall; + +import android.os.Bundle; +import android.os.SystemClock; +import android.telecom.Call; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Chronometer; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.core.util.Pair; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModelProviders; + +import com.android.car.dialer.R; +import com.android.car.telephony.common.TelecomUtils; +import com.android.car.ui.recyclerview.CarUiRecyclerView; + +/** + * A fragment that displays information about an on-going call with options to hang up. + */ +public class OngoingConfCallFragment extends Fragment { + private static final String TAG = "CD.OngoingConfCallFrag"; + + private Fragment mDialpadFragment; + private Fragment mOnholdCallFragment; + private View mConferenceCallProfilesView; + private MutableLiveData<Boolean> mDialpadState; + private CarUiRecyclerView mRecyclerView; + private Chronometer mConferenceTimeTextView; + private TextView mConferenceTitle; + + private ConferenceProfileAdapter mConfProfileAdapter; + + private String mConferenceTitleString; + private String mConfStrTitleFormat; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mConferenceTitleString = getString(R.string.ongoing_conf_title); + mConfStrTitleFormat = getString(R.string.ongoing_conf_title_format); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + View fragmentView = inflater.inflate(R.layout.ongoing_conf_call_fragment, + container, false); + + mOnholdCallFragment = getChildFragmentManager().findFragmentById(R.id.onhold_user_profile); + mDialpadFragment = getChildFragmentManager().findFragmentById(R.id.incall_dialpad_fragment); + mConferenceCallProfilesView = fragmentView.findViewById(R.id.conference_profiles); + mRecyclerView = mConferenceCallProfilesView.findViewById(R.id.recycler_view); + mConferenceTimeTextView = mConferenceCallProfilesView.findViewById(R.id.call_duration); + mConferenceTitle = mConferenceCallProfilesView.findViewById(R.id.conference_title); + + if (mConfProfileAdapter == null) { + mConfProfileAdapter = new ConferenceProfileAdapter(getContext()); + } + mRecyclerView.setAdapter(mConfProfileAdapter); + + InCallViewModel inCallViewModel = ViewModelProviders.of(getActivity()).get( + InCallViewModel.class); + + inCallViewModel.getCallStateAndConnectTime().observe(this, + this::updateCallDescription); + + inCallViewModel.getConferenceCallDetailList().observe(this, list -> { + mConfProfileAdapter.setConferenceList(list); + updateTitle(list.size()); + }); + + mDialpadState = inCallViewModel.getDialpadOpenState(); + mDialpadState.observe(this, isDialpadOpen -> { + if (isDialpadOpen) { + onOpenDialpad(); + } else { + onCloseDialpad(); + } + }); + + inCallViewModel.shouldShowOnholdCall().observe(this, + this::updateOnholdCallFragmentVisibility); + + return fragmentView; + } + + @VisibleForTesting + void onOpenDialpad() { + getChildFragmentManager().beginTransaction() + .show(mDialpadFragment) + .commit(); + mConferenceCallProfilesView.setVisibility(View.GONE); + } + + @VisibleForTesting + void onCloseDialpad() { + getChildFragmentManager().beginTransaction() + .hide(mDialpadFragment) + .commit(); + mConferenceCallProfilesView.setVisibility(View.VISIBLE); + } + + private void updateTitle(int numParticipants) { + String title = String.format(mConfStrTitleFormat, mConferenceTitleString, numParticipants); + mConferenceTitle.setText(title); + } + + /** Presents the call state and call duration. */ + private void updateCallDescription(@Nullable Pair<Integer, Long> callStateAndConnectTime) { + if (callStateAndConnectTime == null || callStateAndConnectTime.first == null) { + mConferenceTimeTextView.stop(); + mConferenceTimeTextView.setText(""); + return; + } + if (callStateAndConnectTime.first == Call.STATE_ACTIVE) { + mConferenceTimeTextView.setBase(callStateAndConnectTime.second + - System.currentTimeMillis() + SystemClock.elapsedRealtime()); + mConferenceTimeTextView.start(); + } else { + mConferenceTimeTextView.stop(); + mConferenceTimeTextView.setText(TelecomUtils.callStateToUiString(getContext(), + callStateAndConnectTime.first)); + } + } + + private void updateOnholdCallFragmentVisibility(Boolean showOnholdCall) { + if (showOnholdCall) { + getChildFragmentManager().beginTransaction().show(mOnholdCallFragment).commit(); + } else { + getChildFragmentManager().beginTransaction().hide(mOnholdCallFragment).commit(); + } + } +} diff --git a/tests/robotests/src/com/android/car/dialer/livedata/CallDetailLiveDataTest.java b/tests/robotests/src/com/android/car/dialer/livedata/CallDetailLiveDataTest.java index 3e3ad7d0..e29251c4 100644 --- a/tests/robotests/src/com/android/car/dialer/livedata/CallDetailLiveDataTest.java +++ b/tests/robotests/src/com/android/car/dialer/livedata/CallDetailLiveDataTest.java @@ -66,7 +66,8 @@ public class CallDetailLiveDataTest { doNothing().when(mMockCall).registerCallback(mCallbackCaptor.capture()); - mCallDetailLiveData = new CallDetailLiveData(mMockCall); + mCallDetailLiveData = new CallDetailLiveData(); + mCallDetailLiveData.setTelecomCall(mMockCall); mLifecycleRegistry = new LifecycleRegistry(mMockLifecycleOwner); when(mMockLifecycleOwner.getLifecycle()).thenReturn(mLifecycleRegistry); } |