summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorUchenna Okoye <uokoye@google.com>2021-09-09 15:34:26 -0700
committerUchenna Okoye <uokoye@google.com>2021-09-09 22:39:21 +0000
commitad864886aa59e239cbd7cb3e4953126c633654a7 (patch)
tree43fd2a49823287e02191e3c31ee4d2c95afa4de0
parent82f8215bc18ee2ab8f301a56dc75eb955d338702 (diff)
downloadMessenger-ad864886aa59e239cbd7cb3e4953126c633654a7.tar.gz
Update to Latest UX Mocks.
PiperOrigin-RevId: 395801506 Change-Id: I32f699c09a359ff10dcc705564c2108e279062a1 Bug: 197576507
-rw-r--r--res/drawable/car_ui_icon_toggle_mute.xml2
-rw-r--r--res/drawable/ic_mute.xml28
-rw-r--r--res/drawable/ic_play.xml17
-rw-r--r--res/drawable/ic_unmute.xml28
-rw-r--r--res/drawable/ui_icon_edit.xml25
-rw-r--r--res/layout/conversation_list_item.xml125
-rw-r--r--res/values/colors.xml2
-rw-r--r--res/values/config.xml1
-rw-r--r--res/values/strings.xml125
-rw-r--r--res/values/styles.xml7
-rw-r--r--src/com/android/car/messenger/core/interfaces/AppFactory.java14
-rw-r--r--src/com/android/car/messenger/core/interfaces/DataModel.java21
-rw-r--r--src/com/android/car/messenger/core/service/MessengerService.java5
-rw-r--r--src/com/android/car/messenger/core/shared/MessageConstants.java6
-rw-r--r--src/com/android/car/messenger/core/shared/NotificationHandler.java22
-rw-r--r--src/com/android/car/messenger/core/ui/conversationlist/ConversationItemAdapter.java2
-rw-r--r--src/com/android/car/messenger/core/ui/conversationlist/ConversationItemViewHolder.java80
-rw-r--r--src/com/android/car/messenger/core/ui/conversationlist/ConversationListFragment.java13
-rw-r--r--src/com/android/car/messenger/core/ui/conversationlist/ConversationListViewModel.java25
-rw-r--r--src/com/android/car/messenger/core/ui/conversationlist/UIConversationItem.java40
-rw-r--r--src/com/android/car/messenger/core/ui/conversationlist/UIConversationItemConverter.java85
-rw-r--r--src/com/android/car/messenger/core/ui/launcher/MessageLauncherActivity.java2
-rw-r--r--src/com/android/car/messenger/core/ui/launcher/MessageLauncherViewModel.java5
-rw-r--r--src/com/android/car/messenger/core/ui/shared/DateTimeView.java537
-rw-r--r--src/com/android/car/messenger/core/util/CarStateListener.java (renamed from src/com/android/car/messenger/impl/common/ProjectionStateListener.java)40
-rw-r--r--src/com/android/car/messenger/core/util/ConversationUtil.java52
-rw-r--r--src/com/android/car/messenger/core/util/VoiceUtil.java21
-rw-r--r--src/com/android/car/messenger/impl/datamodels/ContentProviderLiveData.java4
-rw-r--r--src/com/android/car/messenger/impl/datamodels/ConversationsPerDeviceFetchManager.java9
-rw-r--r--src/com/android/car/messenger/impl/datamodels/NewMessageLiveData.java7
-rw-r--r--src/com/android/car/messenger/impl/datamodels/TelephonyDataModel.java28
-rw-r--r--src/com/android/car/messenger/impl/datamodels/UserAccountLiveData.java7
-rw-r--r--src/com/android/car/messenger/impl/datamodels/util/ConversationFetchUtil.java9
-rw-r--r--src/com/android/car/messenger/impl/datamodels/util/MessageUtils.java8
34 files changed, 1232 insertions, 170 deletions
diff --git a/res/drawable/car_ui_icon_toggle_mute.xml b/res/drawable/car_ui_icon_toggle_mute.xml
index faa8cd5..9b87dc1 100644
--- a/res/drawable/car_ui_icon_toggle_mute.xml
+++ b/res/drawable/car_ui_icon_toggle_mute.xml
@@ -20,7 +20,7 @@
android:viewportHeight="32"
android:viewportWidth="32">
<path
- android:fillColor="@color/car_ui_toolbar_menu_item_icon_color"
+ android:fillColor="@color/secondary_text_color"
android:pathData="M2.295,0L0,2.2788L7.7252,10.004H2.602V19.701H9.0667L17.1475,
27.7818V19.4263L22.5131,24.7919C21.8343,25.1636 21.1394,25.4707
20.3798,25.697V29.0263C21.996,28.6545 23.499,27.9919 24.8566,27.1354L29.7212,32L32,
diff --git a/res/drawable/ic_mute.xml b/res/drawable/ic_mute.xml
new file mode 100644
index 0000000..8e5825e
--- /dev/null
+++ b/res/drawable/ic_mute.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT 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="72dp"
+ android:height="72dp"
+ android:viewportWidth="72"
+ android:viewportHeight="72">
+ <path
+ android:pathData="M16,0L56,0A16,16 0,0 1,72 16L72,56A16,16 0,0 1,56 72L16,72A16,16 0,0 1,0 56L0,16A16,16 0,0 1,16 0z"
+ android:fillColor="#3C4043"/>
+ <path
+ android:pathData="M36,54.3333C38.0259,54.3333 39.6667,52.6924 39.6667,50.6666H32.3334C32.3334,52.6924 33.9742,54.3333 36,54.3333ZM47,43.3333V34.1666C47,28.5291 44.0025,23.8266 38.75,22.5799V21.3333C38.75,19.8116 37.5217,18.5833 36,18.5833C34.4784,18.5833 33.25,19.8116 33.25,21.3333V22.5799C27.9975,23.8266 25,28.5291 25,34.1666V43.3333L21.3334,46.9999V48.8333H50.6667V46.9999L47,43.3333ZM43.3334,45.1666H28.6667V34.1666C28.6667,29.6108 31.4442,25.9166 36,25.9166C40.5559,25.9166 43.3334,29.6108 43.3334,34.1666V45.1666Z"
+ android:fillColor="#BDC1C6"/>
+</vector>
diff --git a/res/drawable/ic_play.xml b/res/drawable/ic_play.xml
index 5e47e70..3c1e820 100644
--- a/res/drawable/ic_play.xml
+++ b/res/drawable/ic_play.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 2020 The Android Open Source Project
+ ~ Copyright (C) 2021 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
@@ -15,11 +15,12 @@
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="11dp"
- android:height="14dp"
- android:viewportHeight="14"
- android:viewportWidth="11">
- <path
- android:fillColor="@color/secondary_text_color"
- android:pathData="M2,3.64L7.27,7L2,10.36V3.64ZM0,0V14L11,7L0,0Z" />
+ android:width="21dp"
+ android:height="26dp"
+ android:viewportWidth="21"
+ android:viewportHeight="26">
+ <path
+ android:fillColor="@color/secondary_text_color"
+ android:pathData="M0.6667,0.1667V25.8334L20.8333,13.0001L0.6667,0.1667Z"
+ />
</vector>
diff --git a/res/drawable/ic_unmute.xml b/res/drawable/ic_unmute.xml
new file mode 100644
index 0000000..af90346
--- /dev/null
+++ b/res/drawable/ic_unmute.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT 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="72dp"
+ android:height="72dp"
+ android:viewportWidth="72"
+ android:viewportHeight="72">
+ <path
+ android:pathData="M16,0L56,0A16,16 0,0 1,72 16L72,56A16,16 0,0 1,56 72L16,72A16,16 0,0 1,0 56L0,16A16,16 0,0 1,16 0z"
+ android:fillColor="#66B5FF"/>
+ <path
+ android:pathData="M50.6667,48.2741L28.3733,25.2749L23.6708,20.4258L21.3333,22.7541L26.4667,27.8874L26.4758,27.8966C25.5133,29.7116 25,31.8566 25,34.1666V43.3333L21.3333,46.9999V48.8333H46.505L50.1717,52.4999L52.5,50.1624L50.6667,48.2741ZM36,54.3333C38.0258,54.3333 39.6667,52.6924 39.6667,50.6666H32.3333C32.3333,52.6924 33.9742,54.3333 36,54.3333ZM47,40.9133V34.1666C47,28.5291 44.0025,23.8266 38.75,22.5799V21.3333C38.75,19.8116 37.5217,18.5833 36,18.5833C34.4783,18.5833 33.25,19.8116 33.25,21.3333V22.5799C32.9842,22.6441 32.7275,22.7174 32.4708,22.7999C32.2783,22.8641 32.095,22.9283 31.9117,23.0016C31.9117,23.0016 31.9025,23.0016 31.9025,23.0108C31.8933,23.0108 31.8842,23.0199 31.875,23.0199C31.4533,23.1849 31.0408,23.3774 30.6375,23.5883C30.6283,23.5883 30.6192,23.5974 30.61,23.5974L47,40.9133Z"
+ android:fillColor="#000000"/>
+</vector>
diff --git a/res/drawable/ui_icon_edit.xml b/res/drawable/ui_icon_edit.xml
new file mode 100644
index 0000000..aee80ca
--- /dev/null
+++ b/res/drawable/ui_icon_edit.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="?android:attr/colorPrimaryDark"
+ android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
+</vector>
diff --git a/res/layout/conversation_list_item.xml b/res/layout/conversation_list_item.xml
index 248184f..1240337 100644
--- a/res/layout/conversation_list_item.xml
+++ b/res/layout/conversation_list_item.xml
@@ -28,7 +28,7 @@ limitations under the License.
android:layout_marginEnd="@dimen/unread_icon_marginEnd"
android:contentDescription="@string/cd_unread"
android:scaleType="centerCrop"
- android:src="@color/unread_dot_color"
+ android:src="@color/unread_color"
app:layout_constraintBottom_toBottomOf="@id/icon"
app:layout_constraintEnd_toStartOf="@id/icon"
app:layout_constraintTop_toTopOf="@id/icon" />
@@ -46,18 +46,6 @@ limitations under the License.
tools:src="@color/car_red_500a" />
<ImageView
- android:id="@+id/last_action_icon_view"
- android:layout_width="@dimen/subtitle_icon_width"
- android:layout_height="0dp"
- android:contentDescription="@string/cd_icon_indicating_the_last_action"
- android:scaleType="centerInside"
- android:src="@drawable/car_ui_icon_reply"
- app:layout_constraintBottom_toBottomOf="@id/time_text"
- app:layout_constraintStart_toStartOf="@id/guideline_begin"
- app:layout_constraintTop_toBottomOf="@id/title"
- app:layout_constraintTop_toTopOf="@id/time_text" />
-
- <ImageView
android:id="@+id/reply_action_button"
android:layout_width="0dp"
android:layout_height="match_parent"
@@ -68,6 +56,22 @@ limitations under the License.
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/mute_action_button"
app:layout_constraintStart_toEndOf="@id/guideline_end"
+ android:visibility="visible"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <ImageView
+ android:id="@+id/play_action_button"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:background="?android:attr/selectableItemBackground"
+ android:contentDescription="@string/cd_play_action_button"
+ android:scaleX=".4"
+ android:scaleY=".4"
+ android:src="@drawable/ic_play"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/mute_action_button"
+ app:layout_constraintStart_toEndOf="@id/guideline_end"
+ tools:visibility="gone"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
@@ -80,7 +84,7 @@ limitations under the License.
android:src="@drawable/car_ui_icon_toggle_mute"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toEndOf="@id/reply_action_button"
+ app:layout_constraintStart_toEndOf="@id/guideline_mid"
app:layout_constraintTop_toTopOf="parent" />
<TextView
@@ -89,48 +93,102 @@ limitations under the License.
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/message_history_text_margin_end"
android:singleLine="true"
+ tools:text="Ashley Bae"
+ app:layout_constraintVertical_chainStyle="packed"
android:theme="@style/Theme.Messaging.BidiText"
- app:layout_constraintBottom_toTopOf="@+id/text"
+ app:layout_constraintBottom_toTopOf="@+id/preview"
app:layout_constraintEnd_toEndOf="@id/guideline_end"
app:layout_constraintStart_toStartOf="@id/guideline_begin"
app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintVertical_chainStyle="packed" />
+ app:layout_constraintBottom_toBottomOf="parent" />
+
+ <ImageView
+ android:id="@+id/last_action_icon_view"
+ android:layout_width="@dimen/subtitle_icon_width"
+ android:layout_height="0dp"
+ android:contentDescription="@string/cd_icon_indicating_the_last_action"
+ android:scaleType="centerInside"
+ android:src="@drawable/car_ui_icon_reply"
+ android:visibility="gone"
+ app:layout_constraintHorizontal_chainStyle="packed"
+ android:layout_marginEnd="@dimen/message_history_icons_margin"
+ app:layout_constraintBottom_toBottomOf="@id/preview"
+ app:layout_constraintStart_toStartOf="@id/title"
+ app:layout_constraintTop_toBottomOf="@id/title"
+ app:layout_constraintEnd_toStartOf="@id/preview"
+ app:layout_constraintTop_toTopOf="@id/preview" />
<TextView
- android:id="@+id/time_text"
+ android:id="@+id/preview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginStart="@dimen/message_history_icons_margin"
android:singleLine="true"
+ app:layout_constraintVertical_bias="0.0"
+ app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/last_action_icon_view"
app:layout_constraintTop_toBottomOf="@id/title"
- tools:text="14:02 PM" />
+ app:layout_constraintEnd_toStartOf="@id/preview_dot"
+ tools:visibility="gone"
+ android:ellipsize="end"
+ android:maxLength="20"
+ tools:text="Let this be the preview. Lots of preview with
+ a whole lot of various texts, one that is quite long in every way.
+ To verify that it still fits in the end" />
<TextView
- android:id="@+id/dot"
+ android:id="@+id/preview_dot"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/message_history_icons_margin"
+ android:layout_marginEnd="@dimen/message_history_icons_margin"
+ app:layout_goneMarginStart="0dp"
+ app:layout_goneMarginEnd="0dp"
android:singleLine="true"
android:text="@string/dot"
- android:visibility="gone"
- app:layout_constraintBottom_toBottomOf="@id/text"
- app:layout_constraintStart_toEndOf="@id/time_text"
- app:layout_constraintTop_toTopOf="@id/text"
- tools:visibility="visible" />
+ app:layout_constraintEnd_toStartOf="@id/text_metadata"
+ app:layout_constraintBottom_toBottomOf="@id/preview"
+ app:layout_constraintStart_toEndOf="@id/preview"
+ app:layout_constraintTop_toTopOf="@id/preview"
+ tools:visibility="gone" />
+
+ <TextView
+ android:id="@+id/text_metadata"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:maxLength="20"
+ app:layout_constraintStart_toEndOf="@id/preview_dot"
+ app:layout_constraintTop_toBottomOf="@id/title"
+ app:layout_constraintEnd_toStartOf="@id/text_metadata_dot"
+ tools:visibility="visible"
+ tools:text="2 more messages" />
<TextView
- android:id="@id/text"
+ android:id="@+id/text_metadata_dot"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:text="@string/dot"
+ android:visibility="visible"
+ android:layout_marginStart="6dp"
+ tools:visibility="visible"
+ app:layout_constraintBottom_toBottomOf="@id/text_metadata"
+ app:layout_constraintStart_toEndOf="@id/text_metadata"
+ app:layout_constraintTop_toTopOf="@id/text_metadata"
+ app:layout_constraintEnd_toStartOf="@id/date_time_view" />
+
+ <com.android.car.messenger.core.ui.shared.DateTimeView
+ android:id="@+id/date_time_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/message_history_text_margin_end"
- android:layout_marginStart="@dimen/message_history_icons_margin"
+ android:layout_marginStart="6dp"
android:singleLine="true"
- app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintBottom_toBottomOf="@id/text_metadata"
app:layout_constraintEnd_toEndOf="@id/guideline_end"
- app:layout_constraintStart_toEndOf="@id/dot"
+ app:layout_constraintStart_toEndOf="@id/text_metadata_dot"
app:layout_constraintTop_toBottomOf="@id/title"
- tools:text="Replied" />
+ tools:text="6 min" />
<View
android:id="@+id/play_action_touch_view"
@@ -166,4 +224,11 @@ limitations under the License.
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_end="200dp" />
+
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/guideline_mid"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:layout_constraintGuide_end="100dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 17d5cb2..1d8527e 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -34,7 +34,7 @@
<color name="divider_color_light">#38FFFFFF</color>
<color name="primary_icon_color">@color/icon_tint</color>
<color name="letter_tile_default_color">#cccccc</color>
- <color name="unread_dot_color">#66B5FF</color>
+ <color name="unread_color">#66B5FF</color>
<color name="letter_tile_font_color">#ffffff</color>
diff --git a/res/values/config.xml b/res/values/config.xml
index db2d231..849ef92 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -17,6 +17,7 @@
<resources>
<bool name="group_avatar_fill_background">false</bool>
<bool name="direct_send_supported">true</bool>
+ <bool name="direct_reply_supported">true</bool>
<bool name="ttr_conversation_supported">false</bool>
<!--
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 837ad3e..0b40bb7 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -15,13 +15,28 @@
~ limitations under the License.
-->
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <!-- Application name [CHAR LIMIT=30] -->
+ <!-- Phrase describing that there is a new message [CHAR LIMIT=20] -->
<plurals name="new_message">
- <item quantity="one" translatable="false">New message</item>
- <item quantity="other" translatable="false">
+ <item quantity="one">New message</item>
+ <item quantity="other">
<xliff:g example="2" id="count">%d</xliff:g> messages</item>
</plurals>
+ <!-- Phrase describing the number of messages [CHAR LIMIT=20] -->
+ <plurals name="no_of_message">
+ <item quantity="zero">No message</item>
+ <item quantity="one">1 message</item>
+ <item quantity="other">
+ <xliff:g example="2" id="count">%d</xliff:g> messages</item>
+ </plurals>
+
+ <!-- Phrase describing a case where there are more messages other than the one shown the user. [CHAR LIMIT=30] -->
+ <plurals name="more_message">
+ <item quantity="one">One more message</item>
+ <item quantity="other">
+ <xliff:g example="2" id="count">%d</xliff:g> more messages</item>
+ </plurals>
+
<!-- Button text for when disconnected from Bluetooth [CHAR LIMIT=40] -->
<string name="app_name" translatable="false">Car Messenger</string>
@@ -32,7 +47,7 @@
<string name="connect_bluetooth_button_text" translatable="false">Connect to Bluetooth</string>
<!-- Status when replied [CHAR LIMIT=40] -->
- <string name="no_new_messages" translatable="false">No new messages</string>
+ <string name="no_messages" translatable="false">No messages</string>
<!-- Dot separator [CHAR LIMIT=1] -->
<string name="replied" translatable="false">Replied</string>
@@ -67,4 +82,106 @@
<string name="cd_loading_info_icon">Loading Info Icon</string>
<!-- Mute button [CHAR LIMIT=40] -->
<string name="cd_reply_action_button">Reply Action Button</string>
+
+ <!-- Mute button [CHAR LIMIT=40] -->
+ <string name="cd_play_action_button">Play Action Button</string>
+
+ <!-- A string denoting the current point in time that should be as short as possible. Abbreviations are preferred to full strings as this might be shown repetitively. It is used in the header of notifications. [CHAR LIMIT=8]-->
+ <string name="now_string_shortest">now</string>
+
+ <!-- Phrase describing a time duration using minutes that is as short as possible, preferrably one character. If the language needs a space in between the integer and the unit, please also integrate it in the string, but preferably it should not have a space in between.[CHAR LIMIT=6] -->
+ <plurals name="duration_minutes_shortest">
+ <item quantity="one"><xliff:g example="1" id="count" translatable="false">%d</xliff:g>m</item>
+ <item quantity="other"><xliff:g example="2" id="count" translatable="false">%d</xliff:g>m</item>
+ </plurals>
+
+ <!-- Phrase describing a time duration using hours that is as short as possible, preferrably one character. If the language needs a space in between the integer and the unit, please also integrate it in the string, but preferably it should not have a space in between.[CHAR LIMIT=6] -->
+ <plurals name="duration_hours_shortest">
+ <item quantity="one"><xliff:g example="1" id="count">%d</xliff:g>h</item>
+ <item quantity="other"><xliff:g example="2" id="count">%d</xliff:g>h</item>
+ </plurals>
+
+ <!-- Phrase describing a time duration using days that is as short as possible, preferrably one character. If the language needs a space in between the integer and the unit, please also integrate it in the string, but preferably it should not have a space in between.[CHAR LIMIT=6] -->
+ <plurals name="duration_days_shortest">
+ <item quantity="one"><xliff:g example="1" id="count">%d</xliff:g>d</item>
+ <item quantity="other"><xliff:g example="2" id="count">%d</xliff:g>d</item>
+ </plurals>
+
+ <!-- Phrase describing a time duration using years that is as short as possible, preferrably one character. If the language needs a space in between the integer and the unit, please also integrate it in the string, but preferably it should not have a space in between.[CHAR LIMIT=6] -->
+ <plurals name="duration_years_shortest">
+ <item quantity="one"><xliff:g example="1" id="count">%d</xliff:g>y</item>
+ <item quantity="other"><xliff:g example="2" id="count">%d</xliff:g>y</item>
+ </plurals>
+
+ <!-- Phrase describing a time duration using minutes that is as short as possible, preferrably one character. This version should be a future point in time. If the language needs a space in between the integer and the unit, please also integrate it in the string, but preferably it should not have a space in between.[CHAR LIMIT=14] -->
+ <plurals name="duration_minutes_shortest_future">
+ <item quantity="one">in <xliff:g example="1" id="count">%d</xliff:g>m</item>
+ <item quantity="other">in <xliff:g example="2" id="count">%d</xliff:g>m</item>
+ </plurals>
+
+ <!-- Phrase describing a time duration using hours that is as short as possible, preferrably one character. This version should be a future point in time. If the language needs a space in between the integer and the unit, please also integrate it in the string, but preferably it should not have a space in between.[CHAR LIMIT=14] -->
+ <plurals name="duration_hours_shortest_future">
+ <item quantity="one">in <xliff:g example="1" id="count">%d</xliff:g>h</item>
+ <item quantity="other">in <xliff:g example="2" id="count">%d</xliff:g>h</item>
+ </plurals>
+
+ <!-- Phrase describing a time duration using days that is as short as possible, preferrably one character. This version should be a future point in time. If the language needs a space in between the integer and the unit, please also integrate it in the string, but preferably it should not have a space in between.[CHAR LIMIT=14] -->
+ <plurals name="duration_days_shortest_future">
+ <item quantity="one">in <xliff:g example="1" id="count">%d</xliff:g>d</item>
+ <item quantity="other">in <xliff:g example="2" id="count">%d</xliff:g>d</item>
+ </plurals>
+
+ <!-- Phrase describing a time duration using years that is as short as possible, preferrably one character. This version should be a future point in time. If the language needs a space in between the integer and the unit, please also integrate it in the string, but preferably it should not have a space in between.[CHAR LIMIT=14] -->
+ <plurals name="duration_years_shortest_future">
+ <item quantity="one">in <xliff:g example="1" id="count">%d</xliff:g>y</item>
+ <item quantity="other">in <xliff:g example="2" id="count">%d</xliff:g>y</item>
+ </plurals>
+
+ <!-- Phrase describing a relative time using minutes in the past that is not shown on the screen but used for accessibility. [CHAR LIMIT=40] -->
+ <plurals name="duration_minutes_relative">
+ <item quantity="one"><xliff:g example="1" id="count">%d</xliff:g> minute ago</item>
+ <item quantity="other"><xliff:g example="2" id="count">%d</xliff:g> minutes ago</item>
+ </plurals>
+
+ <!-- Phrase describing a relative time using hours in the past that is not shown on the screen but used for accessibility. [CHAR LIMIT=40] -->
+ <plurals name="duration_hours_relative">
+ <item quantity="one"><xliff:g example="1" id="count">%d</xliff:g> hour ago</item>
+ <item quantity="other"><xliff:g example="2" id="count">%d</xliff:g> hours ago</item>
+ </plurals>
+
+ <!-- Phrase describing a relative time using days in the past that is not shown on the screen but used for accessibility. [CHAR LIMIT=40] -->
+ <plurals name="duration_days_relative">
+ <item quantity="one"><xliff:g example="1" id="count">%d</xliff:g> day ago</item>
+ <item quantity="other"><xliff:g example="2" id="count">%d</xliff:g> days ago</item>
+ </plurals>
+
+ <!-- Phrase describing a relative time using years in the past that is not shown on the screen but used for accessibility. [CHAR LIMIT=40] -->
+ <plurals name="duration_years_relative">
+ <item quantity="one"><xliff:g example="1" id="count">%d</xliff:g> year ago</item>
+ <item quantity="other"><xliff:g example="2" id="count">%d</xliff:g> years ago</item>
+ </plurals>
+
+ <!-- Phrase describing a relative time using minutes that is not shown on the screen but used for accessibility. This version should be a future point in time. [CHAR LIMIT=NONE] -->
+ <plurals name="duration_minutes_relative_future">
+ <item quantity="one">in <xliff:g example="1" id="count">%d</xliff:g> minute</item>
+ <item quantity="other">in <xliff:g example="2" id="count">%d</xliff:g> minutes</item>
+ </plurals>
+
+ <!-- Phrase describing a relative time using hours that is not shown on the screen but used for accessibility. This version should be a future point in time. [CHAR LIMIT=NONE] -->
+ <plurals name="duration_hours_relative_future">
+ <item quantity="one">in <xliff:g example="1" id="count">%d</xliff:g> hour</item>
+ <item quantity="other">in <xliff:g example="2" id="count">%d</xliff:g> hours</item>
+ </plurals>
+
+ <!-- Phrase describing a relative time using days that is not shown on the screen but used for accessibility. This version should be a future point in time. [CHAR LIMIT=NONE] -->
+ <plurals name="duration_days_relative_future">
+ <item quantity="one">in <xliff:g example="1" id="count">%d</xliff:g> day</item>
+ <item quantity="other">in <xliff:g example="2" id="count">%d</xliff:g> days</item>
+ </plurals>
+
+ <!-- Phrase describing a relative time using years that is not shown on the screen but used for accessibility. This version should be a future point in time. [CHAR LIMIT=NONE] -->
+ <plurals name="duration_years_relative_future">
+ <item quantity="one">in <xliff:g example="1" id="count">%d</xliff:g> year</item>
+ <item quantity="other">in <xliff:g example="2" id="count">%d</xliff:g> years</item>
+ </plurals>
</resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 2568a05..0e8c798 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -17,7 +17,7 @@
<!-- Message history -->
<style name="TextAppearance.MessageHistoryTitle" parent="TextAppearance.Body1" />
- <style name="TextAppearance.MessageHistorySubtitle" parent="TextAppearance.Body3">
+ <style name="TextAppearance.MessageHistoryTextPreview" parent="TextAppearance.Body3">
<item name="android:textColor">@color/secondary_text_color</item>
</style>
<!-- Customized text color for unread messages can be added here -->
@@ -26,9 +26,10 @@
<item name="android:textStyle">bold</item>
</style>
- <style name="TextAppearance.MessageHistoryUnreadSubtitle"
- parent="TextAppearance.MessageHistorySubtitle">
+ <style name="TextAppearance.MessageHistoryUnreadMetadata"
+ parent="TextAppearance.MessageHistoryTextPreview">
<item name="android:textStyle">bold</item>
+ <item name="android:textColor">@color/unread_color</item>
</style>
<style name="Widget.Button" parent="android:Widget.DeviceDefault.Button">
diff --git a/src/com/android/car/messenger/core/interfaces/AppFactory.java b/src/com/android/car/messenger/core/interfaces/AppFactory.java
index a88078d..683098b 100644
--- a/src/com/android/car/messenger/core/interfaces/AppFactory.java
+++ b/src/com/android/car/messenger/core/interfaces/AppFactory.java
@@ -20,6 +20,9 @@ import android.content.Context;
import android.content.SharedPreferences;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.car.messenger.core.util.CarStateListener;
/**
* The AppFactory provides singleton instances to be used throughout the app.
@@ -34,6 +37,9 @@ public abstract class AppFactory {
protected static boolean sRegistered;
protected static boolean sInitialized;
+ // Context is required to initialize
+ @Nullable protected CarStateListener mCarStateListener;
+
/** Returns the Factory instance for the Application. */
@NonNull
public static AppFactory get() {
@@ -53,6 +59,14 @@ public abstract class AppFactory {
sInstance = factory;
}
+ /** Gets the Car State Listener */
+ public final CarStateListener getCarStateListener() {
+ if (mCarStateListener == null) {
+ mCarStateListener = new CarStateListener(AppFactory.get().getContext());
+ }
+ return mCarStateListener;
+ }
+
/** Returns context most appropriate for UI context-requiring tasks. */
@NonNull
public abstract Context getContext();
diff --git a/src/com/android/car/messenger/core/interfaces/DataModel.java b/src/com/android/car/messenger/core/interfaces/DataModel.java
index 0ef31d3..93a728f 100644
--- a/src/com/android/car/messenger/core/interfaces/DataModel.java
+++ b/src/com/android/car/messenger/core/interfaces/DataModel.java
@@ -18,10 +18,8 @@ package com.android.car.messenger.core.interfaces;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
-
import com.android.car.messenger.common.Conversation;
import com.android.car.messenger.core.models.UserAccount;
-
import java.util.Collection;
/**
@@ -96,4 +94,23 @@ public interface DataModel {
*/
void replyConversation(
@NonNull int accountId, @NonNull String conversationId, @NonNull String message);
+
+ /**
+ * Called by UI to send a message to a phone number on a device
+ *
+ * @param accountId The user account/device id to send the message from
+ * @param phoneNumber The desired phone number to send message to
+ * @param message The desired message to send to conversation thread
+ */
+ void sendMessage(int accountId, @NonNull String phoneNumber, @NonNull String message);
+
+ /**
+ * Called by UI to send a message to a phone number on a device
+ *
+ * @param iccId The {@link UserAccount#getIccId()} belonging to the device/user account to send
+ * the message from
+ * @param phoneNumber The desired phone number to send message to
+ * @param message The desired message to send to conversation thread
+ */
+ void sendMessage(@NonNull String iccId, @NonNull String phoneNumber, @NonNull String message);
}
diff --git a/src/com/android/car/messenger/core/service/MessengerService.java b/src/com/android/car/messenger/core/service/MessengerService.java
index 5c53487..aef64d6 100644
--- a/src/com/android/car/messenger/core/service/MessengerService.java
+++ b/src/com/android/car/messenger/core/service/MessengerService.java
@@ -31,14 +31,13 @@ import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.provider.Settings;
+import androidx.core.app.NotificationCompat;
import android.telephony.TelephonyManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.core.app.NotificationCompat;
import com.android.car.messenger.R;
-import com.android.car.messenger.common.MessagingUtils;
import com.android.car.messenger.core.interfaces.AppFactory;
import com.android.car.messenger.core.interfaces.DataModel;
import com.android.car.messenger.core.shared.NotificationHandler;
@@ -180,7 +179,7 @@ public class MessengerService extends Service {
VoiceUtil.markAsRead(intent);
break;
case ACTION_DIRECT_SEND:
- MessagingUtils.directSend(this, intent);
+ VoiceUtil.directSend(intent);
break;
case TelephonyManager.ACTION_RESPOND_VIA_MESSAGE:
// Not currently supported. This was added to allow CarMessenger become the default
diff --git a/src/com/android/car/messenger/core/shared/MessageConstants.java b/src/com/android/car/messenger/core/shared/MessageConstants.java
index 5ba2779..40c34e8 100644
--- a/src/com/android/car/messenger/core/shared/MessageConstants.java
+++ b/src/com/android/car/messenger/core/shared/MessageConstants.java
@@ -37,6 +37,12 @@ public final class MessageConstants {
*/
@NonNull public static final String LAST_REPLY_TIMESTAMP_EXTRA = "LAST_REPLY_TIMESTAMP_EXTRA";
+ /**
+ * This is added as an extra in the {@link com.android.car.messenger.common.Conversation} to
+ * indicate what the last reply is, if any
+ */
+ @NonNull public static final String LAST_REPLY_TEXT_EXTRA = "LAST_REPLY_TEXT_EXTRA";
+
/** Used to reply to message. */
@NonNull public static final String ACTION_REPLY = "ACTION_REPLY";
diff --git a/src/com/android/car/messenger/core/shared/NotificationHandler.java b/src/com/android/car/messenger/core/shared/NotificationHandler.java
index 0087c58..f9836ec 100644
--- a/src/com/android/car/messenger/core/shared/NotificationHandler.java
+++ b/src/com/android/car/messenger/core/shared/NotificationHandler.java
@@ -20,7 +20,9 @@ import static com.android.car.messenger.core.shared.MessageConstants.EXTRA_ACCOU
import android.app.Notification;
import android.app.NotificationManager;
+import android.app.PendingIntent;
import android.content.Context;
+import android.content.Intent;
import android.service.notification.StatusBarNotification;
import androidx.annotation.NonNull;
@@ -31,6 +33,7 @@ import com.android.car.messenger.R;
import com.android.car.messenger.common.Conversation;
import com.android.car.messenger.core.interfaces.AppFactory;
import com.android.car.messenger.core.service.MessengerService;
+import com.android.car.messenger.core.ui.launcher.MessageLauncherActivity;
import com.android.car.messenger.core.util.L;
import com.android.car.messenger.core.util.VoiceUtil;
@@ -70,10 +73,29 @@ public class NotificationHandler {
Notification notification =
ConversationPayloadHandler.createNotificationFromConversation(
context, channelId, tapToReadConversation, R.drawable.ic_message, null);
+ notification.contentIntent = createServiceIntent();
notificationManager.notify(tapToReadConversation.getId().hashCode(), notification);
}
+ private static PendingIntent createServiceIntent() {
+ Context context = AppFactory.get().getContext();
+
+ Intent intent =
+ new Intent(context, MessageLauncherActivity.class)
+ .addFlags(
+ Intent.FLAG_ACTIVITY_NEW_TASK
+ | Intent.FLAG_ACTIVITY_CLEAR_TOP
+ | Intent.FLAG_ACTIVITY_SINGLE_TOP)
+ .setClass(context, MessengerService.class);
+
+ return PendingIntent.getActivity(
+ context,
+ /* requestCode= */ 0,
+ intent,
+ PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
+ }
+
/**
* Posts a notification in the foreground for Tap To Read
*
diff --git a/src/com/android/car/messenger/core/ui/conversationlist/ConversationItemAdapter.java b/src/com/android/car/messenger/core/ui/conversationlist/ConversationItemAdapter.java
index b54c82c..eeb6786 100644
--- a/src/com/android/car/messenger/core/ui/conversationlist/ConversationItemAdapter.java
+++ b/src/com/android/car/messenger/core/ui/conversationlist/ConversationItemAdapter.java
@@ -38,6 +38,8 @@ public class ConversationItemAdapter extends RecyclerView.Adapter<ConversationIt
void onConversationItemClicked(@NonNull Conversation conversation);
/** Callback to start tap to reply voice interaction for conversation item */
void onReplyIconClicked(@NonNull Conversation conversation);
+ /** Callback to start tap to read voice interaction for conversation item */
+ void onPlayIconClicked(@NonNull Conversation conversation);
}
@NonNull private final List<UIConversationItem> mUIConversationItems = new ArrayList<>();
diff --git a/src/com/android/car/messenger/core/ui/conversationlist/ConversationItemViewHolder.java b/src/com/android/car/messenger/core/ui/conversationlist/ConversationItemViewHolder.java
index 67900c0..cb7f324 100644
--- a/src/com/android/car/messenger/core/ui/conversationlist/ConversationItemViewHolder.java
+++ b/src/com/android/car/messenger/core/ui/conversationlist/ConversationItemViewHolder.java
@@ -16,15 +16,12 @@
package com.android.car.messenger.core.ui.conversationlist;
-import android.graphics.Color;
-import android.graphics.PorterDuff;
-import android.graphics.PorterDuffColorFilter;
+import android.graphics.drawable.Drawable;
import androidx.recyclerview.widget.RecyclerView;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
-import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import com.android.car.messenger.R;
@@ -33,6 +30,7 @@ import com.android.car.messenger.core.interfaces.DataModel;
import com.android.car.messenger.core.shared.NotificationHandler;
import com.android.car.messenger.core.ui.conversationlist.ConversationItemAdapter.OnConversationItemClickListener;
import com.android.car.messenger.core.ui.shared.CircularOutputlineProvider;
+import com.android.car.messenger.core.ui.shared.DateTimeView;
import com.android.car.messenger.core.ui.shared.ViewUtils;
/**
@@ -49,13 +47,16 @@ public class ConversationItemViewHolder extends RecyclerView.ViewHolder {
@NonNull private final View mPlayMessageTouchView;
@NonNull private final ImageView mAvatarView;
@NonNull private final TextView mTitleView;
- @NonNull private final TextView mTimeTextView;
- @NonNull private final TextView mTextView;
+ @NonNull private final TextView mPreviewTextView;
+ @NonNull private final TextView mTextMetadataView;
@NonNull private final TextView mDotSeparatorView;
+ @NonNull private final TextView mTextMetadataDotView;
@NonNull private final ImageView mSubtitleIconView;
@NonNull private final ImageView mMuteActionButton;
@NonNull private final View mReplyActionButton;
+ @NonNull private final View mPlayActionButton;
@NonNull private final View mUnreadIconIndicator;
+ @NonNull private final DateTimeView mDateTimeView;
@NonNull private final View mDivider;
/** Conversation Item View Holder constructor */
@@ -67,13 +68,17 @@ public class ConversationItemViewHolder extends RecyclerView.ViewHolder {
mPlayMessageTouchView = itemView.findViewById(R.id.play_action_touch_view);
mAvatarView = itemView.findViewById(R.id.icon);
mTitleView = itemView.findViewById(R.id.title);
- mTimeTextView = itemView.findViewById(R.id.time_text);
- mTextView = itemView.findViewById(R.id.text);
- mDotSeparatorView = itemView.findViewById(R.id.dot);
+ mPreviewTextView = itemView.findViewById(R.id.preview);
+ mTextMetadataView = itemView.findViewById(R.id.text_metadata);
+ mTextMetadataDotView = itemView.findViewById(R.id.text_metadata_dot);
+ mDateTimeView = itemView.findViewById(R.id.date_time_view);
+ mDateTimeView.setShowRelativeTime(true);
+ mDotSeparatorView = itemView.findViewById(R.id.preview_dot);
mUnreadIconIndicator = itemView.findViewById(R.id.unread_indicator);
mSubtitleIconView = itemView.findViewById(R.id.last_action_icon_view);
mMuteActionButton = itemView.findViewById(R.id.mute_action_button);
mReplyActionButton = itemView.findViewById(R.id.reply_action_button);
+ mPlayActionButton = itemView.findViewById(R.id.play_action_button);
mDivider = itemView.findViewById(R.id.divider);
mAvatarView.setOutlineProvider(CircularOutputlineProvider.get());
mUnreadIconIndicator.setOutlineProvider(CircularOutputlineProvider.get());
@@ -83,14 +88,16 @@ public class ConversationItemViewHolder extends RecyclerView.ViewHolder {
/** Binds the view holder with relevant data. */
public void bind(@NonNull UIConversationItem uiData) {
mTitleView.setText(uiData.getTitle());
- mTimeTextView.setText(uiData.getReadableTime());
- mTextView.setText(uiData.getSubtitle());
+ mPreviewTextView.setText(uiData.getTextPreview());
+ mTextMetadataView.setText(uiData.getTextMetadata());
+ mDateTimeView.setTime(uiData.mLastMessageTimestamp);
mAvatarView.setImageDrawable(uiData.getAvatar());
mPlayMessageTouchView.setOnClickListener(null);
mSubtitleIconView.setImageDrawable(uiData.getSubtitleIcon());
- boolean showDotSeparatorSubtitle =
- !uiData.getReadableTime().isEmpty() && !uiData.getSubtitle().isEmpty();
- ViewUtils.setVisible(mDotSeparatorView, showDotSeparatorSubtitle);
+ boolean showPreviewSeparator = !uiData.getTextPreview().isEmpty();
+ boolean showMetadataSeparator = !uiData.getTextMetadata().isEmpty();
+ ViewUtils.setVisible(mDotSeparatorView, showPreviewSeparator);
+ ViewUtils.setVisible(mTextMetadataDotView, showMetadataSeparator);
ViewUtils.setVisible(mSubtitleIconView, uiData.getSubtitleIcon() != null);
setUpActionButton(uiData);
setUpTextAppearance(uiData);
@@ -101,25 +108,31 @@ public class ConversationItemViewHolder extends RecyclerView.ViewHolder {
private void setUpTextAppearance(@NonNull UIConversationItem uiData) {
if (uiData.shouldUseUnreadTheme()) {
mTitleView.setTextAppearance(R.style.TextAppearance_MessageHistoryUnreadTitle);
- mTimeTextView.setTextAppearance(R.style.TextAppearance_MessageHistoryUnreadSubtitle);
- mTextView.setTextAppearance(R.style.TextAppearance_MessageHistoryUnreadSubtitle);
- mDotSeparatorView.setTextAppearance(
- R.style.TextAppearance_MessageHistoryUnreadSubtitle);
+ mPreviewTextView.setTextAppearance(R.style.TextAppearance_MessageHistoryTextPreview);
+ mTextMetadataView.setTextAppearance(
+ R.style.TextAppearance_MessageHistoryUnreadMetadata);
+ mTextMetadataDotView.setTextAppearance(
+ R.style.TextAppearance_MessageHistoryUnreadMetadata);
+ mDateTimeView.setTextAppearance(R.style.TextAppearance_MessageHistoryUnreadMetadata);
+ mDotSeparatorView.setTextAppearance(R.style.TextAppearance_MessageHistoryTextPreview);
ViewUtils.setVisible(mUnreadIconIndicator, /* visible= */ true);
} else {
mTitleView.setTextAppearance(R.style.TextAppearance_MessageHistoryTitle);
- mTimeTextView.setTextAppearance(R.style.TextAppearance_MessageHistorySubtitle);
- mTextView.setTextAppearance(R.style.TextAppearance_MessageHistorySubtitle);
- mDotSeparatorView.setTextAppearance(R.style.TextAppearance_MessageHistorySubtitle);
+ mPreviewTextView.setTextAppearance(R.style.TextAppearance_MessageHistoryTextPreview);
+ mTextMetadataView.setTextAppearance(R.style.TextAppearance_MessageHistoryTextPreview);
+ mTextMetadataDotView.setTextAppearance(
+ R.style.TextAppearance_MessageHistoryTextPreview);
+ mDateTimeView.setTextAppearance(R.style.TextAppearance_MessageHistoryTextPreview);
+ mDotSeparatorView.setTextAppearance(R.style.TextAppearance_MessageHistoryTextPreview);
ViewUtils.setVisible(mUnreadIconIndicator, /* visible= */ false);
}
}
private void updateMuteButton(boolean isMuted) {
- @ColorInt int color = isMuted ? Color.RED : Color.WHITE;
- PorterDuffColorFilter porterDuffColorFilter =
- new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP);
- mMuteActionButton.getDrawable().setColorFilter(porterDuffColorFilter);
+ int drawableRes = isMuted ? R.drawable.ic_unmute : R.drawable.ic_mute;
+ Drawable drawable = AppFactory.get().getContext().getDrawable(drawableRes);
+
+ mMuteActionButton.setImageDrawable(drawable);
}
/** Recycles views. */
@@ -128,15 +141,23 @@ public class ConversationItemViewHolder extends RecyclerView.ViewHolder {
}
private void setUpActionButton(@NonNull UIConversationItem uiData) {
- ViewUtils.setVisible(mDivider, uiData.shouldShowReplyIcon() || uiData.shouldShowMuteIcon());
+ ViewUtils.setVisible(
+ mDivider,
+ uiData.shouldShowReplyIcon()
+ || uiData.shouldShowMuteIcon()
+ || uiData.shouldShowPlayIcon());
ViewUtils.setVisible(mMuteActionButton, uiData.shouldShowMuteIcon());
ViewUtils.setVisible(mReplyActionButton, uiData.shouldShowReplyIcon());
+ ViewUtils.setVisible(mPlayActionButton, uiData.shouldShowPlayIcon());
if (uiData.shouldShowReplyIcon()) {
mReplyActionButton.setEnabled(true);
}
- if (uiData.shouldShowReplyIcon()) {
+ if (uiData.shouldShowMuteIcon()) {
mMuteActionButton.setEnabled(true);
}
+ if (uiData.shouldShowPlayIcon()) {
+ mPlayActionButton.setEnabled(true);
+ }
mPlayMessageTouchView.setOnClickListener(
view ->
@@ -147,6 +168,11 @@ public class ConversationItemViewHolder extends RecyclerView.ViewHolder {
view ->
mOnConversationItemClickListener.onReplyIconClicked(
uiData.getConversation()));
+
+ mPlayActionButton.setOnClickListener(
+ view ->
+ mOnConversationItemClickListener.onPlayIconClicked(
+ uiData.getConversation()));
mMuteActionButton.setOnClickListener(
view -> {
boolean mute = !uiData.isMuted();
diff --git a/src/com/android/car/messenger/core/ui/conversationlist/ConversationListFragment.java b/src/com/android/car/messenger/core/ui/conversationlist/ConversationListFragment.java
index ccc0396..b00fb5f 100644
--- a/src/com/android/car/messenger/core/ui/conversationlist/ConversationListFragment.java
+++ b/src/com/android/car/messenger/core/ui/conversationlist/ConversationListFragment.java
@@ -87,7 +87,7 @@ public class ConversationListFragment extends MessageListBaseFragment
|| conversationLog.getData().isEmpty()) {
mLoadingFrameLayout.showEmpty(
MessageConstants.INVALID_RES_ID,
- R.string.no_new_messages,
+ R.string.no_messages,
MessageConstants.INVALID_RES_ID);
setMenuItems();
} else {
@@ -122,10 +122,11 @@ public class ConversationListFragment extends MessageListBaseFragment
}
MenuItem newMessageButton =
new MenuItem.Builder(activity)
- .setIcon(R.drawable.car_ui_icon_edit)
+ .setIcon(R.drawable.ui_icon_edit)
.setTinted(true)
.setShowIconAndTitle(true)
.setTitle(R.string.new_message)
+ .setPrimary(true)
.setOnClickListener(
item ->
VoiceUtil.voiceRequestGenericCompose(
@@ -152,6 +153,14 @@ public class ConversationListFragment extends MessageListBaseFragment
VoiceUtil.voiceRequestReplyConversation(requireActivity(), mUserAccount, conversation);
}
+ @Override
+ public void onPlayIconClicked(@NonNull Conversation conversation) {
+ if (mUserAccount == null) {
+ return;
+ }
+ VoiceUtil.voiceRequestReadConversation(requireActivity(), mUserAccount, conversation);
+ }
+
/**
* Get instance of Conversation Log fragment
*
diff --git a/src/com/android/car/messenger/core/ui/conversationlist/ConversationListViewModel.java b/src/com/android/car/messenger/core/ui/conversationlist/ConversationListViewModel.java
index 8ac0851..6b5d199 100644
--- a/src/com/android/car/messenger/core/ui/conversationlist/ConversationListViewModel.java
+++ b/src/com/android/car/messenger/core/ui/conversationlist/ConversationListViewModel.java
@@ -19,15 +19,17 @@ package com.android.car.messenger.core.ui.conversationlist;
import android.annotation.SuppressLint;
import android.app.Application;
import androidx.lifecycle.AndroidViewModel;
-import androidx.lifecycle.LiveData;
-import androidx.lifecycle.MediatorLiveData;
+import android.car.drivingstate.CarUxRestrictions;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MediatorLiveData;
import com.android.car.messenger.core.interfaces.AppFactory;
import com.android.car.messenger.core.interfaces.DataModel;
import com.android.car.messenger.core.models.UserAccount;
+import com.android.car.messenger.core.util.L;
import java.util.List;
import java.util.stream.Collectors;
@@ -75,15 +77,30 @@ public class ConversationListViewModel extends AndroidViewModel {
MediatorLiveData<UIConversationLog> mutableLiveData = new MediatorLiveData<>();
mutableLiveData.postValue(UIConversationLog.getLoadingState());
mutableLiveData.addSource(
+ AppFactory.get().getCarStateListener().getUxrRestrictions(),
+ uxrRestrictions ->
+ subscribeToConversations(userAccount, mutableLiveData, uxrRestrictions));
+ return mutableLiveData;
+ }
+
+ private void subscribeToConversations(
+ @NonNull UserAccount userAccount,
+ MediatorLiveData<UIConversationLog> mutableLiveData,
+ CarUxRestrictions uxRestrictions) {
+ L.w("Got new ux restrictions: " + uxRestrictions);
+ mutableLiveData.addSource(
mDataModel.getConversations(userAccount),
list -> {
List<UIConversationItem> data =
list.stream()
- .map(UIConversationItemConverter::convertToUIConversationItem)
+ .map(
+ conversation ->
+ UIConversationItemConverter
+ .convertToUIConversationItem(
+ conversation, uxRestrictions))
.collect(Collectors.toList());
UIConversationLog log = UIConversationLog.getLoadedState(data);
mutableLiveData.postValue(log);
});
- return mutableLiveData;
}
}
diff --git a/src/com/android/car/messenger/core/ui/conversationlist/UIConversationItem.java b/src/com/android/car/messenger/core/ui/conversationlist/UIConversationItem.java
index bdd71e7..7381e91 100644
--- a/src/com/android/car/messenger/core/ui/conversationlist/UIConversationItem.java
+++ b/src/com/android/car/messenger/core/ui/conversationlist/UIConversationItem.java
@@ -28,36 +28,42 @@ public class UIConversationItem {
@NonNull String mConversationId;
@NonNull String mTitle;
- @NonNull String mSubtitle;
+ @NonNull String mTextPreview;
@Nullable Drawable mSubtitleIcon;
- @NonNull String mReadableTime;
+ @NonNull String mTextMetadata;
+ long mLastMessageTimestamp;
@Nullable Drawable mAvatar;
boolean mIsMuted;
boolean mShowMuteIcon;
boolean mShowReplyIcon;
+ boolean mShowPlayIcon;
boolean mUseUnreadTheme;
@NonNull Conversation mConversation;
public UIConversationItem(
@NonNull String conversationId,
@NonNull String title,
- @NonNull String subtitle,
+ @NonNull String textPreview,
@Nullable Drawable subtitleIcon,
- @NonNull String readableTime,
+ @NonNull String textMetadata,
+ long lastMessageTimestamp,
@Nullable Drawable avatar,
boolean showMuteIcon,
boolean showReplyIcon,
+ boolean showPlayIcon,
boolean useUnreadTheme,
boolean isMuted,
@NonNull Conversation conversation) {
this.mConversationId = conversationId;
this.mTitle = title;
- this.mSubtitle = subtitle;
+ this.mTextPreview = textPreview;
this.mSubtitleIcon = subtitleIcon;
- this.mReadableTime = readableTime;
+ this.mTextMetadata = textMetadata;
+ this.mLastMessageTimestamp = lastMessageTimestamp;
this.mAvatar = avatar;
this.mShowMuteIcon = showMuteIcon;
this.mShowReplyIcon = showReplyIcon;
+ this.mShowPlayIcon = showPlayIcon;
this.mUseUnreadTheme = useUnreadTheme;
this.mIsMuted = isMuted;
this.mConversation = conversation;
@@ -75,10 +81,10 @@ public class UIConversationItem {
return mTitle;
}
- /** Returns subtitle for the conversation */
+ /** Returns text preview for the conversation */
@NonNull
- public String getSubtitle() {
- return mSubtitle;
+ public String getTextPreview() {
+ return mTextPreview;
}
/**
@@ -90,10 +96,15 @@ public class UIConversationItem {
return mSubtitleIcon;
}
- /** Gets the human readable time in hh::mm */
+ /** Gets text metadata */
@NonNull
- public String getReadableTime() {
- return mReadableTime;
+ public String getTextMetadata() {
+ return mTextMetadata;
+ }
+
+ /** Gets last message timestamp */
+ public long getLastMessageTimestamp() {
+ return mLastMessageTimestamp;
}
/** Returns the avatar for the conversation */
@@ -117,6 +128,11 @@ public class UIConversationItem {
return mShowReplyIcon;
}
+ /** Returns true, if play icon should be shown, false otherwise */
+ public boolean shouldShowPlayIcon() {
+ return mShowPlayIcon;
+ }
+
/** Returns true, if unread theme should be used, false otherwise */
public boolean shouldUseUnreadTheme() {
return mUseUnreadTheme;
diff --git a/src/com/android/car/messenger/core/ui/conversationlist/UIConversationItemConverter.java b/src/com/android/car/messenger/core/ui/conversationlist/UIConversationItemConverter.java
index 94f597d..00f2a71 100644
--- a/src/com/android/car/messenger/core/ui/conversationlist/UIConversationItemConverter.java
+++ b/src/com/android/car/messenger/core/ui/conversationlist/UIConversationItemConverter.java
@@ -16,12 +16,11 @@
package com.android.car.messenger.core.ui.conversationlist;
+import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import android.graphics.drawable.Drawable;
-import android.text.format.DateFormat;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import com.android.car.messenger.R;
import com.android.car.messenger.common.Conversation;
@@ -36,55 +35,103 @@ public class UIConversationItemConverter {
private UIConversationItemConverter() {}
/** Converts Conversation Item to UIConversationItem */
- public static UIConversationItem convertToUIConversationItem(Conversation conversation) {
+ public static UIConversationItem convertToUIConversationItem(
+ Conversation conversation, CarUxRestrictions carUxRestrictions) {
Context context = AppFactory.get().getContext();
boolean isUnread = conversation.getUnreadCount() > 0;
long timestamp = ConversationUtil.getConversationTimestamp(conversation);
boolean isReplied = ConversationUtil.isReplied(conversation);
- String subtitle = "";
Drawable subtitleIcon = null;
+
+ // show reply icon to the side when replied
if (isReplied) {
- subtitle = context.getString(R.string.replied);
subtitleIcon = context.getDrawable(R.drawable.car_ui_icon_reply);
- } else if (isUnread) {
- subtitle = getNumberOfUnreadMessages(context, conversation.getUnreadCount());
- subtitleIcon = context.getDrawable(R.drawable.ic_play);
}
+ boolean showTextPreview =
+ (carUxRestrictions.getActiveRestrictions()
+ & CarUxRestrictions.UX_RESTRICTIONS_NO_TEXT_MESSAGE)
+ == 0;
+ String textPreview = "";
+ String textMetadata = "";
+
+ // show a preview when parked
+ if (showTextPreview) {
+ textPreview = ConversationUtil.getLastMessagePreview(conversation);
+ if (isUnread) {
+ textMetadata = getNumberOfMoreMessages(context, conversation.getUnreadCount());
+ }
+ } else {
+ if (isUnread) {
+ textMetadata = getNumberOfUnreadMessages(context, conversation.getUnreadCount());
+ } else if (isReplied) {
+ textMetadata = context.getString(R.string.replied);
+ } else {
+ textMetadata = getNumberOfMessages(context, conversation.getMessages().size());
+ }
+ }
+
+ boolean showPlayIcon = isUnread;
+ boolean showReplyIcon =
+ !showPlayIcon && context.getResources().getBoolean(R.bool.direct_reply_supported);
+
return new UIConversationItem(
conversation.getId(),
Objects.requireNonNull(conversation.getConversationTitle()),
- subtitle,
+ textPreview,
subtitleIcon,
- toHumanDisplay(timestamp),
+ textMetadata,
+ timestamp,
getConversationAvatar(context, conversation),
/* showMuteIcon= */ true,
- /* showReplyIcon= */ true,
+ /* showReplyIcon= */ showReplyIcon,
+ /* showPlayIcon= */ showPlayIcon,
isUnread,
conversation.isMuted(),
conversation);
}
+ /**
+ * For the text "More Unread Messages", indicates the number of messages remaining after the
+ * preview.
+ */
+ @NonNull
+ private static String getNumberOfMoreMessages(
+ @NonNull Context context, int noOfUnreadMessages) {
+ int remainingMessagesAfterPreview = noOfUnreadMessages - 1;
+ if (remainingMessagesAfterPreview == 0) {
+ return "";
+ }
+ if (remainingMessagesAfterPreview == 1) {
+ return context.getResources().getQuantityString(R.plurals.more_message, 1);
+ }
+ return context.getResources()
+ .getQuantityString(
+ R.plurals.more_message,
+ remainingMessagesAfterPreview,
+ remainingMessagesAfterPreview);
+ }
+
@NonNull
private static String getNumberOfUnreadMessages(
@NonNull Context context, int noOfUnreadMessages) {
if (noOfUnreadMessages == 1) {
return context.getResources().getQuantityString(R.plurals.new_message, 1);
- } else {
- return context.getResources()
- .getQuantityString(
- R.plurals.new_message, noOfUnreadMessages, noOfUnreadMessages);
}
+ return context.getResources()
+ .getQuantityString(R.plurals.new_message, noOfUnreadMessages, noOfUnreadMessages);
}
@NonNull
- private static String toHumanDisplay(long timeInMillis) {
- String delegate = "hh:mm aaa";
- return (String) DateFormat.format(delegate, timeInMillis);
+ private static String getNumberOfMessages(@NonNull Context context, int noOfMessages) {
+ if (noOfMessages < 2) {
+ return context.getResources().getQuantityString(R.plurals.no_of_message, noOfMessages);
+ }
+ return context.getResources()
+ .getQuantityString(R.plurals.no_of_message, noOfMessages, noOfMessages);
}
- @Nullable
private static Drawable getConversationAvatar(
@NonNull Context context, @NonNull Conversation conversation) {
return (conversation.getConversationIcon() != null)
diff --git a/src/com/android/car/messenger/core/ui/launcher/MessageLauncherActivity.java b/src/com/android/car/messenger/core/ui/launcher/MessageLauncherActivity.java
index ddd227c..14c1951 100644
--- a/src/com/android/car/messenger/core/ui/launcher/MessageLauncherActivity.java
+++ b/src/com/android/car/messenger/core/ui/launcher/MessageLauncherActivity.java
@@ -74,9 +74,9 @@ public class MessageLauncherActivity extends FragmentActivity implements InsetsC
@Override
protected void onResume() {
- super.onResume();
L.d("On Resume of Message Activity.");
AppFactory.get().getDataModel().refreshUserAccounts();
+ super.onResume();
}
private void pushContentFragment(
diff --git a/src/com/android/car/messenger/core/ui/launcher/MessageLauncherViewModel.java b/src/com/android/car/messenger/core/ui/launcher/MessageLauncherViewModel.java
index 4b4d96a..2df42c1 100644
--- a/src/com/android/car/messenger/core/ui/launcher/MessageLauncherViewModel.java
+++ b/src/com/android/car/messenger/core/ui/launcher/MessageLauncherViewModel.java
@@ -18,16 +18,13 @@ package com.android.car.messenger.core.ui.launcher;
import android.app.Application;
import androidx.lifecycle.AndroidViewModel;
-import androidx.lifecycle.LiveData;
import androidx.lifecycle.Transformations;
-
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-
+import androidx.lifecycle.LiveData;
import com.android.car.messenger.core.interfaces.AppFactory;
import com.android.car.messenger.core.interfaces.DataModel;
import com.android.car.messenger.core.models.UserAccount;
-
import java.util.List;
import java.util.stream.Collectors;
diff --git a/src/com/android/car/messenger/core/ui/shared/DateTimeView.java b/src/com/android/car/messenger/core/ui/shared/DateTimeView.java
new file mode 100644
index 0000000..6947fcd
--- /dev/null
+++ b/src/com/android/car/messenger/core/ui/shared/DateTimeView.java
@@ -0,0 +1,537 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.messenger.core.ui.shared;
+
+import static android.text.format.DateUtils.DAY_IN_MILLIS;
+import static android.text.format.DateUtils.HOUR_IN_MILLIS;
+import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
+import static android.text.format.DateUtils.YEAR_IN_MILLIS;
+
+import static java.lang.Math.max;
+import static java.lang.Math.min;
+
+import android.annotation.SuppressLint;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Configuration;
+import android.os.Handler;
+import android.util.AttributeSet;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.TextView;
+
+import com.android.car.messenger.R;
+import com.android.car.messenger.core.interfaces.AppFactory;
+
+import java.text.DateFormat;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.ZoneId;
+import java.time.temporal.JulianFields;
+import java.util.ArrayList;
+
+/**
+ * Class to provide an updatable custom view with the relative time, based on the time set and the
+ * current time. Example: 1 min ago, 2 min ago etc. This is a copy of the Android Source Hidden
+ * File:
+ * https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/widget/DateTimeView.java
+ */
+@SuppressLint("AppCompatCustomView")
+public class DateTimeView extends TextView {
+ private static final int SHOW_TIME = 0;
+ private static final int SHOW_MONTH_DAY_YEAR = 1;
+
+ private long mTimeMillis;
+ // The LocalDateTime equivalent of mTimeMillis but truncated to minute, i.e. no seconds / nanos.
+ private LocalDateTime mLocalTime;
+
+ private static final int LAST_DISPLAY = -1;
+ private DateFormat mLastFormat;
+
+ private long mUpdateTimeMillis;
+ private static final ThreadLocal<ReceiverInfo> sReceiverInfo = new ThreadLocal<>();
+ private String mNowText;
+ private boolean mShowRelativeTime;
+
+ public DateTimeView(Context context) {
+ this(context, null);
+ }
+
+ public DateTimeView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ ReceiverInfo ri = sReceiverInfo.get();
+ if (ri == null) {
+ ri = new ReceiverInfo();
+ sReceiverInfo.set(ri);
+ }
+ ri.addView(this);
+ // The view may not be added to the view hierarchy immediately right after setTime()
+ // is called which means it won't get any update from intents before being added.
+ // In such case, the view might show the incorrect relative time after being added to the
+ // view hierarchy until the next update intent comes.
+ // So we update the time here if mShowRelativeTime is enabled to prevent this case.
+ if (mShowRelativeTime) {
+ update();
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ final ReceiverInfo ri = sReceiverInfo.get();
+ if (ri != null) {
+ ri.removeView(this);
+ }
+ }
+
+ /** Set the time when the event occurred to compare against the current time. */
+ public void setTime(long timeMillis) {
+ mTimeMillis = timeMillis;
+ LocalDateTime dateTime = toLocalDateTime(timeMillis, ZoneId.systemDefault());
+ mLocalTime = dateTime.withSecond(0);
+ update();
+ }
+
+ /**
+ * Show Relative Time, allows the view to show the current time against the time set in {@link
+ * #setTime(long)}
+ */
+ public void setShowRelativeTime(boolean showRelativeTime) {
+ mShowRelativeTime = showRelativeTime;
+ updateNowText();
+ update();
+ }
+
+ /**
+ * Returns whether this view shows relative time
+ *
+ * @return True if it shows relative time, false otherwise
+ */
+ public boolean isShowRelativeTime() {
+ return mShowRelativeTime;
+ }
+
+ @Override
+ public void setVisibility(int visibility) {
+ boolean gotVisible = visibility != GONE && getVisibility() == GONE;
+ super.setVisibility(visibility);
+ if (gotVisible) {
+ update();
+ }
+ }
+
+ void update() {
+ if (mLocalTime == null || getVisibility() == GONE) {
+ return;
+ }
+ if (mShowRelativeTime) {
+ updateRelativeTime();
+ return;
+ }
+
+ int display;
+ ZoneId zoneId = ZoneId.systemDefault();
+
+ // localTime is the local time for mTimeMillis but at zero seconds past the minute.
+ LocalDateTime localTime = mLocalTime;
+ LocalDateTime localStartOfDay = localTime.toLocalDate().atStartOfDay();
+ LocalDateTime localTomorrowStartOfDay = localStartOfDay.plusDays(1);
+ // now is current local time but at zero seconds past the minute.
+ LocalDateTime localNow = LocalDateTime.now(zoneId).withSecond(0);
+
+ long twelveHoursBefore = toEpochMillis(localTime.minusHours(12), zoneId);
+ long twelveHoursAfter = toEpochMillis(localTime.plusHours(12), zoneId);
+ long midnightBefore = toEpochMillis(localStartOfDay, zoneId);
+ long midnightAfter = toEpochMillis(localTomorrowStartOfDay, zoneId);
+ long time = toEpochMillis(localTime, zoneId);
+ long now = toEpochMillis(localNow, zoneId);
+
+ // Choose the display mode
+ choose_display:
+ {
+ if ((now >= midnightBefore && now < midnightAfter)
+ || (now >= twelveHoursBefore && now < twelveHoursAfter)) {
+ display = SHOW_TIME;
+ break choose_display;
+ }
+ // Else, show month day and year.
+ display = SHOW_MONTH_DAY_YEAR;
+ break choose_display;
+ }
+
+ // Choose the format
+ DateFormat format;
+ if (display == LAST_DISPLAY && mLastFormat != null) {
+ // use cached format
+ format = mLastFormat;
+ } else {
+ switch (display) {
+ case SHOW_TIME:
+ format = getTimeFormat();
+ break;
+ case SHOW_MONTH_DAY_YEAR:
+ format = DateFormat.getDateInstance(DateFormat.SHORT);
+ break;
+ default:
+ throw new IllegalArgumentException("unknown display value: " + display);
+ }
+ mLastFormat = format;
+ }
+
+ // Set the text
+ String text = format.format(Instant.ofEpochMilli(time));
+ setText(text);
+
+ // Schedule the next update
+ if (display == SHOW_TIME) {
+ // Currently showing the time, update at the later of twelve hours after or midnight.
+ mUpdateTimeMillis = max(twelveHoursAfter, midnightAfter);
+ } else {
+ // Currently showing the date
+ // If hte time is in the future, schedule one at the earlier of twelve hours
+ // before or midnight before.
+ if (mTimeMillis < now) {
+ // If the time is in the past, don't schedule an update
+ mUpdateTimeMillis = 0;
+ } else {
+ mUpdateTimeMillis = min(twelveHoursBefore, midnightBefore);
+ }
+ }
+ }
+
+ private void updateRelativeTime() {
+ long now = System.currentTimeMillis();
+ long duration = Math.abs(now - mTimeMillis);
+ int count;
+ long millisIncrease;
+ boolean past = (now >= mTimeMillis);
+ String result;
+ if (duration < MINUTE_IN_MILLIS) {
+ setText(mNowText);
+ mUpdateTimeMillis = mTimeMillis + MINUTE_IN_MILLIS + 1;
+ return;
+ } else if (duration < HOUR_IN_MILLIS) {
+ count = (int) (duration / MINUTE_IN_MILLIS);
+ result =
+ String.format(
+ getContext()
+ .getResources()
+ .getQuantityString(
+ past
+ ? R.plurals.duration_minutes_shortest
+ : R.plurals.duration_minutes_shortest_future,
+ count),
+ count);
+ millisIncrease = MINUTE_IN_MILLIS;
+ } else if (duration < DAY_IN_MILLIS) {
+ count = (int) (duration / HOUR_IN_MILLIS);
+ result =
+ String.format(
+ getContext()
+ .getResources()
+ .getQuantityString(
+ past
+ ? R.plurals.duration_hours_shortest
+ : R.plurals.duration_hours_shortest_future,
+ count),
+ count);
+ millisIncrease = HOUR_IN_MILLIS;
+ } else if (duration < YEAR_IN_MILLIS) {
+ // In weird cases it can become 0 because of daylight savings
+ LocalDateTime localDateTime = mLocalTime;
+ ZoneId zoneId = ZoneId.systemDefault();
+ LocalDateTime localNow = toLocalDateTime(now, zoneId);
+
+ count = max(Math.abs(dayDistance(localDateTime, localNow)), 1);
+ result =
+ String.format(
+ getContext()
+ .getResources()
+ .getQuantityString(
+ past
+ ? R.plurals.duration_days_shortest
+ : R.plurals.duration_days_shortest_future,
+ count),
+ count);
+ if (past || count != 1) {
+ mUpdateTimeMillis = computeNextMidnight(localNow, zoneId);
+ millisIncrease = -1;
+ } else {
+ millisIncrease = DAY_IN_MILLIS;
+ }
+
+ } else {
+ count = (int) (duration / YEAR_IN_MILLIS);
+ result =
+ String.format(
+ getContext()
+ .getResources()
+ .getQuantityString(
+ past
+ ? R.plurals.duration_years_shortest
+ : R.plurals.duration_years_shortest_future,
+ count),
+ count);
+ millisIncrease = YEAR_IN_MILLIS;
+ }
+ if (millisIncrease != -1) {
+ if (past) {
+ mUpdateTimeMillis = mTimeMillis + millisIncrease * (count + 1) + 1;
+ } else {
+ mUpdateTimeMillis = mTimeMillis - millisIncrease * count + 1;
+ }
+ }
+ setText(result);
+ }
+
+ /** Returns the epoch millis for the next midnight in the specified timezone. */
+ private static long computeNextMidnight(LocalDateTime time, ZoneId zoneId) {
+ // This ignores the chance of overflow: it should never happen.
+ LocalDate tomorrow = time.toLocalDate().plusDays(1);
+ LocalDateTime nextMidnight = LocalDateTime.of(tomorrow, LocalTime.MIDNIGHT);
+ return toEpochMillis(nextMidnight, zoneId);
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ updateNowText();
+ update();
+ }
+
+ private void updateNowText() {
+ if (!mShowRelativeTime) {
+ return;
+ }
+ mNowText = getContext().getResources().getString(R.string.now_string_shortest);
+ }
+
+ // Return the number of days between the two dates.
+ private static int dayDistance(LocalDateTime start, LocalDateTime end) {
+ return (int)
+ (end.getLong(JulianFields.JULIAN_DAY) - start.getLong(JulianFields.JULIAN_DAY));
+ }
+
+ private DateFormat getTimeFormat() {
+ return android.text.format.DateFormat.getTimeFormat(getContext());
+ }
+
+ void clearFormatAndUpdate() {
+ mLastFormat = null;
+ update();
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ if (mShowRelativeTime) {
+ // The short version of the time might not be completely understandable and for
+ // accessibility we rather have a longer version.
+ long now = System.currentTimeMillis();
+ long duration = Math.abs(now - mTimeMillis);
+ int count;
+ boolean past = (now >= mTimeMillis);
+ String result;
+ if (duration < MINUTE_IN_MILLIS) {
+ result = mNowText;
+ } else if (duration < HOUR_IN_MILLIS) {
+ count = (int) (duration / MINUTE_IN_MILLIS);
+ result =
+ String.format(
+ getContext()
+ .getResources()
+ .getQuantityString(
+ past
+ ? R.plurals.duration_minutes_relative
+ : R.plurals
+ .duration_minutes_relative_future,
+ count),
+ count);
+ } else if (duration < DAY_IN_MILLIS) {
+ count = (int) (duration / HOUR_IN_MILLIS);
+ result =
+ String.format(
+ getContext()
+ .getResources()
+ .getQuantityString(
+ past
+ ? R.plurals.duration_hours_relative
+ : R.plurals.duration_hours_relative_future,
+ count),
+ count);
+ } else if (duration < YEAR_IN_MILLIS) {
+ // In weird cases it can become 0 because of daylight savings
+ LocalDateTime localDateTime = mLocalTime;
+ ZoneId zoneId = ZoneId.systemDefault();
+ LocalDateTime localNow = toLocalDateTime(now, zoneId);
+
+ count = max(Math.abs(dayDistance(localDateTime, localNow)), 1);
+ result =
+ String.format(
+ getContext()
+ .getResources()
+ .getQuantityString(
+ past
+ ? R.plurals.duration_days_relative
+ : R.plurals.duration_days_relative_future,
+ count),
+ count);
+
+ } else {
+ count = (int) (duration / YEAR_IN_MILLIS);
+ result =
+ String.format(
+ getContext()
+ .getResources()
+ .getQuantityString(
+ past
+ ? R.plurals.duration_years_relative
+ : R.plurals.duration_years_relative_future,
+ count),
+ count);
+ }
+ info.setText(result);
+ }
+ }
+
+ /** Set Receiver Handler */
+ public static void setReceiverHandler(Handler handler) {
+ ReceiverInfo ri = sReceiverInfo.get();
+ if (ri == null) {
+ ri = new ReceiverInfo();
+ sReceiverInfo.set(ri);
+ }
+ ri.setHandler(handler);
+ }
+
+ private static class ReceiverInfo {
+ private final ArrayList<DateTimeView> mAttachedViews = new ArrayList<DateTimeView>();
+ private final BroadcastReceiver mReceiver =
+ new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (Intent.ACTION_TIME_TICK.equals(action)) {
+ if (System.currentTimeMillis() < getSoonestUpdateTime()) {
+ // The update() function takes a few milliseconds to run because of
+ // all of the time conversions it needs to do, so we can't do that
+ // every minute.
+ return;
+ }
+ }
+ // ACTION_TIME_CHANGED can also signal a change of 12/24 hr. format.
+ updateAll();
+ }
+ };
+
+ private Handler mHandler = new Handler();
+
+ public void addView(DateTimeView v) {
+ synchronized (mAttachedViews) {
+ final boolean register = mAttachedViews.isEmpty();
+ mAttachedViews.add(v);
+ if (register) {
+ register(getApplicationContextIfAvailable(v.getContext()));
+ }
+ }
+ }
+
+ public void removeView(DateTimeView v) {
+ synchronized (mAttachedViews) {
+ final boolean removed = mAttachedViews.remove(v);
+ // Only unregister once when we remove the last view in the list otherwise we risk
+ // trying to unregister a receiver that is no longer registered.
+ if (removed && mAttachedViews.isEmpty()) {
+ unregister(getApplicationContextIfAvailable(v.getContext()));
+ }
+ }
+ }
+
+ void updateAll() {
+ synchronized (mAttachedViews) {
+ final int count = mAttachedViews.size();
+ for (int i = 0; i < count; i++) {
+ DateTimeView view = mAttachedViews.get(i);
+ view.post(() -> view.clearFormatAndUpdate());
+ }
+ }
+ }
+
+ long getSoonestUpdateTime() {
+ long result = Long.MAX_VALUE;
+ synchronized (mAttachedViews) {
+ final int count = mAttachedViews.size();
+ for (int i = 0; i < count; i++) {
+ final long time = mAttachedViews.get(i).mUpdateTimeMillis;
+ if (time < result) {
+ result = time;
+ }
+ }
+ }
+ return result;
+ }
+
+ static final Context getApplicationContextIfAvailable(Context context) {
+ final Context ac = context.getApplicationContext();
+ return ac != null ? ac : AppFactory.get().getContext();
+ }
+
+ void register(Context context) {
+ final IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_TIME_TICK);
+ filter.addAction(Intent.ACTION_TIME_CHANGED);
+ filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
+ filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
+ context.registerReceiver(mReceiver, filter, null, mHandler);
+ }
+
+ void unregister(Context context) {
+ context.unregisterReceiver(mReceiver);
+ }
+
+ public void setHandler(Handler handler) {
+ mHandler = handler;
+ synchronized (mAttachedViews) {
+ if (!mAttachedViews.isEmpty()) {
+ unregister(mAttachedViews.get(0).getContext());
+ register(mAttachedViews.get(0).getContext());
+ }
+ }
+ }
+ }
+
+ private static LocalDateTime toLocalDateTime(long timeMillis, ZoneId zoneId) {
+ // java.time types like LocalDateTime / Instant can support the full range of "long millis"
+ // with room to spare so we do not need to worry about overflow / underflow and the rsulting
+ // exceptions while the input to this class is a long.
+ Instant instant = Instant.ofEpochMilli(timeMillis);
+ return LocalDateTime.ofInstant(instant, zoneId);
+ }
+
+ private static long toEpochMillis(LocalDateTime time, ZoneId zoneId) {
+ Instant instant = time.toInstant(zoneId.getRules().getOffset(time));
+ return instant.toEpochMilli();
+ }
+}
diff --git a/src/com/android/car/messenger/impl/common/ProjectionStateListener.java b/src/com/android/car/messenger/core/util/CarStateListener.java
index cb3b26d..3e54288 100644
--- a/src/com/android/car/messenger/impl/common/ProjectionStateListener.java
+++ b/src/com/android/car/messenger/core/util/CarStateListener.java
@@ -14,11 +14,13 @@
* limitations under the License.
*/
-package com.android.car.messenger.impl.common;
+package com.android.car.messenger.core.util;
import android.bluetooth.BluetoothDevice;
import android.car.Car;
import android.car.CarProjectionManager;
+import android.car.drivingstate.CarUxRestrictions;
+import android.car.drivingstate.CarUxRestrictionsManager;
import android.car.projection.ProjectionStatus;
import android.content.Context;
import android.os.Bundle;
@@ -26,8 +28,8 @@ import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-
-import com.android.car.messenger.core.util.L;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
import java.util.ArrayList;
import java.util.Collections;
@@ -37,12 +39,20 @@ import java.util.List;
* {@link ProjectionStatus} listener that exposes APIs to detect whether a projection application is
* active.
*/
-public class ProjectionStateListener {
+public class CarStateListener {
@NonNull
static final String PROJECTION_STATUS_EXTRA_DEVICE_STATE =
"android.car.projection.DEVICE_STATE";
@Nullable private CarProjectionManager mCarProjectionManager = null;
+ @Nullable private CarUxRestrictionsManager mCarUxRestrictionsManager = null;
+
+ @NonNull
+ private final MutableLiveData<CarUxRestrictions> mUxRestrictions = new MutableLiveData<>();
+
+ @NonNull
+ private final CarUxRestrictionsManager.OnUxRestrictionsChangedListener
+ mCarUxRestrictionListener = mUxRestrictions::postValue;
@NonNull
private final CarProjectionManager.ProjectionStatusListener mListener =
@@ -56,7 +66,7 @@ public class ProjectionStateListener {
private int mProjectionState = ProjectionStatus.PROJECTION_STATE_INACTIVE;
@NonNull private List<ProjectionStatus> mProjectionDetails = new ArrayList<>();
- public ProjectionStateListener(@NonNull Context context) {
+ public CarStateListener(@NonNull Context context) {
Car.createCar(
context,
/* handler= */ null,
@@ -65,9 +75,17 @@ public class ProjectionStateListener {
mCar = car;
mCarProjectionManager =
(CarProjectionManager) mCar.getCarManager(Car.PROJECTION_SERVICE);
+ mCarUxRestrictionsManager =
+ (CarUxRestrictionsManager)
+ mCar.getCarManager(Car.CAR_UX_RESTRICTION_SERVICE);
if (mCarProjectionManager != null) {
mCarProjectionManager.registerProjectionStatusListener(mListener);
}
+ if (mCarUxRestrictionsManager != null) {
+ mCarUxRestrictionsManager.registerListener(mCarUxRestrictionListener);
+ mCarUxRestrictionListener.onUxRestrictionsChanged(
+ mCarUxRestrictionsManager.getCurrentCarUxRestrictions());
+ }
});
}
@@ -76,6 +94,9 @@ public class ProjectionStateListener {
if (mCarProjectionManager != null) {
mCarProjectionManager.unregisterProjectionStatusListener(mListener);
}
+ if (mCarUxRestrictionsManager != null) {
+ mCarUxRestrictionsManager.unregisterListener();
+ }
if (mCar != null) {
mCar.disconnect();
mCar = null;
@@ -84,6 +105,15 @@ public class ProjectionStateListener {
mProjectionDetails = Collections.emptyList();
}
+ /** Gets the UxrRestrictions */
+ @NonNull
+ public final LiveData<CarUxRestrictions> getUxrRestrictions() {
+ if (mUxRestrictions.getValue() == null && mCarUxRestrictionsManager != null) {
+ mUxRestrictions.postValue(mCarUxRestrictionsManager.getCurrentCarUxRestrictions());
+ }
+ return mUxRestrictions;
+ }
+
/**
* Returns {@code true} if the input device currently has a projection app running in the
* foreground.
diff --git a/src/com/android/car/messenger/core/util/ConversationUtil.java b/src/com/android/car/messenger/core/util/ConversationUtil.java
index b56cbce..750e21e 100644
--- a/src/com/android/car/messenger/core/util/ConversationUtil.java
+++ b/src/com/android/car/messenger/core/util/ConversationUtil.java
@@ -16,6 +16,7 @@
package com.android.car.messenger.core.util;
+import static com.android.car.messenger.core.shared.MessageConstants.LAST_REPLY_TEXT_EXTRA;
import static com.android.car.messenger.core.shared.MessageConstants.LAST_REPLY_TIMESTAMP_EXTRA;
import static java.lang.Math.max;
@@ -25,9 +26,11 @@ import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.android.car.messenger.R;
import com.android.car.messenger.common.Conversation;
import com.android.car.messenger.common.Conversation.Message;
import com.android.car.messenger.common.Conversation.Message.MessageStatus;
+import com.android.car.messenger.core.interfaces.AppFactory;
/** Conversation Util class for the {@link Conversation} DAO */
public class ConversationUtil {
@@ -42,7 +45,7 @@ public class ConversationUtil {
return 0L;
}
long replyTimestamp = conversation.getExtras().getLong(LAST_REPLY_TIMESTAMP_EXTRA, 0L);
- Message lastMessage = getLastMessage(conversation);
+ Message lastMessage = getLastIncomingMessage(conversation);
long lastMessageTimestamp = lastMessage == null ? 0L : lastMessage.getTimestamp();
return max(replyTimestamp, lastMessageTimestamp);
}
@@ -54,7 +57,7 @@ public class ConversationUtil {
}
long lastReplyTimestamp = getReplyTimestamp(conversation);
long lastMessageTimestamp = 0L;
- Message lastMessageGroup = ConversationUtil.getLastMessage(conversation);
+ Message lastMessageGroup = ConversationUtil.getLastIncomingMessage(conversation);
if (lastMessageGroup != null) {
lastMessageTimestamp = lastMessageGroup.getTimestamp();
}
@@ -62,11 +65,11 @@ public class ConversationUtil {
}
/**
- * Returns the last message in the conversation, or null if {@link Conversation#getMessages} is
- * empty
+ * Returns the last incoming message in the conversation, or null if {@link
+ * Conversation#getMessages} is empty
*/
@Nullable
- public static Message getLastMessage(@Nullable Conversation conversation) {
+ public static Message getLastIncomingMessage(@Nullable Conversation conversation) {
if (conversation == null || conversation.getMessages().isEmpty()) {
return null;
}
@@ -75,12 +78,31 @@ public class ConversationUtil {
}
/**
+ * Returns the last incoming message in the conversation, or null if {@link
+ * Conversation#getMessages} is empty
+ */
+ @NonNull
+ public static String getLastMessagePreview(@Nullable Conversation conversation) {
+ Message lastIncomingMessage = getLastIncomingMessage(conversation);
+ if (isReplied(conversation)) {
+ String lastReply = getLastReply(conversation);
+ if (lastReply == null) {
+ return AppFactory.get().getContext().getString(R.string.replied);
+ }
+ return lastReply;
+ } else if (lastIncomingMessage != null) {
+ return lastIncomingMessage.getText();
+ }
+ return "";
+ }
+
+ /**
* Gets the conversation status of the last messages Returns {@link
* MessageStatus#MESSAGE_STATUS_NONE} when no known message status or last message is a reply
*/
@MessageStatus
public static int getConversationStatus(@Nullable Conversation conversation) {
- Message lastMessage = getLastMessage(conversation);
+ Message lastMessage = getLastIncomingMessage(conversation);
return isReplied(conversation) || lastMessage == null
? MessageStatus.MESSAGE_STATUS_NONE
: lastMessage.getMessageStatus();
@@ -93,15 +115,16 @@ public class ConversationUtil {
* to. If no extra is passed, a new one will be created. The final extras will be added to
* the {@link Conversation#getExtras()}
*/
- public static void setReplyTimestampAsAnExtra(
+ public static void setReplyAsAnExtra(
@NonNull Conversation.Builder conversationBuilder,
@Nullable Bundle extras,
- long lastReplyTimestamp) {
- if (lastReplyTimestamp > 0L) {
+ @Nullable Message lastReply) {
+ if (lastReply != null) {
if (extras == null) {
extras = new Bundle();
}
- extras.putLong(LAST_REPLY_TIMESTAMP_EXTRA, lastReplyTimestamp);
+ extras.putLong(LAST_REPLY_TIMESTAMP_EXTRA, lastReply.getTimestamp());
+ extras.putString(LAST_REPLY_TEXT_EXTRA, lastReply.getText());
conversationBuilder.setExtras(extras);
}
}
@@ -113,4 +136,13 @@ public class ConversationUtil {
}
return conversation.getExtras().getLong(LAST_REPLY_TIMESTAMP_EXTRA, 0L);
}
+
+ /** Gets last reply, if any */
+ @Nullable
+ private static String getLastReply(@Nullable Conversation conversation) {
+ if (conversation == null) {
+ return null;
+ }
+ return conversation.getExtras().getString(LAST_REPLY_TEXT_EXTRA, "");
+ }
}
diff --git a/src/com/android/car/messenger/core/util/VoiceUtil.java b/src/com/android/car/messenger/core/util/VoiceUtil.java
index ca39595..bd7997b 100644
--- a/src/com/android/car/messenger/core/util/VoiceUtil.java
+++ b/src/com/android/car/messenger/core/util/VoiceUtil.java
@@ -20,6 +20,7 @@ import static com.android.car.assist.CarVoiceInteractionSession.KEY_CONVERSATION
import static com.android.car.assist.CarVoiceInteractionSession.KEY_DEVICE_ADDRESS;
import static com.android.car.assist.CarVoiceInteractionSession.KEY_DEVICE_NAME;
import static com.android.car.assist.CarVoiceInteractionSession.KEY_NOTIFICATION;
+import static com.android.car.assist.CarVoiceInteractionSession.KEY_PHONE_NUMBER;
import static com.android.car.assist.CarVoiceInteractionSession.KEY_SEND_PENDING_INTENT;
import static com.android.car.assist.CarVoiceInteractionSession.VOICE_ACTION_READ_CONVERSATION;
import static com.android.car.assist.CarVoiceInteractionSession.VOICE_ACTION_READ_NOTIFICATION;
@@ -227,10 +228,22 @@ public class VoiceUtil {
int requestCode =
(conversationKey == null) ? action.hashCode() : conversationKey.hashCode();
return PendingIntent.getForegroundService(
- context,
- requestCode,
- intent,
- PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_ONE_SHOT);
+ context, requestCode, intent, PendingIntent.FLAG_MUTABLE);
+ }
+
+ /** Sends a reply, meant to be used from a caller originating from voice input. */
+ public static void directSend(Intent intent) {
+ final CharSequence phoneNumber = intent.getCharSequenceExtra(KEY_PHONE_NUMBER);
+ final String iccId = intent.getStringExtra(KEY_DEVICE_ADDRESS);
+ final CharSequence message = intent.getCharSequenceExtra(Intent.EXTRA_TEXT);
+ if (iccId == null || phoneNumber == null || TextUtils.isEmpty(message)) {
+ L.e("Dropping voice reply. Received no icc id, phone Number and/or empty message!");
+ return;
+ }
+ L.d("Sending a message to specified phone number");
+ AppFactory.get()
+ .getDataModel()
+ .sendMessage(iccId, phoneNumber.toString(), message.toString());
}
/** Sends a reply, meant to be used from a caller originating from voice input. */
diff --git a/src/com/android/car/messenger/impl/datamodels/ContentProviderLiveData.java b/src/com/android/car/messenger/impl/datamodels/ContentProviderLiveData.java
index 59e7849..fbd6d14 100644
--- a/src/com/android/car/messenger/impl/datamodels/ContentProviderLiveData.java
+++ b/src/com/android/car/messenger/impl/datamodels/ContentProviderLiveData.java
@@ -16,13 +16,11 @@
package com.android.car.messenger.impl.datamodels;
-import androidx.lifecycle.MediatorLiveData;
import android.content.Context;
import android.database.ContentObserver;
import android.net.Uri;
-
import androidx.annotation.NonNull;
-
+import androidx.lifecycle.MediatorLiveData;
import com.android.car.messenger.core.interfaces.AppFactory;
/**
diff --git a/src/com/android/car/messenger/impl/datamodels/ConversationsPerDeviceFetchManager.java b/src/com/android/car/messenger/impl/datamodels/ConversationsPerDeviceFetchManager.java
index 7618b15..379e922 100644
--- a/src/com/android/car/messenger/impl/datamodels/ConversationsPerDeviceFetchManager.java
+++ b/src/com/android/car/messenger/impl/datamodels/ConversationsPerDeviceFetchManager.java
@@ -20,21 +20,18 @@ import static android.provider.Telephony.MmsSms.CONTENT_CONVERSATIONS_URI;
import static android.provider.Telephony.TextBasedSmsColumns.SUBSCRIPTION_ID;
import static android.provider.Telephony.TextBasedSmsColumns.THREAD_ID;
-import androidx.lifecycle.LiveData;
-import androidx.lifecycle.MediatorLiveData;
-import androidx.lifecycle.Observer;
import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
import android.telephony.SubscriptionInfo;
-
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MediatorLiveData;
+import androidx.lifecycle.Observer;
import com.android.car.messenger.core.interfaces.AppFactory;
import com.android.car.messenger.core.models.UserAccount;
-
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
diff --git a/src/com/android/car/messenger/impl/datamodels/NewMessageLiveData.java b/src/com/android/car/messenger/impl/datamodels/NewMessageLiveData.java
index 8f6fb64..05e5c57 100644
--- a/src/com/android/car/messenger/impl/datamodels/NewMessageLiveData.java
+++ b/src/com/android/car/messenger/impl/datamodels/NewMessageLiveData.java
@@ -32,9 +32,9 @@ import com.android.car.messenger.common.Conversation;
import com.android.car.messenger.core.interfaces.AppFactory;
import com.android.car.messenger.core.models.UserAccount;
import com.android.car.messenger.core.shared.MessageConstants;
+import com.android.car.messenger.core.util.CarStateListener;
import com.android.car.messenger.core.util.ConversationUtil;
import com.android.car.messenger.core.util.L;
-import com.android.car.messenger.impl.common.ProjectionStateListener;
import java.time.Instant;
import java.util.ArrayList;
@@ -62,8 +62,7 @@ public class NewMessageLiveData extends ContentProviderLiveData<Conversation> {
+ " = %d";
@NonNull
- private final ProjectionStateListener mProjectionStateListener =
- new ProjectionStateListener(AppFactory.get().getContext());
+ private final CarStateListener mCarStateListener = AppFactory.get().getCarStateListener();
NewMessageLiveData() {
super(Telephony.Sms.CONTENT_URI, Telephony.Mms.CONTENT_URI, Telephony.MmsSms.CONTENT_URI);
@@ -164,6 +163,6 @@ public class NewMessageLiveData extends ContentProviderLiveData<Conversation> {
}
private boolean hasProjectionInForeground(@NonNull UserAccount userAccount) {
- return mProjectionStateListener.isProjectionInActiveForeground(userAccount.getIccId());
+ return mCarStateListener.isProjectionInActiveForeground(userAccount.getIccId());
}
}
diff --git a/src/com/android/car/messenger/impl/datamodels/TelephonyDataModel.java b/src/com/android/car/messenger/impl/datamodels/TelephonyDataModel.java
index b62c678..40568cc 100644
--- a/src/com/android/car/messenger/impl/datamodels/TelephonyDataModel.java
+++ b/src/com/android/car/messenger/impl/datamodels/TelephonyDataModel.java
@@ -18,17 +18,15 @@ package com.android.car.messenger.impl.datamodels;
import static com.android.car.messenger.core.shared.MessageConstants.KEY_MUTED_CONVERSATIONS;
+import androidx.lifecycle.Transformations;
import android.content.ContentValues;
import android.content.Context;
import android.content.SharedPreferences;
import android.net.Uri;
import android.provider.Telephony;
import android.telephony.SmsManager;
-
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
-import androidx.lifecycle.Transformations;
-
import com.android.car.messenger.common.Conversation;
import com.android.car.messenger.core.interfaces.AppFactory;
import com.android.car.messenger.core.interfaces.DataModel;
@@ -36,7 +34,6 @@ import com.android.car.messenger.core.models.UserAccount;
import com.android.car.messenger.core.util.L;
import com.android.car.messenger.impl.datamodels.UserAccountLiveData.UserAccountChangeList;
import com.android.car.messenger.impl.datamodels.util.CursorUtils;
-
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
@@ -111,6 +108,29 @@ public class TelephonyDataModel implements DataModel {
/* deliveryIntent= */ null);
}
+ @Override
+ public void sendMessage(int accountId, @NonNull String phoneNumber, @NonNull String message) {
+ L.d("Sending a message to a phone number");
+ SmsManager.getSmsManagerForSubscriptionId(accountId)
+ .sendTextMessage(
+ phoneNumber,
+ /* scAddress= */ null,
+ message,
+ /* sentIntent= */ null,
+ /* deliveryIntent= */ null);
+ }
+
+ @Override
+ public void sendMessage(
+ @NonNull String iccId, @NonNull String phoneNumber, @NonNull String message) {
+ UserAccount userAccount = UserAccountLiveData.getUserAccount(iccId);
+ if (userAccount == null) {
+ L.d("Could not find User Account with specified iccId. Unable to send message");
+ return;
+ }
+ sendMessage(userAccount.getId(), phoneNumber, message);
+ }
+
@NonNull
@Override
public LiveData<String> onConversationRemoved() {
diff --git a/src/com/android/car/messenger/impl/datamodels/UserAccountLiveData.java b/src/com/android/car/messenger/impl/datamodels/UserAccountLiveData.java
index 1a95a2f..feaecea 100644
--- a/src/com/android/car/messenger/impl/datamodels/UserAccountLiveData.java
+++ b/src/com/android/car/messenger/impl/datamodels/UserAccountLiveData.java
@@ -15,20 +15,17 @@
*/
package com.android.car.messenger.impl.datamodels;
-import androidx.lifecycle.LiveData;
import android.content.Context;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener;
import android.telephony.TelephonyManager;
-
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-
+import androidx.lifecycle.LiveData;
import com.android.car.messenger.core.interfaces.AppFactory;
import com.android.car.messenger.core.models.UserAccount;
import com.android.car.messenger.impl.datamodels.UserAccountLiveData.UserAccountChangeList;
-
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
@@ -82,7 +79,7 @@ public class UserAccountLiveData extends LiveData<UserAccountChangeList> {
/**
* Refresh the user accounts. Updates listeners if a change is found. Useful to call when
* something occurs that indicates a change in accounts, such as empty messages. This is useful
- * as there are occasions when the subscription on change listener is not called after a
+ * as t here are occasions when the subscription on change listener is not called after a
* subscription is deleted.
*/
public void refresh() {
diff --git a/src/com/android/car/messenger/impl/datamodels/util/ConversationFetchUtil.java b/src/com/android/car/messenger/impl/datamodels/util/ConversationFetchUtil.java
index 42b97e4..ddedbe2 100644
--- a/src/com/android/car/messenger/impl/datamodels/util/ConversationFetchUtil.java
+++ b/src/com/android/car/messenger/impl/datamodels/util/ConversationFetchUtil.java
@@ -56,19 +56,18 @@ public class ConversationFetchUtil {
// messages to read: first get unread messages
List<Conversation.Message> messagesToRead = MessageUtils.getUnreadMessages(messagesCursor);
int unreadCount = messagesToRead.size();
- long lastReplyTimestamp = 0L;
+ Conversation.Message lastReply = null;
// if no unread messages, get read messages
if (messagesToRead.isEmpty()) {
- Pair<List<Conversation.Message>, Long> readMessagesAndReplyTimestamp =
+ Pair<List<Conversation.Message>, Conversation.Message> readMessagesAndReplyTimestamp =
MessageUtils.getReadMessagesAndReplyTimestamp(messagesCursor);
messagesToRead = readMessagesAndReplyTimestamp.first;
- lastReplyTimestamp = readMessagesAndReplyTimestamp.second;
+ lastReply = readMessagesAndReplyTimestamp.second;
}
conversationBuilder.setMessages(messagesToRead).setUnreadCount(unreadCount);
- ConversationUtil.setReplyTimestampAsAnExtra(
- conversationBuilder, /* extras= */ null, lastReplyTimestamp);
+ ConversationUtil.setReplyAsAnExtra(conversationBuilder, /* extras= */ null, lastReply);
return conversationBuilder.build();
}
diff --git a/src/com/android/car/messenger/impl/datamodels/util/MessageUtils.java b/src/com/android/car/messenger/impl/datamodels/util/MessageUtils.java
index 08dc1b3..8da6318 100644
--- a/src/com/android/car/messenger/impl/datamodels/util/MessageUtils.java
+++ b/src/com/android/car/messenger/impl/datamodels/util/MessageUtils.java
@@ -68,14 +68,15 @@ public final class MessageUtils {
}
/**
- * Gets Read Messages and Reply Timestamp.
+ * Gets Read Messages and Last Reply
*
* @param messagesCursor MessageCursor in descending order
*/
@NonNull
- public static Pair<List<Message>, Long> getReadMessagesAndReplyTimestamp(
+ public static Pair<List<Message>, Message> getReadMessagesAndReplyTimestamp(
@Nullable Cursor messagesCursor) {
List<Message> readMessages = new ArrayList<>();
+ AtomicReference<Message> replyMessage = new AtomicReference<>();
AtomicReference<Long> lastReply = new AtomicReference<>(0L);
MessageUtils.forEachDesc(
messagesCursor,
@@ -89,6 +90,7 @@ public final class MessageUtils {
if (message.getMessageType() == MessageType.MESSAGE_TYPE_SENT) {
if (lastReply.get() < message.getTimestamp()) {
lastReply.set(message.getTimestamp());
+ replyMessage.set(message);
}
return readMessages.isEmpty();
}
@@ -101,7 +103,7 @@ public final class MessageUtils {
return false;
});
readMessages.sort(comparingLong(Message::getTimestamp));
- return new Pair<>(readMessages, lastReply.get());
+ return new Pair<>(readMessages, replyMessage.get());
}
/**