summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorUchenna Okoye <uokoye@google.com>2021-03-18 21:52:05 +0000
committerUchenna Okoye <uokoye@google.com>2021-03-18 21:52:05 +0000
commit010c6e0449a5a171e76e30a7593ca9acbcbd4ade (patch)
tree4426f3813862c26564d0ea826b84fce222c8dad8
parentc3c585e07c255d3af5779b5daff7161d9cf1f6fc (diff)
downloadMessenger-010c6e0449a5a171e76e30a7593ca9acbcbd4ade.tar.gz
Revert "Project import generated by Copybara."
This reverts commit c3c585e07c255d3af5779b5daff7161d9cf1f6fc. Reason for revert: Need to fix authoring Change-Id: I513c3785dd0d3719fe1836167a92e2121b2c166b
-rw-r--r--AndroidManifest.xml157
-rw-r--r--res/anim/trans_bottom_in.xml32
-rw-r--r--res/anim/trans_bottom_out.xml32
-rw-r--r--res/color/uxr_button_text_color_selector.xml22
-rw-r--r--res/drawable/car_ui_icon_reply.xml27
-rw-r--r--res/drawable/car_ui_icon_toggle_mute.xml33
-rw-r--r--res/drawable/hero_button_background.xml24
-rw-r--r--res/drawable/ic_launcher_icon.xml41
-rw-r--r--res/drawable/ic_message.xml17
-rw-r--r--res/drawable/ic_person.xml26
-rw-r--r--res/drawable/ic_voice_out.xml32
-rw-r--r--res/drawable/list_divider.xml24
-rw-r--r--res/layout/conversation_list_item.xml169
-rw-r--r--res/layout/loading_info_view.xml60
-rw-r--r--res/layout/loading_list_fragment.xml24
-rw-r--r--res/layout/loading_progress_view.xml31
-rw-r--r--res/values/attrs.xml22
-rw-r--r--res/values/colors.xml67
-rw-r--r--res/values/config.xml43
-rw-r--r--res/values/dimens.xml82
-rw-r--r--res/values/integers.xml (renamed from res/drawable/ic_play.xml)18
-rw-r--r--res/values/strings.xml102
-rw-r--r--res/values/styles.xml105
-rw-r--r--res/values/themes.xml28
-rw-r--r--src/com/android/car/messenger/MessageNotificationDelegate.java487
-rw-r--r--src/com/android/car/messenger/MessengerActivity.java45
-rw-r--r--src/com/android/car/messenger/MessengerService.java263
-rw-r--r--src/com/android/car/messenger/MmsReceiver.java (renamed from src/com/android/car/messenger/impl/receivers/MmsReceiver.java)16
-rw-r--r--src/com/android/car/messenger/SmsDatabaseHandler.java220
-rw-r--r--src/com/android/car/messenger/SmsReceiver.java (renamed from src/com/android/car/messenger/impl/receivers/SmsReceiver.java)16
-rw-r--r--src/com/android/car/messenger/bluetooth/BluetoothHelper.java64
-rw-r--r--src/com/android/car/messenger/bluetooth/BluetoothMonitor.java329
-rw-r--r--src/com/android/car/messenger/core/interfaces/AppFactory.java70
-rw-r--r--src/com/android/car/messenger/core/interfaces/DataModel.java102
-rw-r--r--src/com/android/car/messenger/core/models/ConnectionStatus.java31
-rw-r--r--src/com/android/car/messenger/core/models/UserAccount.java114
-rw-r--r--src/com/android/car/messenger/core/service/MessengerService.java181
-rw-r--r--src/com/android/car/messenger/core/service/OnBootReceiver.java39
-rw-r--r--src/com/android/car/messenger/core/shared/MessageConstants.java55
-rw-r--r--src/com/android/car/messenger/core/shared/NotificationHandler.java140
-rw-r--r--src/com/android/car/messenger/core/ui/base/MessageListBaseFragment.java125
-rw-r--r--src/com/android/car/messenger/core/ui/conversationlist/ConversationItemAdapter.java82
-rw-r--r--src/com/android/car/messenger/core/ui/conversationlist/ConversationItemViewHolder.java155
-rw-r--r--src/com/android/car/messenger/core/ui/conversationlist/ConversationListFragment.java172
-rw-r--r--src/com/android/car/messenger/core/ui/conversationlist/ConversationListViewModel.java89
-rw-r--r--src/com/android/car/messenger/core/ui/conversationlist/UIConversationItem.java130
-rw-r--r--src/com/android/car/messenger/core/ui/conversationlist/UIConversationItemConverter.java94
-rw-r--r--src/com/android/car/messenger/core/ui/conversationlist/UIConversationLog.java77
-rw-r--r--src/com/android/car/messenger/core/ui/launcher/MessageLauncherActivity.java89
-rw-r--r--src/com/android/car/messenger/core/ui/launcher/MessageLauncherViewModel.java61
-rw-r--r--src/com/android/car/messenger/core/ui/shared/CircularOutputlineProvider.java51
-rw-r--r--src/com/android/car/messenger/core/ui/shared/LetterTileDrawable.java300
-rw-r--r--src/com/android/car/messenger/core/ui/shared/LoadingFrameLayout.java321
-rw-r--r--src/com/android/car/messenger/core/ui/shared/ViewUtils.java34
-rw-r--r--src/com/android/car/messenger/core/util/ConversationUtil.java116
-rw-r--r--src/com/android/car/messenger/core/util/L.java110
-rw-r--r--src/com/android/car/messenger/core/util/VoiceUtil.java257
-rw-r--r--src/com/android/car/messenger/impl/AppFactoryImpl.java105
-rw-r--r--src/com/android/car/messenger/impl/CarMessengerApp.java65
-rw-r--r--src/com/android/car/messenger/impl/common/ProjectionStateListener.java166
-rw-r--r--src/com/android/car/messenger/impl/datamodels/ContentProviderLiveData.java78
-rw-r--r--src/com/android/car/messenger/impl/datamodels/ConversationItemLiveData.java284
-rw-r--r--src/com/android/car/messenger/impl/datamodels/ConversationsPerDeviceLiveData.java163
-rw-r--r--src/com/android/car/messenger/impl/datamodels/TelephonyDataModel.java315
-rw-r--r--src/com/android/car/messenger/impl/datamodels/UserAccountLiveData.java188
-rw-r--r--src/com/android/car/messenger/impl/datamodels/util/AvatarUtil.java412
-rw-r--r--src/com/android/car/messenger/impl/datamodels/util/ContactUtils.java176
-rw-r--r--src/com/android/car/messenger/impl/datamodels/util/CursorUtils.java119
-rw-r--r--src/com/android/car/messenger/impl/datamodels/util/MessageUtils.java185
-rw-r--r--src/com/android/car/messenger/impl/datamodels/util/MmsSmsMessage.java33
-rw-r--r--src/com/android/car/messenger/impl/datamodels/util/MmsUtils.java99
-rw-r--r--src/com/android/car/messenger/impl/datamodels/util/SmsUtils.java60
-rw-r--r--src/com/android/car/messenger/log/L.java126
-rw-r--r--tests/robotests/Android.bp22
-rw-r--r--tests/robotests/AndroidManifest.xml (renamed from res/layout/list_fragment.xml)15
-rw-r--r--tests/robotests/config/robolectric.properties1
-rw-r--r--tests/robotests/readme.md6
-rw-r--r--tests/robotests/src/com/android/car/messenger/MessengerDelegateTest.java168
-rw-r--r--tests/robotests/src/com/android/car/messenger/bluetooth/BluetoothHelperTest.java137
-rw-r--r--tests/robotests/src/com/android/car/messenger/bluetooth/BluetoothMonitorTest.java56
-rw-r--r--tests/robotests/src/com/android/car/messenger/testutils/ShadowBluetoothAdapter.java67
81 files changed, 2259 insertions, 6440 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index b24137a..5438926 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -1,136 +1,91 @@
<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright (C) 2020 The Android Open Source Project
+<!-- Copyright (C) 2017 The Android Open Source Project
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
+ http://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- -->
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.android.car.messenger">
-
- <application
- android:name="com.android.car.messenger.impl.CarMessengerApp"
- android:icon="@drawable/ic_launcher_icon"
- android:label="@string/app_name"
- android:screenOrientation="landscape"
- android:supportsRtl="true"
- android:theme="@style/Theme.CarUi.WithToolbar">
-
- <activity
- android:name=".core.ui.launcher.MessageLauncherActivity"
- android:exported="true"
- android:screenOrientation="landscape">
- <intent-filter>
- <action android:name="android.intent.action.MAIN" />
- <category android:name="android.intent.category.LAUNCHER" />
- </intent-filter>
-
- <intent-filter>
- <action android:name="android.intent.action.MAIN" />
- <category android:name="android.intent.category.APP_MESSAGING" />
- </intent-filter>
+ package="com.android.car.messenger">
+
+ <uses-sdk android:minSdkVersion="26" android:targetSdkVersion="29"/>
+
+ <uses-permission android:name="android.permission.BLUETOOTH"/>
+ <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
+ <uses-permission android:name="android.permission.READ_CONTACTS"/>
+ <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
+ <uses-permission android:name="android.permission.RECEIVE_SMS"/>
+ <uses-permission android:name="android.permission.SEND_SMS"/>
+ <uses-permission android:name="android.permission.READ_SMS"/>
+ <uses-permission android:name="android.permission.WRITE_SMS"/>
+ <uses-permission android:name="android.car.permission.ACCESS_CAR_PROJECTION_STATUS"/>
+
+ <application android:label="@string/app_name">
+ <service android:name=".MessengerService"
+ android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE"
+ android:exported="true" >
<intent-filter>
- <action android:name="android.intent.action.VIEW" />
- <action android:name="android.intent.action.SENDTO" />
-
+ <action android:name="android.intent.action.RESPOND_VIA_MESSAGE" />
<category android:name="android.intent.category.DEFAULT" />
- <category android:name="android.intent.category.BROWSABLE" />
-
<data android:scheme="sms" />
<data android:scheme="smsto" />
- </intent-filter>
- <intent-filter>
- <action android:name="android.intent.action.VIEW" />
- <action android:name="android.intent.action.SENDTO" />
-
- <category android:name="android.intent.category.DEFAULT" />
- <category android:name="android.intent.category.BROWSABLE" />
-
<data android:scheme="mms" />
<data android:scheme="mmsto" />
</intent-filter>
- <meta-data
- android:name="distractionOptimized"
- android:value="true" />
- </activity>
+ </service>
<!-- BroadcastReceiver that listens for incoming SMS messages -->
- <receiver
- android:name=".impl.receivers.MmsReceiver"
- android:exported="false"
- android:permission="android.permission.BROADCAST_WAP_PUSH">
+ <receiver android:name=".SmsReceiver"
+ android:exported="true"
+ android:permission="android.permission.BROADCAST_SMS">
<intent-filter>
- <action android:name="android.provider.Telephony.WAP_PUSH_DELIVER" />
- <data android:mimeType="application/vnd.wap.mms-message" />
+ <action android:name="android.provider.Telephony.SMS_DELIVER" />
+ <action android:name="android.provider.Telephony.SMS_RECEIVED" />
</intent-filter>
</receiver>
<!-- BroadcastReceiver that listens for incoming MMS messages -->
- <receiver
- android:name=".core.service.OnBootReceiver"
- android:enabled="true"
- android:exported="false"
- android:permission="android.permission.RECEIVE_BOOT_COMPLETED">
+ <receiver android:name=".MmsReceiver"
+ android:exported="true"
+ android:permission="android.permission.BROADCAST_WAP_PUSH">
<intent-filter>
- <action android:name="android.intent.action.BOOT_COMPLETED" />
+ <action android:name="android.provider.Telephony.WAP_PUSH_DELIVER" />
+ <data android:mimeType="application/vnd.wap.mms-message" />
</intent-filter>
</receiver>
- <!-- BroadcastReceiver for car booting -->
- <receiver
- android:name=".impl.receivers.SmsReceiver"
- android:exported="false"
- android:permission="android.permission.BROADCAST_SMS">
+ <activity android:name=".MessengerActivity" android:exported="true">
+ <meta-data android:name="distractionOptimized" android:value="true"/>
<intent-filter>
- <action android:name="android.provider.Telephony.SMS_DELIVER" />
- <action android:name="android.provider.Telephony.SMS_RECEIVED" />
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.APP_MESSAGING"/>
</intent-filter>
- </receiver>
-
- <service
- android:name=".core.service.MessengerService"
- android:exported="false"
- android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE">
<intent-filter>
- <action android:name="android.intent.action.RESPOND_VIA_MESSAGE" />
+ <action android:name="android.intent.action.VIEW"/>
+ <action android:name="android.intent.action.SENDTO" />
<category android:name="android.intent.category.DEFAULT" />
-
+ <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="sms" />
<data android:scheme="smsto" />
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <action android:name="android.intent.action.SENDTO" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="mms" />
<data android:scheme="mmsto" />
</intent-filter>
- </service>
-
+ </activity>
</application>
-
- <uses-permission android:name="android.permission.SEND_SMS" />
- <uses-permission android:name="android.permission.RECEIVE_SMS" />
- <uses-permission android:name="android.permission.READ_CONTACTS" />
- <uses-permission android:name="android.permission.READ_PHONE_STATE" />
- <uses-permission android:name="android.permission.READ_SMS" />
- <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
- <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
- <!-- Permissions required to know the current projection app status. -->
- <uses-permission android:name="android.car.permission.ACCESS_CAR_PROJECTION_STATUS" />
- <!-- Permissions required to retrieve the SubscriptionInfo#getIccId.
- This maps to the bluetooth address and is necessary
- for various functions such as Assistant device disambiguation,
- checking the projection state and more etc.
- -->
- <uses-permission android:name="android.permission.READ_PRIVILEGED_PHONE_STATE" />
-
- <uses-sdk
- android:minSdkVersion="30"
- android:targetSdkVersion="30" />
</manifest>
diff --git a/res/anim/trans_bottom_in.xml b/res/anim/trans_bottom_in.xml
new file mode 100644
index 0000000..0635de3
--- /dev/null
+++ b/res/anim/trans_bottom_in.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <objectAnimator
+ android:interpolator="@android:interpolator/decelerate_quint"
+ android:valueFrom="100dp"
+ android:valueTo="0dp"
+ android:valueType="floatType"
+ android:propertyName="translationY"
+ android:duration="@integer/anim_time" />
+ <objectAnimator
+ android:interpolator="@android:interpolator/decelerate_quint"
+ android:valueFrom="0.0"
+ android:valueTo="1.0"
+ android:valueType="floatType"
+ android:propertyName="alpha"
+ android:duration="@integer/anim_time" />
+</set> \ No newline at end of file
diff --git a/res/anim/trans_bottom_out.xml b/res/anim/trans_bottom_out.xml
new file mode 100644
index 0000000..81295e8
--- /dev/null
+++ b/res/anim/trans_bottom_out.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <objectAnimator
+ android:interpolator="@android:interpolator/decelerate_quint"
+ android:valueFrom="0dp"
+ android:valueTo="-100dp"
+ android:valueType="floatType"
+ android:propertyName="translationY"
+ android:duration="@integer/anim_time" />
+ <objectAnimator
+ android:interpolator="@android:interpolator/decelerate_quint"
+ android:valueFrom="1.0"
+ android:valueTo="0.0"
+ android:valueType="floatType"
+ android:propertyName="alpha"
+ android:duration="@integer/anim_time" />
+</set> \ No newline at end of file
diff --git a/res/color/uxr_button_text_color_selector.xml b/res/color/uxr_button_text_color_selector.xml
deleted file mode 100644
index 8c0f6c8..0000000
--- a/res/color/uxr_button_text_color_selector.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright (C) 2019 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- -->
-
-<selector xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto">
- <item app:state_ux_restricted="true" android:color="@color/uxr_button_text_disabled_color"/>
- <item app:state_ux_restricted="false" android:color="@color/uxr_button_text_color"/>
-</selector>
diff --git a/res/drawable/car_ui_icon_reply.xml b/res/drawable/car_ui_icon_reply.xml
deleted file mode 100644
index 3cdb44a..0000000
--- a/res/drawable/car_ui_icon_reply.xml
+++ /dev/null
@@ -1,27 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
- ~ Copyright (C) 2020 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- -->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="48dp"
- android:height="37.333332dp"
- android:viewportHeight="28"
- android:viewportWidth="36">
- <path
- android:fillColor="@color/secondary_text_color"
- android:pathData="M26,10H7.66L12,5.66L14.82,2.84L12,0L0,12L12,24L14.82,21.18L12,18.34L7.66,
- 14H26C29.3,14 32,16.7 32,20V28H36V20C36,14.48 31.52,10 26,10Z" />
-</vector>
-
diff --git a/res/drawable/car_ui_icon_toggle_mute.xml b/res/drawable/car_ui_icon_toggle_mute.xml
deleted file mode 100644
index faa8cd5..0000000
--- a/res/drawable/car_ui_icon_toggle_mute.xml
+++ /dev/null
@@ -1,33 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2020 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- -->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="48dp"
- android:height="48dp"
- android:viewportHeight="32"
- android:viewportWidth="32">
- <path
- android:fillColor="@color/car_ui_toolbar_menu_item_icon_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,
- 29.7212L2.295,0ZM13.9151,19.9758L10.4081,16.4687H5.8343V13.2364H10.9576L13.9151,
- 16.1939V19.9758ZM17.1475,1.9232L12.9616,6.1091L17.1475,10.2788V1.9232ZM24.4202,
- 14.8525C24.4202,11.9919 22.7717,9.5353 20.3798,8.3394V13.5111L24.0323,17.1636C24.2747,
- 16.4364 24.4202,15.6606 24.4202,14.8525ZM28.4606,14.8525C28.4606,16.7919 27.9596,18.6343
- 27.103,20.2343L29.4626,22.5939C30.8687,20.3475 31.6929,17.697 31.6929,14.8525C31.6929,7.9353
- 26.8606,2.1495 20.3798,0.6788V4.0081C25.0505,5.398 28.4606,9.7293 28.4606,14.8525Z" />
-</vector>
diff --git a/res/drawable/hero_button_background.xml b/res/drawable/hero_button_background.xml
deleted file mode 100644
index 496ecfa..0000000
--- a/res/drawable/hero_button_background.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2019 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<ripple xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="@color/car_card_ripple_background">
- <item>
- <shape android:shape="rectangle">
- <corners android:radius="@dimen/hero_button_corner_radius" />
- <solid android:color="@color/hero_button_background_color" />
- </shape>
- </item>
-</ripple>
diff --git a/res/drawable/ic_launcher_icon.xml b/res/drawable/ic_launcher_icon.xml
deleted file mode 100644
index bf04507..0000000
--- a/res/drawable/ic_launcher_icon.xml
+++ /dev/null
@@ -1,41 +0,0 @@
-<?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="44dp"
- android:height="44dp"
- android:viewportHeight="44"
- android:viewportWidth="44">
- <path
- android:fillColor="#1A73E8"
- android:pathData="M22,44C34.1503,44 44,34.1503 44,22C44,9.8497 34.1503,0 22,0C9.8497,
- 0 0,9.8497 0,22C0,34.1503 9.8497,44 22,44Z" />
- <path
- android:fillColor="#ffffff"
- android:pathData="M30,13H8.995C8.145,13 7.685,13.73 8.245,14.5L11,19.25V27.75C11,29.9225
- 12.59,31.75 14.75,31.75H30C32.16,31.75 34,29.9225 34,27.75V17C34,14.8275 32.16,13 30,13Z" />
- <path
- android:fillColor="#8AB4F8"
- android:pathData="M29.75,19H15.25C14.615,19 14,18.6225 14,18C14,17.3775 14.615,17
- 15.25,17H29.75C30.385,17 31,17.3775 31,18C31,18.6225 30.385,19 29.75,19Z" />
- <path
- android:fillColor="#8AB4F8"
- android:pathData="M29.75,23H15.25C14.615,23 14,22.6225 14,22C14,21.3775 14.615,21
- 15.25,21H29.75C30.385,21 31,21.3775 31,22C31,22.6225 30.385,23 29.75,23Z" />
- <path
- android:fillColor="#8AB4F8"
- android:pathData="M25.75,27H15.25C14.62,27 14,26.6225 14,26C14,25.3775 14.62,25
- 15.25,25H25.75C26.38,25 27,25.3775 27,26C27,26.6225 26.38,27 25.75,27Z" />
-</vector>
diff --git a/res/drawable/ic_message.xml b/res/drawable/ic_message.xml
index 503f790..3f9dbd7 100644
--- a/res/drawable/ic_message.xml
+++ b/res/drawable/ic_message.xml
@@ -15,13 +15,12 @@
limitations under the License.
-->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="48dp"
- android:height="48dp"
+<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"
+ android:viewportWidth="48"
android:viewportHeight="48"
- android:viewportWidth="48">
- <path
- android:fillColor="#FFFFFF"
- android:pathData="M40 4H8C5.79 4 4.02 5.79 4.02 8L4 44l8 -8h28c2.21 0 4 -1.79 4 -4V8c0 -2.21
- -1.79 -4 -4 -4zM12 18h24v4H12v-4zm16 10H12v-4h16v4zm8 -12H12v-4h24v4z" />
-</vector>
+ android:width="48dp"
+ android:height="48dp">
+ <path
+ android:pathData="M40 4H8C5.79 4 4.02 5.79 4.02 8L4 44l8 -8h28c2.21 0 4 -1.79 4 -4V8c0 -2.21 -1.79 -4 -4 -4zM12 18h24v4H12v-4zm16 10H12v-4h16v4zm8 -12H12v-4h24v4z"
+ android:fillColor="#FFFFFF" />
+</vector> \ No newline at end of file
diff --git a/res/drawable/ic_person.xml b/res/drawable/ic_person.xml
deleted file mode 100644
index 258d5a0..0000000
--- a/res/drawable/ic_person.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2020 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- -->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24dp"
- android:height="24dp"
- android:viewportWidth="24.0"
- android:viewportHeight="24.0">
- <path
- android:pathData="M12,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4
- 4,4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z"
- android:fillColor="#000000"/>
-</vector>
diff --git a/res/drawable/ic_voice_out.xml b/res/drawable/ic_voice_out.xml
new file mode 100644
index 0000000..7672029
--- /dev/null
+++ b/res/drawable/ic_voice_out.xml
@@ -0,0 +1,32 @@
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<vector android:height="24dp"
+ android:viewportHeight="48.0"
+ android:viewportWidth="50.0"
+ android:width="24dp"
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="#00796B" android:pathData="M22,0h6v48h-6z"
+ android:strokeColor="#00000000" android:strokeWidth="1"/>
+ <path android:fillColor="#00796B" android:pathData="M33,10h6v28h-6z"
+ android:strokeColor="#00000000" android:strokeWidth="1"/>
+ <path android:fillColor="#00796B" android:pathData="M11,10h6v28h-6z"
+ android:strokeColor="#00000000" android:strokeWidth="1"/>
+ <path android:fillColor="#00796B" android:pathData="M0,19h6v10h-6z"
+ android:strokeColor="#00000000" android:strokeWidth="1"/>
+ <path android:fillColor="#00796B" android:pathData="M44,19h6v10h-6z"
+ android:strokeColor="#00000000" android:strokeWidth="1"/>
+</vector>
diff --git a/res/drawable/list_divider.xml b/res/drawable/list_divider.xml
deleted file mode 100644
index 647392b..0000000
--- a/res/drawable/list_divider.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2020 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- -->
-<inset xmlns:android="http://schemas.android.com/apk/res/android"
- android:insetLeft="@dimen/list_divider_inset"
- android:insetRight="@dimen/list_divider_inset">
- <shape android:shape="rectangle">
- <size android:height="@dimen/list_divider_height" />
- <solid android:color="@color/divider_color" />
- </shape>
-</inset>
diff --git a/res/layout/conversation_list_item.xml b/res/layout/conversation_list_item.xml
deleted file mode 100644
index 6ada6fc..0000000
--- a/res/layout/conversation_list_item.xml
+++ /dev/null
@@ -1,169 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2020 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<androidx.constraintlayout.widget.ConstraintLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- xmlns:tools="http://schemas.android.com/tools"
- android:layout_width="match_parent"
- android:layout_height="@dimen/message_history_item_height"
- tools:background="@color/background_image_30p_black">
-
- <ImageView
- android:id="@+id/unread_indicator"
- android:layout_width="@dimen/unread_icon_size"
- android:layout_height="@dimen/unread_icon_size"
- android:layout_marginEnd="@dimen/unread_icon_marginEnd"
- android:contentDescription="@string/cd_unread"
- android:scaleType="centerCrop"
- app:layout_constraintBottom_toBottomOf="@id/icon"
- app:layout_constraintEnd_toStartOf="@id/icon"
- app:layout_constraintTop_toTopOf="@id/icon"
- tools:src="#66B5FF" />
-
- <ImageView
- android:id="@+id/icon"
- android:layout_width="@dimen/avatar_icon_size"
- android:layout_height="@dimen/avatar_icon_size"
- android:layout_marginStart="@dimen/message_history_item_padding"
- android:contentDescription="@string/cd_conversation_icon"
- android:scaleType="centerCrop"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent"
- 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"
- android:background="?android:attr/selectableItemBackground"
- android:contentDescription="@string/cd_reply_action_button"
- android:scaleType="center"
- android:src="@drawable/car_ui_icon_reply"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toStartOf="@id/mute_action_button"
- app:layout_constraintStart_toEndOf="@id/guideline_end"
- app:layout_constraintTop_toTopOf="parent" />
-
- <ImageView
- android:id="@+id/mute_action_button"
- android:layout_width="0dp"
- android:layout_height="match_parent"
- android:background="?android:attr/selectableItemBackground"
- android:contentDescription="@string/cd_mute_button"
- android:scaleType="center"
- 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_constraintTop_toTopOf="parent" />
-
- <TextView
- android:id="@+id/title"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_marginEnd="@dimen/message_history_text_margin_end"
- android:singleLine="true"
- android:theme="@style/Theme.Messaging.BidiText"
- app:layout_constraintBottom_toTopOf="@+id/text"
- app:layout_constraintEnd_toEndOf="@id/guideline_end"
- app:layout_constraintStart_toStartOf="@id/guideline_begin"
- app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintVertical_chainStyle="packed" />
-
- <TextView
- android:id="@+id/time_text"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginStart="@dimen/message_history_icons_margin"
- android:singleLine="true"
- app:layout_constraintStart_toEndOf="@id/last_action_icon_view"
- app:layout_constraintTop_toBottomOf="@id/title"
- tools:text="14:02 PM" />
-
- <TextView
- android:id="@+id/dot"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginStart="@dimen/message_history_icons_margin"
- 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" />
-
- <TextView
- android:id="@id/text"
- 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:singleLine="true"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toEndOf="@id/guideline_end"
- app:layout_constraintStart_toEndOf="@id/dot"
- app:layout_constraintTop_toBottomOf="@id/title"
- tools:text="Replied" />
-
- <View
- android:id="@+id/play_action_touch_view"
- android:layout_width="0dp"
- android:layout_height="match_parent"
- android:background="?android:attr/selectableItemBackground"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toStartOf="@id/guideline_end"
- app:layout_constraintHorizontal_bias="0.0"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintVertical_bias="0.0" />
-
- <View
- android:id="@+id/divider"
- android:layout_width="@dimen/vertical_divider_width"
- android:layout_height="match_parent"
- android:layout_marginBottom="@dimen/vertical_divider_inset"
- android:layout_marginTop="@dimen/vertical_divider_inset"
- android:background="@color/divider_color"
- app:layout_constraintStart_toStartOf="@id/guideline_end" />
-
- <androidx.constraintlayout.widget.Guideline
- android:id="@+id/guideline_begin"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:orientation="vertical"
- app:layout_constraintGuide_begin="@dimen/message_history_guideline_begin" />
-
- <androidx.constraintlayout.widget.Guideline
- android:id="@+id/guideline_end"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:orientation="vertical"
- app:layout_constraintGuide_end="200dp" />
-</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/res/layout/loading_info_view.xml b/res/layout/loading_info_view.xml
deleted file mode 100644
index 07ffb73..0000000
--- a/res/layout/loading_info_view.xml
+++ /dev/null
@@ -1,60 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright (C) 2020 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- -->
-<androidx.constraintlayout.widget.ConstraintLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
-
- <Button
- android:id="@+id/loading_info_action_button"
- style="@style/LoadingInfoActionButtonStyle"
- android:layout_marginTop="@dimen/loading_info_button_margin_top"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@id/loading_info_secondary_message" />
-
- <ImageView
- android:id="@+id/loading_info_icon"
- android:layout_width="@dimen/loading_info_icon_size"
- android:layout_height="@dimen/loading_info_icon_size"
- android:layout_marginBottom="@dimen/loading_info_icon_margin_bottom"
- android:contentDescription="@string/cd_loading_info_icon"
- app:layout_constraintBottom_toTopOf="@id/loading_info_message"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintVertical_chainStyle="packed"
- app:tint="@color/primary_icon_color" />
-
- <TextView
- android:id="@+id/loading_info_secondary_message"
- style="@style/LoadingInfoSecondaryMessageStyle"
- app:layout_constraintBottom_toTopOf="@id/loading_info_action_button"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@id/loading_info_message" />
-
- <TextView
- android:id="@+id/loading_info_message"
- style="@style/LoadingInfoMessageStyle"
- app:layout_constraintBottom_toTopOf="@id/loading_info_secondary_message"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@id/loading_info_icon" />
-</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/res/layout/loading_list_fragment.xml b/res/layout/loading_list_fragment.xml
deleted file mode 100644
index b6c4765..0000000
--- a/res/layout/loading_list_fragment.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright (C) 2020 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- -->
-<com.android.car.messenger.core.ui.shared.LoadingFrameLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/loading_frame_layout"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
-
- <include layout="@layout/list_fragment" />
-</com.android.car.messenger.core.ui.shared.LoadingFrameLayout>
diff --git a/res/layout/loading_progress_view.xml b/res/layout/loading_progress_view.xml
deleted file mode 100644
index 79eaf37..0000000
--- a/res/layout/loading_progress_view.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright (C) 2020 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- -->
-<androidx.constraintlayout.widget.ConstraintLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
-
- <ProgressBar
- style="?android:attr/progressBarStyleLarge"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent" />
-</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
deleted file mode 100644
index 983562b..0000000
--- a/res/values/attrs.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2020 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<resources>
- <declare-styleable name="LoadingFrameLayout">
- <attr name="progressViewLayout" format="reference" />
- <attr name="emptyViewLayout" format="reference" />
- <attr name="errorViewLayout" format="reference" />
- </declare-styleable>
-</resources>
diff --git a/res/values/colors.xml b/res/values/colors.xml
deleted file mode 100644
index b4a189e..0000000
--- a/res/values/colors.xml
+++ /dev/null
@@ -1,67 +0,0 @@
-<?xml version='1.0' encoding='UTF-8'?>
-<!-- Copyright (C) 2020 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<resources>
- <array name="letter_tile_colors">
- <item>#db4437</item>
- <item>#e91e63</item>
- <item>#9c27b0</item>
- <item>#673ab7</item>
- <item>#3f51b5</item>
- <item>#4285f4</item>
- <item>#039be5</item>
- <item>#0097a7</item>
- <item>#008577</item>
- <item>#0f9d58</item>
- <item>#689f38</item>
- <item>#ef6c00</item>
- <item>#ff5722</item>
- <item>#757575</item>
- </array>
- <color name="divider_color">@color/divider_color_light</color>
- <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="letter_tile_font_color">#ffffff</color>
-
- <color name="icon_tint">@color/car_grey_50</color>
-
- <color name="primary_text_color">#FFFFFFFF</color>
- <color name="secondary_text_color">#B8FFFFFF</color>
-
- <color name="car_card_ripple_background">#17000000</color>
-
- <color name="background_image_30p_black">#4D000000</color>
-
- <color name="uxr_button_text_color">@color/primary_text_color</color>
- <color name="uxr_button_text_disabled_color">#80FFFFFF</color>
-
- <color name="hero_button_background_color">@color/car_grey_868</color>
- <color name="hero_button_text_color">@color/uxr_button_text_color_selector</color>
-
- <!--
- Color palette for cars.
- Those values are NOT part of the car-ui-lib "resource API", they are just constants used in
- various places to give a default value to the attributes of the "api".
- -->
- <color name="car_grey_868">#ff282a2d</color>
- <color name="car_grey_50">#fff8f9fa</color>
-
- <color name="car_red_500a">#ffd50000</color>
-
- <color name="group_avatar_stroke_color">@android:color/transparent</color>
- <color name="group_avatar_background_color">@android:color/transparent</color>
-</resources>
diff --git a/res/values/config.xml b/res/values/config.xml
index 0ed08d4..56cb667 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -1,34 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 2020 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License
- -->
-<resources>
- <bool name="group_avatar_fill_background">false</bool>
-
- <!--
- The maximum number of individual avatars used for group avatar.
- A number between 1 and 4 is required.
- When the value is 1, the first avatar is used for the group avatar.
- When the value is 2-4, the first nth avatars make up the group avatar,
- where n is the value.
- -->
- <integer name="group_avatar_max_group_size">4</integer>
+ Copyright (C) 2019 The Android Open Source Project
- <integer name="config_letter_tile_text_style">0</integer>
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
- <!-- Typeface.NORMAL=0; Typeface.BOLD=1; Typeface.ITALIC=2; Typeface.BOLD_ITALIC=3-->
- <string name="config_letter_tile_font_family" translatable="false">sans-serif-light</string>
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources>
+ <!-- Whether existing messages should be loaded. Recommended to turn off if head-unit's and
+ BT-paired phone's clocks are not synced.-->
+ <bool name="config_loadExistingMessages">false</bool>
</resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 42d721b..1f9b6af 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -1,70 +1,20 @@
<?xml version='1.0' encoding='UTF-8'?>
-<!-- Copyright (C) 2020 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
+<!--
+ ~ Copyright (C) 2017 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
<resources>
- <!-- Message list dimensions -->
- <dimen name="message_history_item_height">@dimen/list_item_height</dimen>
- <dimen name="message_history_item_padding">@dimen/list_item_padding</dimen>
- <dimen name="message_history_guideline_begin">
- @dimen/list_item_guideline_begin
- </dimen>
- <dimen name="message_history_text_margin_end">
- @dimen/list_item_text_margin_end
- </dimen>
- <dimen name="message_history_icons_margin">@dimen/car_ui_padding_1</dimen>
-
- <dimen name="list_top_padding">@dimen/car_ui_padding_2</dimen>
- <!-- Components -->
- <dimen name="list_item_height">116dp</dimen>
- <dimen name="list_item_guideline_begin">@dimen/car_keyline_4</dimen>
- <dimen name="list_item_text_margin_end">@dimen/car_ui_padding_2</dimen>
- <dimen name="list_item_padding">@dimen/car_keyline_1</dimen>
- <dimen name="list_divider_inset">@dimen/car_keyline_1</dimen>
- <dimen name="list_divider_height">
- @dimen/car_ui_list_item_action_divider_height
- </dimen>
- <dimen name="vertical_divider_inset">@dimen/car_ui_padding_2</dimen>
- <dimen name="vertical_divider_width">2dp</dimen>
- <dimen name="avatar_icon_size">76dp</dimen>
+ <dimen name="notification_contact_photo_size">300dp</dimen>
<dimen name="contact_avatar_corner_radius_percent" format="float">0.5</dimen>
- <dimen name="car_keyline_1">60dp</dimen>
- <dimen name="car_keyline_4">168dp</dimen>
-
- <!-- Loading status view dimensions -->
- <dimen name="loading_info_icon_size">56dp</dimen>
- <dimen name="loading_info_icon_margin_bottom">@dimen/car_ui_padding_3</dimen>
- <dimen name="loading_info_button_margin_top">@dimen/car_ui_padding_2</dimen>
-
- <!-- Dialog and button -->
- <dimen name="dialog_max_width">706dp</dimen>
- <dimen name="hero_button_max_width">@dimen/dialog_max_width</dimen>
- <dimen name="hero_button_min_width">@dimen/car_ui_touch_target_size</dimen>
- <dimen name="hero_button_height">@dimen/car_ui_touch_target_size</dimen>
- <dimen name="hero_button_corner_radius">38dp</dimen>
-
- <dimen name="conversation_avatar_width">50dp</dimen>
-
- <!-- Letter spacing -->
- <dimen name="unread_icon_size">16dp</dimen>
- <dimen name="unread_icon_marginEnd">24dp</dimen>
- <dimen name="subtitle_icon_width">20dp</dimen>
- <item name="letter_to_tile_ratio" format="fraction" type="fraction">67%</item>
-
- <!-- Contact Avatar -->
- <item name="letter_spacing_display3" format="float" type="dimen">0.0</item>
- <item name="letter_spacing_body1" format="float" type="dimen">0.0</item>
- <item name="letter_spacing_body2" format="float" type="dimen">0.0</item>
- <item name="letter_spacing_body3" format="float" type="dimen">0.0</item>
</resources>
diff --git a/res/drawable/ic_play.xml b/res/values/integers.xml
index 5e47e70..22ff0de 100644
--- a/res/drawable/ic_play.xml
+++ b/res/values/integers.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 2020 The Android Open Source Project
+ ~ Copyright (C) 2017 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
@@ -12,14 +12,10 @@
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
- ~ limitations under the License.
+ ~ 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" />
-</vector>
+
+<resources>
+ <integer name="anim_time">1000</integer>
+ <integer name="notification_conversation_title_length">30</integer>
+</resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 837ad3e..1bd3c74 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1,70 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright 2020 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- -->
-<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <!-- Application name [CHAR LIMIT=30] -->
- <plurals name="new_message">
- <item quantity="one" translatable="false">New message</item>
- <item quantity="other" translatable="false">
- <xliff:g example="2" id="count">%d</xliff:g> messages</item>
- </plurals>
+<!-- Copyright (C) 2007 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
- <!-- Button text for when disconnected from Bluetooth [CHAR LIMIT=40] -->
- <string name="app_name" translatable="false">Car Messenger</string>
+ http://www.apache.org/licenses/LICENSE-2.0
- <!-- Button text for connecting to Bluetooth [CHAR LIMIT=40] -->
- <string name="bluetooth_disconnected" translatable="false">Bluetooth disconnected</string>
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
- <!-- Status when no new messages[CHAR LIMIT=40] -->
- <string name="connect_bluetooth_button_text" translatable="false">Connect to Bluetooth</string>
+<resources>
+ <string name="app_name">Messenger</string>
- <!-- Status when replied [CHAR LIMIT=40] -->
- <string name="no_new_messages" translatable="false">No new messages</string>
+ <plurals name="notification_new_message">
+ <item quantity="one">New message</item>
+ <item quantity="other">%d new messages</item>
+ </plurals>
- <!-- Dot separator [CHAR LIMIT=1] -->
- <string name="replied" translatable="false">Replied</string>
- <string name="dot" translatable="false">·</string>
- <string name="action_reply" translatable="false">Reply</string>
- <string name="action_mute" translatable="false">Mute</string>
+ <string name="action_play">Play</string>
+ <string name="action_mark_as_read">Mark As Read</string>
+ <string name="action_repeat">Repeat</string>
+ <string name="action_reply">Reply</string>
+ <string name="action_stop">Stop</string>
+ <string name="action_close_messages">Close</string>
+ <string name="auto_reply_failed_message">Unable to send reply. Please try again.</string>
+ <string name="auto_reply_device_disconnected">Unable to send reply. Device is not connected.
+ </string>
- <!-- The message service channel name [CHAR LIMIT=40] -->
- <string name="action_mark_as_read" translatable="false">Mark As Read</string>
- <!-- The message service channel description [CHAR LIMIT=40] -->
- <string name="app_running_msg_notification_title">Messaging service is active</string>
- <!-- The message channel name [CHAR LIMIT=40] -->
- <string name="app_running_msg_notification_content">Receiving Messages on car</string>
- <!-- The message channel description [CHAR LIMIT=40] -->
- <string name="message_channel_name">Message Channel</string>
- <!-- New Message String [CHAR LIMIT=40] -->
- <string name="message_channel_description">Phone Message Receiver Service</string>
+ <string name="tts_sender_says">%s says</string>
- <!-- Status when there is a new message [CHAR LIMIT=40] -->
- <string name="new_message">New Message</string>
+ <string name="tts_failed_toast">Text playout failed!</string>
+ <string name="reply_message_display_template">\"%s\"</string>
+ <string name="message_sent_notice">Reply sent to %s</string>
- <!-- Content Descriptions-->
- <!-- An icon indicating that this is unread [CHAR LIMIT=40] -->
- <string name="cd_unread">Unread</string>
- <!-- Conversation Icon [CHAR LIMIT=40] -->
- <string name="cd_conversation_icon">Conversation Icon</string>
- <!-- Subtitle Text Icon [CHAR LIMIT=40] -->
- <string name="cd_icon_indicating_the_last_action">Subtitle Text Icon</string>
- <!-- Mute button [CHAR LIMIT=40] -->
- <string name="cd_mute_button">Mute Button</string>
- <!-- Mute button [CHAR LIMIT=40] -->
- <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>
+ <string name="sms_channel_name">SMS Channel</string>
+ <string name="sms_channel_description">Phone SMS Receiver Service</string>
+
+ <!-- Default Sender name that appears in message notification if sender name is not available. [CHAR_LIMIT=NONE] -->
+ <string name="name_not_available">Name not available</string>
+ <!-- Separator between names in a list (i.e. ", " for "Harry, Ron"). [CHAR_LIMIT=NONE] -->
+ <string name="name_separator">,&#160;</string>
+
+ <string name="app_running_msg_channel_name">Uncategorized</string>
+ <string name="app_running_msg_notification_title">Messaging service is active</string>
+ <string name="app_running_msg_notification_content">Receiving SMS through Bluetooth</string>
</resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
deleted file mode 100644
index 2568a05..0000000
--- a/res/values/styles.xml
+++ /dev/null
@@ -1,105 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2020 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
- <!-- Message history -->
- <style name="TextAppearance.MessageHistoryTitle" parent="TextAppearance.Body1" />
-
- <style name="TextAppearance.MessageHistorySubtitle" parent="TextAppearance.Body3">
- <item name="android:textColor">@color/secondary_text_color</item>
- </style>
- <!-- Customized text color for unread messages can be added here -->
- <style name="TextAppearance.MessageHistoryUnreadTitle"
- parent="TextAppearance.MessageHistoryTitle">
- <item name="android:textStyle">bold</item>
- </style>
-
- <style name="TextAppearance.MessageHistoryUnreadSubtitle"
- parent="TextAppearance.MessageHistorySubtitle">
- <item name="android:textStyle">bold</item>
- </style>
-
- <style name="Widget.Button" parent="android:Widget.DeviceDefault.Button">
- <item name="android:ellipsize">none</item>
- <item name="android:requiresFadingEdge">horizontal</item>
- </style>
-
- <style name="LoadingInfoMessageStyle" parent="FullScreenErrorMessageStyle">
- <item name="android:textAppearance">@style/TextAppearance.Display3</item>
- <item name="android:textFontWeight">500</item>
- <item name="android:textStyle">normal</item>
- </style>
-
- <style name="LoadingInfoSecondaryMessageStyle" parent="FullScreenErrorMessageStyle">
- <item name="android:textAppearance">@style/TextAppearance.Body2</item>
- </style>
-
- <style name="LoadingInfoActionButtonStyle" parent="FullScreenErrorButtonStyle">
- <item name="android:textAppearance">@style/TextAppearance.Body3</item>
- <item name="android:textFontWeight">500</item>
- <item name="android:textStyle">normal</item>
- </style>
-
- <!-- Styles for text. Sub1-3 are not included here as their use should be exceptional -->
- <style name="TextAppearance">
- <item name="android:fontFamily">roboto-regular</item>
- <item name="android:textColor">@color/primary_text_color</item>
- <item name="android:textAlignment">viewStart</item>
- </style>
-
- <style name="TextAppearance.Display3" parent="TextAppearance">
- <item name="android:textSize">36sp</item>
- <item name="android:letterSpacing">@dimen/letter_spacing_display3</item>
- </style>
-
- <style name="TextAppearance.Body1" parent="TextAppearance">
- <item name="android:textSize">32sp</item>
- <item name="android:letterSpacing">@dimen/letter_spacing_body1</item>
- </style>
-
- <style name="TextAppearance.Body2" parent="TextAppearance">
- <item name="android:textSize">28sp</item>
- <item name="android:letterSpacing">@dimen/letter_spacing_body2</item>
- </style>
-
- <style name="TextAppearance.Body3" parent="TextAppearance">
- <item name="android:textSize">24sp</item>
- <item name="android:letterSpacing">@dimen/letter_spacing_body3</item>
- </style>
-
- <!-- Styles for ControlBar -->
- <style name="FullScreenErrorMessageStyle">
- <item name="android:layout_width">wrap_content</item>
- <item name="android:layout_height">wrap_content</item>
- <item name="android:textAppearance">@style/TextAppearance.Body1</item>
- <item name="android:gravity">center</item>
- <item name="android:maxWidth">@dimen/dialog_max_width</item>
- </style>
-
- <style name="FullScreenErrorButtonStyle">
- <item name="android:layout_width">wrap_content</item>
- <item name="android:layout_height">@dimen/hero_button_height</item>
- <item name="android:textAppearance">@style/TextAppearance.Body1</item>
- <item name="android:textStyle">bold</item>
- <item name="android:maxWidth">@dimen/hero_button_max_width</item>
- <item name="android:minWidth">@dimen/hero_button_min_width</item>
- <item name="android:textAllCaps">false</item>
- <item name="android:singleLine">true</item>
- <item name="android:background">@drawable/hero_button_background</item>
- <item name="android:textColor">@color/hero_button_text_color</item>
- <item name="android:gravity">center</item>
- <item name="android:paddingHorizontal">@dimen/hero_button_corner_radius</item>
- </style>
-</resources>
diff --git a/res/values/themes.xml b/res/values/themes.xml
deleted file mode 100644
index f2ddf7a..0000000
--- a/res/values/themes.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2020 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<resources>
- <!-- The base theme for the Message UI Library-->
- <style name="Theme.Messaging" parent="Theme.CarUi.WithToolbar">
- <item name="android:listDivider">@drawable/list_divider</item>
- <item name="android:buttonStyle">@style/Widget.Button</item>
- <item name="android:textDirection">locale</item>
- </style>
-
- <style name="Theme.Messaging.BidiText" parent="Theme.Messaging">
- <item name="android:textDirection">ltr</item>
- <item name="android:textAlignment">viewStart</item>
- </style>
-</resources>
diff --git a/src/com/android/car/messenger/MessageNotificationDelegate.java b/src/com/android/car/messenger/MessageNotificationDelegate.java
new file mode 100644
index 0000000..5fa6db0
--- /dev/null
+++ b/src/com/android/car/messenger/MessageNotificationDelegate.java
@@ -0,0 +1,487 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.messenger;
+
+
+import static com.android.car.apps.common.util.SafeLog.logd;
+import static com.android.car.apps.common.util.SafeLog.loge;
+import static com.android.car.apps.common.util.SafeLog.logw;
+
+import android.annotation.Nullable;
+import android.app.PendingIntent;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothMapClient;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.widget.Toast;
+
+import androidx.core.graphics.drawable.RoundedBitmapDrawable;
+import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
+
+import com.android.car.messenger.bluetooth.BluetoothHelper;
+import com.android.car.messenger.bluetooth.BluetoothMonitor;
+import com.android.car.messenger.common.BaseNotificationDelegate;
+import com.android.car.messenger.common.ConversationKey;
+import com.android.car.messenger.common.ConversationNotificationInfo;
+import com.android.car.messenger.common.Message;
+import com.android.car.messenger.common.ProjectionStateListener;
+import com.android.car.messenger.common.SenderKey;
+import com.android.car.messenger.common.Utils;
+import com.android.car.telephony.common.TelecomUtils;
+import com.android.internal.annotations.GuardedBy;
+
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.request.RequestOptions;
+import com.bumptech.glide.request.target.SimpleTarget;
+import com.bumptech.glide.request.transition.Transition;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Consumer;
+
+/** Delegate class responsible for handling messaging service actions */
+public class MessageNotificationDelegate extends BaseNotificationDelegate implements
+ BluetoothMonitor.OnBluetoothEventListener {
+ private static final String TAG = "MsgNotiDelegate";
+ private static final Object mMapClientLock = new Object();
+
+ @GuardedBy("mMapClientLock")
+ private BluetoothMapClient mBluetoothMapClient;
+ /** Tracks whether a projection application is active in the foreground. **/
+ private ProjectionStateListener mProjectionStateListener;
+ private CompletableFuture<Void> mPhoneNumberInfoFuture;
+ private Locale mGeneratedGroupConversationTitlesLocale;
+ private static int mBitmapSize;
+ private static float mCornerRadiusPercent;
+ private static boolean mShouldLoadExistingMessages;
+ private static int mNotificationConversationTitleLength;
+
+ final Map<String, Long> mBtDeviceAddressToConnectionTimestamp = new HashMap<>();
+ final Map<SenderKey, Bitmap> mSenderToLargeIconBitmap = new HashMap<>();
+ final Map<String, String> mUriToSenderNameMap = new HashMap<>();
+ final Set<ConversationKey> mGeneratedGroupConversationTitles = new HashSet<>();
+
+ public MessageNotificationDelegate(Context context) {
+ super(context, /* useLetterTile */ true);
+ mProjectionStateListener = new ProjectionStateListener(context);
+ loadConfigValues(context);
+ }
+
+ /** Loads all necessary values from the config.xml at creation or when values are changed. **/
+ protected static void loadConfigValues(Context context) {
+ mBitmapSize = 300;
+ mCornerRadiusPercent = (float) 0.5;
+ mShouldLoadExistingMessages = false;
+ mNotificationConversationTitleLength = 30;
+ try {
+ mBitmapSize =
+ context.getResources()
+ .getDimensionPixelSize(R.dimen.notification_contact_photo_size);
+ mCornerRadiusPercent = context.getResources()
+ .getFloat(R.dimen.contact_avatar_corner_radius_percent);
+ mShouldLoadExistingMessages =
+ context.getResources().getBoolean(R.bool.config_loadExistingMessages);
+ mNotificationConversationTitleLength = context.getResources().getInteger(
+ R.integer.notification_conversation_title_length);
+ } catch (Resources.NotFoundException e) {
+ // Should only happen for robolectric unit tests;
+ loge(TAG, "Disabling loading of existing messages: " + e.getMessage());
+ }
+ }
+
+ @Override
+ public void onMessageReceived(Intent intent) {
+ addNamesToSenderMap(intent);
+ if (Utils.isGroupConversation(intent)) {
+ // Group Conversations have URIs of senders whose names we need to load from the DB.
+ loadNamesFromDatabase(intent);
+ }
+ loadAvatarIconAndProcessMessage(intent);
+ }
+
+ @Override
+ public void onMessageSent(Intent intent) {
+ logd(TAG, "onMessageSent");
+ }
+
+ @Override
+ public void onDeviceConnected(BluetoothDevice device) {
+ logd(TAG, "Device connected: " + device.getAddress());
+ mBtDeviceAddressToConnectionTimestamp.put(device.getAddress(), System.currentTimeMillis());
+ synchronized (mMapClientLock) {
+ if (mBluetoothMapClient != null) {
+ if (mShouldLoadExistingMessages) {
+ mBluetoothMapClient.getUnreadMessages(device);
+ }
+ } else {
+ // onDeviceConnected should be sent by BluetoothMapClient, so log if we run into
+ // this strange case.
+ loge(TAG, "BluetoothMapClient is null after connecting to device.");
+ }
+ }
+ }
+
+ @Override
+ public void onDeviceDisconnected(BluetoothDevice device) {
+ String deviceAddress = device.getAddress();
+ logd(TAG, "Device disconnected: " + deviceAddress);
+ cleanupMessagesAndNotifications(key -> key.matches(deviceAddress));
+ mBtDeviceAddressToConnectionTimestamp.remove(deviceAddress);
+ mSenderToLargeIconBitmap.entrySet().removeIf(entry ->
+ entry.getKey().getDeviceId().equals(deviceAddress));
+ mGeneratedGroupConversationTitles.removeIf(
+ convoKey -> convoKey.getDeviceId().equals(deviceAddress));
+ }
+
+ @Override
+ public void onMapConnected(BluetoothMapClient client) {
+ logd(TAG, "Connected to BluetoothMapClient");
+ List<BluetoothDevice> connectedDevices;
+ synchronized (mMapClientLock) {
+ if (mBluetoothMapClient == client) {
+ return;
+ }
+
+ mBluetoothMapClient = client;
+ connectedDevices = mBluetoothMapClient.getConnectedDevices();
+ }
+ if (connectedDevices != null) {
+ for (BluetoothDevice device : connectedDevices) {
+ onDeviceConnected(device);
+ }
+ }
+ }
+
+ @Override
+ public void onMapDisconnected() {
+ logd(TAG, "Disconnected from BluetoothMapClient");
+ resetInternalData();
+ synchronized (mMapClientLock) {
+ mBluetoothMapClient = null;
+ }
+ }
+
+ @Override
+ public void onSdpRecord(BluetoothDevice device, boolean supportsReply) {
+ /* NO_OP */
+ }
+
+ protected void markAsRead(ConversationKey convoKey) {
+ excludeFromNotification(convoKey);
+ }
+
+ protected void dismiss(ConversationKey convoKey) {
+ super.dismissInternal(convoKey);
+ }
+
+ @Override
+ protected boolean shouldAddReplyAction(String deviceAddress) {
+ BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+ if (adapter == null) {
+ return false;
+ }
+ BluetoothDevice device = adapter.getRemoteDevice(deviceAddress);
+
+ synchronized (mMapClientLock) {
+ return (mBluetoothMapClient != null) && mBluetoothMapClient.isUploadingSupported(
+ device);
+ }
+ }
+
+ protected void sendMessage(ConversationKey conversationKey, String messageText) {
+ final boolean deviceConnected = mBtDeviceAddressToConnectionTimestamp.containsKey(
+ conversationKey.getDeviceId());
+ if (!deviceConnected) {
+ logw(TAG, "sendMessage: device disconnected, can't send message");
+ return;
+ }
+ boolean success = false;
+ synchronized (mMapClientLock) {
+ if (mBluetoothMapClient != null) {
+ ConversationNotificationInfo notificationInfo = mNotificationInfos.get(
+ conversationKey);
+ if (notificationInfo == null) {
+ logw(TAG, "No notificationInfo found for senderKey "
+ + conversationKey.toString());
+ } else if (notificationInfo.getCcRecipientsUris().isEmpty()) {
+ logw(TAG, "No contact URI for sender!");
+ } else {
+ success = sendMessageInternal(conversationKey, messageText);
+ }
+ }
+ }
+
+ if (!success) {
+ Toast.makeText(mContext, R.string.auto_reply_failed_message, Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ protected void onDestroy() {
+ resetInternalData();
+ if (mPhoneNumberInfoFuture != null) {
+ mPhoneNumberInfoFuture.cancel(true);
+ }
+ mProjectionStateListener.destroy();
+ }
+
+ private void resetInternalData() {
+ cleanupMessagesAndNotifications(key -> true);
+ mUriToSenderNameMap.clear();
+ mSenderToLargeIconBitmap.clear();
+ mBtDeviceAddressToConnectionTimestamp.clear();
+ mGeneratedGroupConversationTitles.clear();
+ }
+
+ /**
+ * Creates a new message and links it to the conversation identified by the convoKey. Then
+ * posts the message notification after all loading queries from the database have finished.
+ */
+ private void initializeNewMessage(ConversationKey convoKey, Message message) {
+ addMessageToNotificationInfo(message, convoKey);
+ ConversationNotificationInfo notificationInfo = mNotificationInfos.get(convoKey);
+ // Only show notifications for messages received AFTER phone was connected.
+ mPhoneNumberInfoFuture.thenRun(() -> {
+ setGroupConversationTitle(convoKey);
+ // Only show notifications for messages received AFTER phone was connected.
+ if (message.getReceivedTime()
+ >= mBtDeviceAddressToConnectionTimestamp.get(convoKey.getDeviceId())) {
+ postNotification(convoKey, notificationInfo, getChannelId(convoKey.getDeviceId()),
+ mSenderToLargeIconBitmap.get(message.getSenderKey()));
+ }
+ });
+ }
+
+ /**
+ * Creates a new conversation with all of the conversation metadata, and adds the first
+ * message to the conversation.
+ */
+ private void initializeNewConversation(ConversationKey convoKey, Intent intent) {
+ if (mNotificationInfos.containsKey(convoKey)) {
+ logw(TAG, "Conversation already exists! " + convoKey.toString());
+ }
+ Message message = Message.parseFromIntent(intent);
+ ConversationNotificationInfo notiInfo;
+ try {
+ // Pass in null icon, since the fallback icon represents the system app's icon.
+ notiInfo =
+ ConversationNotificationInfo.createConversationNotificationInfo(intent,
+ message.getSenderName(), mContext.getClass().getName(),
+ /* appIcon */ null);
+ } catch (IllegalArgumentException e) {
+ logw(TAG, "initNewConvo: Message could not be created from the intent.");
+ return;
+ }
+ mNotificationInfos.put(convoKey, notiInfo);
+ initializeNewMessage(convoKey, message);
+ }
+
+ /** Loads the avatar icon, and processes the message after avatar is loaded. **/
+ private void loadAvatarIconAndProcessMessage(Intent intent) {
+ SenderKey senderKey = SenderKey.createSenderKey(intent);
+ String phoneNumber = Utils.getPhoneNumberFromMapClient(Utils.getSenderUri(intent));
+ if (mSenderToLargeIconBitmap.containsKey(senderKey) || phoneNumber == null) {
+ addMessageFromIntent(intent);
+ return;
+ }
+ loadPhoneNumberInfo(phoneNumber, phoneNumberInfo -> {
+ if (phoneNumberInfo == null) {
+ return;
+ }
+ Glide.with(mContext)
+ .asBitmap()
+ .load(phoneNumberInfo.getAvatarUri())
+ .apply(new RequestOptions().override(mBitmapSize))
+ .into(new SimpleTarget<Bitmap>() {
+ @Override
+ public void onResourceReady(Bitmap bitmap,
+ Transition<? super Bitmap> transition) {
+ RoundedBitmapDrawable roundedBitmapDrawable =
+ RoundedBitmapDrawableFactory
+ .create(mContext.getResources(), bitmap);
+ Icon avatarIcon = TelecomUtils
+ .createFromRoundedBitmapDrawable(roundedBitmapDrawable,
+ mBitmapSize,
+ mCornerRadiusPercent);
+ mSenderToLargeIconBitmap.put(senderKey, avatarIcon.getBitmap());
+ addMessageFromIntent(intent);
+ return;
+ }
+
+ @Override
+ public void onLoadFailed(@Nullable Drawable fallback) {
+ addMessageFromIntent(intent);
+ return;
+ }
+ });
+ });
+ }
+
+ /**
+ * Extracts the message from the intent and creates a new conversation or message
+ * appropriately.
+ */
+ private void addMessageFromIntent(Intent intent) {
+ ConversationKey convoKey = ConversationKey.createConversationKey(intent);
+
+ if (convoKey == null) return;
+ logd(TAG, "Received message from " + convoKey.getDeviceId());
+ if (mNotificationInfos.containsKey(convoKey)) {
+ try {
+ initializeNewMessage(convoKey, Message.parseFromIntent(intent));
+ } catch (IllegalArgumentException e) {
+ logw(TAG, "addMessage: Message could not be created from the intent.");
+ return;
+ }
+ } else {
+ initializeNewConversation(convoKey, intent);
+ }
+ }
+
+ private void addNamesToSenderMap(Intent intent) {
+ String senderUri = Utils.getSenderUri(intent);
+ String senderName = Utils.getSenderName(intent);
+ if (senderUri != null) {
+ mUriToSenderNameMap.put(senderUri, senderName);
+ }
+ }
+
+ /**
+ * Loads the name of a sender based on the sender's contact URI.
+ *
+ * This is needed to load the participants' names of a group conversation since
+ * {@link BluetoothMapClient} only sends the URIs of these participants.
+ */
+ private void loadNamesFromDatabase(Intent intent) {
+ for (String uri : Utils.getInclusiveRecipientsUrisList(intent)) {
+ String phoneNumber = Utils.getPhoneNumberFromMapClient(uri);
+ if (phoneNumber != null && !mUriToSenderNameMap.containsKey(uri)) {
+ loadPhoneNumberInfo(phoneNumber, (phoneNumberInfo) -> {
+ mUriToSenderNameMap.put(uri, phoneNumberInfo.getDisplayName());
+ });
+ }
+ }
+ }
+
+ /**
+ * Sets the group conversation title using the names of all the participants in the group.
+ * If all the participants' names have been loaded from the database, then we don't need
+ * to generate the title again.
+ *
+ * A group conversation's title should be an alphabetically sorted list of the participant's
+ * names, separated by commas.
+ */
+ private void setGroupConversationTitle(ConversationKey conversationKey) {
+ ConversationNotificationInfo notificationInfo = mNotificationInfos.get(conversationKey);
+ Locale locale = Locale.getDefault();
+
+ // Do not reuse the old titles if locale has changed. The new locale might need different
+ // formatting or text direction.
+ if (locale != mGeneratedGroupConversationTitlesLocale) {
+ mGeneratedGroupConversationTitles.clear();
+ }
+ if (!notificationInfo.isGroupConvo()
+ || mGeneratedGroupConversationTitles.contains(conversationKey)) {
+ return;
+ }
+
+ List<String> names = new ArrayList<>();
+
+ boolean allNamesLoaded = true;
+ for (String uri : notificationInfo.getCcRecipientsUris()) {
+ if (mUriToSenderNameMap.containsKey(uri)) {
+ names.add(mUriToSenderNameMap.get(uri));
+ } else {
+ names.add(Utils.getPhoneNumberFromMapClient(uri));
+ // This URI has not been loaded from the database, set allNamesLoaded to false.
+ allNamesLoaded = false;
+ }
+ }
+
+ notificationInfo.setConvoTitle(Utils.constructGroupConversationTitle(names,
+ mContext.getString(R.string.name_separator), mNotificationConversationTitleLength));
+ if (allNamesLoaded) {
+ mGeneratedGroupConversationTitlesLocale = locale;
+ mGeneratedGroupConversationTitles.add(conversationKey);
+ }
+ }
+
+ private void loadPhoneNumberInfo(@Nullable String phoneNumber,
+ Consumer<? super TelecomUtils.PhoneNumberInfo> action) {
+ if (phoneNumber == null) {
+ logw(TAG, " Could not load PhoneNumberInfo due to null phone number");
+ return;
+ }
+
+ mPhoneNumberInfoFuture = TelecomUtils.getPhoneNumberInfo(mContext, phoneNumber)
+ .thenAcceptAsync(action, mContext.getMainExecutor());
+ }
+
+ private String getChannelId(String deviceAddress) {
+ if (mProjectionStateListener.isProjectionInActiveForeground(deviceAddress)) {
+ return MessengerService.SILENT_SMS_CHANNEL_ID;
+ }
+ return MessengerService.SMS_CHANNEL_ID;
+ }
+
+ /** Sends reply message to the BluetoothMapClient to send to the connected phone. **/
+ private boolean sendMessageInternal(ConversationKey conversationKey, String messageText) {
+ ConversationNotificationInfo notificationInfo = mNotificationInfos.get(conversationKey);
+ Uri[] recipientUrisArray = generateRecipientUriArray(notificationInfo);
+
+ final int requestCode = conversationKey.hashCode();
+
+ Intent intent = new Intent(BluetoothMapClient.ACTION_MESSAGE_SENT_SUCCESSFULLY);
+ PendingIntent sentIntent = PendingIntent.getBroadcast(mContext, requestCode,
+ intent,
+ PendingIntent.FLAG_ONE_SHOT);
+
+ try {
+ return BluetoothHelper.sendMessage(mBluetoothMapClient,
+ conversationKey.getDeviceId(), recipientUrisArray, messageText,
+ sentIntent, null);
+ } catch (IllegalArgumentException e) {
+ logw(TAG, "Invalid device address: " + conversationKey.getDeviceId());
+ }
+ return false;
+ }
+
+ /**
+ * Generate an array containing all the recipients' URIs that should receive the user's
+ * message for the given notificationInfo.
+ */
+ private Uri[] generateRecipientUriArray(ConversationNotificationInfo notificationInfo) {
+ List<String> ccRecipientsUris = notificationInfo.getCcRecipientsUris();
+ Uri[] recipientUris = new Uri[ccRecipientsUris.size()];
+
+ for (int i = 0; i < ccRecipientsUris.size(); i++) {
+ recipientUris[i] = Uri.parse(ccRecipientsUris.get(i));
+ }
+ return recipientUris;
+ }
+}
diff --git a/src/com/android/car/messenger/MessengerActivity.java b/src/com/android/car/messenger/MessengerActivity.java
new file mode 100644
index 0000000..e350dce
--- /dev/null
+++ b/src/com/android/car/messenger/MessengerActivity.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.messenger;
+
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+
+import androidx.annotation.Nullable;
+
+/**
+ * No-op Activity that only exists in order to have an entry in the manifest with SMS specific
+ * intent-filter.
+ * <p>
+ * We need the manifest entry so that PackageManager will grant this pre-installed app SMS related
+ * permissions. See DefaultPermissionGrantPolicy.grantDefaultSystemHandlerPermissions().
+ */
+public class MessengerActivity extends Activity {
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ finish();
+ }
+
+ @Override
+ public void startActivity(Intent intent) {
+ super.startActivity(intent);
+ finish();
+ }
+
+}
diff --git a/src/com/android/car/messenger/MessengerService.java b/src/com/android/car/messenger/MessengerService.java
new file mode 100644
index 0000000..905109f
--- /dev/null
+++ b/src/com/android/car/messenger/MessengerService.java
@@ -0,0 +1,263 @@
+package com.android.car.messenger;
+
+
+import static com.android.car.messenger.common.BaseNotificationDelegate.ACTION_DISMISS_NOTIFICATION;
+import static com.android.car.messenger.common.BaseNotificationDelegate.ACTION_MARK_AS_READ;
+import static com.android.car.messenger.common.BaseNotificationDelegate.ACTION_REPLY;
+import static com.android.car.messenger.common.BaseNotificationDelegate.EXTRA_CONVERSATION_KEY;
+import static com.android.car.messenger.common.BaseNotificationDelegate.EXTRA_REMOTE_INPUT_KEY;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.Service;
+import android.content.Intent;
+import android.media.AudioAttributes;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.provider.Settings;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+
+import androidx.core.app.NotificationCompat;
+import androidx.core.app.RemoteInput;
+
+import com.android.car.messenger.bluetooth.BluetoothMonitor;
+import com.android.car.messenger.common.BaseNotificationDelegate;
+import com.android.car.messenger.common.ConversationKey;
+import com.android.car.messenger.log.L;
+
+/** Service responsible for handling SMS messaging events from paired Bluetooth devices. */
+public class MessengerService extends Service {
+ private final static String TAG = "CM.MessengerService";
+
+ /* ACTIONS */
+ /** Used to start this service at boot-complete. Takes no arguments. */
+ public static final String ACTION_START = "com.android.car.messenger.ACTION_START";
+
+ /** Used to notify when a sms is received. Takes no arguments. */
+ public static final String ACTION_RECEIVED_SMS =
+ "com.android.car.messenger.ACTION_RECEIVED_SMS";
+
+ /** Used to notify when a mms is received. Takes no arguments. */
+ public static final String ACTION_RECEIVED_MMS =
+ "com.android.car.messenger.ACTION_RECEIVED_MMS";
+
+ /* EXTRAS */
+
+ /* NOTIFICATIONS */
+ static final String SMS_CHANNEL_ID = "SMS_CHANNEL_ID";
+ static final String SILENT_SMS_CHANNEL_ID = "SILENT_SMS_CHANNEL_ID";
+ private static final String APP_RUNNING_CHANNEL_ID = "APP_RUNNING_CHANNEL_ID";
+ private static final int SERVICE_STARTED_NOTIFICATION_ID = Integer.MAX_VALUE;
+
+ /** Delegate class used to handle this services' actions */
+ private MessageNotificationDelegate mMessengerDelegate;
+
+ /** Notifies this service of new bluetooth actions */
+ private BluetoothMonitor mBluetoothMonitor;
+
+ /* Binding boilerplate */
+ private final IBinder mBinder = new LocalBinder();
+
+ public class LocalBinder extends Binder {
+ MessengerService getService() {
+ return MessengerService.this;
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ L.d(TAG, "onCreate");
+
+ mMessengerDelegate = new MessageNotificationDelegate(this);
+ mBluetoothMonitor = new BluetoothMonitor(this);
+ mBluetoothMonitor.registerListener(mMessengerDelegate);
+ sendServiceRunningNotification();
+ }
+
+
+ private void sendServiceRunningNotification() {
+ NotificationManager notificationManager = getSystemService(NotificationManager.class);
+
+ if (notificationManager == null) {
+ L.e(TAG, "Failed to get NotificationManager instance");
+ return;
+ }
+
+ // Create notification channel for app running notification
+ {
+ NotificationChannel appRunningNotificationChannel =
+ new NotificationChannel(APP_RUNNING_CHANNEL_ID,
+ getString(R.string.app_running_msg_channel_name),
+ NotificationManager.IMPORTANCE_MIN);
+ notificationManager.createNotificationChannel(appRunningNotificationChannel);
+ }
+
+ // Create notification channel for notifications that should be posted silently in the
+ // notification center, without a heads up notification.
+ {
+ NotificationChannel silentNotificationChannel =
+ new NotificationChannel(SILENT_SMS_CHANNEL_ID,
+ getString(R.string.sms_channel_description),
+ NotificationManager.IMPORTANCE_LOW);
+ notificationManager.createNotificationChannel(silentNotificationChannel);
+ }
+
+ {
+ AudioAttributes attributes = new AudioAttributes.Builder()
+ .setUsage(AudioAttributes.USAGE_NOTIFICATION)
+ .build();
+ NotificationChannel smsChannel = new NotificationChannel(SMS_CHANNEL_ID,
+ getString(R.string.sms_channel_name),
+ NotificationManager.IMPORTANCE_HIGH);
+ smsChannel.setDescription(getString(R.string.sms_channel_description));
+ smsChannel.setSound(Settings.System.DEFAULT_NOTIFICATION_URI, attributes);
+ notificationManager.createNotificationChannel(smsChannel);
+ }
+
+ final Notification notification =
+ new NotificationCompat.Builder(this, APP_RUNNING_CHANNEL_ID)
+ .setSmallIcon(R.drawable.ic_message)
+ .setContentTitle(getString(R.string.app_running_msg_notification_title))
+ .setContentText(getString(R.string.app_running_msg_notification_content))
+ .build();
+ startForeground(SERVICE_STARTED_NOTIFICATION_ID, notification);
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ L.d(TAG, "onDestroy");
+ mMessengerDelegate.onDestroy();
+ mBluetoothMonitor.onDestroy();
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ final int result = START_STICKY;
+
+ if (intent == null || intent.getAction() == null) return result;
+
+ final String action = intent.getAction();
+ if (!hasRequiredArgs(intent)) {
+ L.e(TAG, "Dropping command: %s. Reason: Missing required argument.", action);
+ return result;
+ }
+
+ switch (action) {
+ case ACTION_START:
+ // NO-OP
+ break;
+ case ACTION_REPLY:
+ voiceReply(intent);
+ break;
+ case ACTION_DISMISS_NOTIFICATION:
+ clearNotificationState(intent);
+ break;
+ case ACTION_MARK_AS_READ:
+ markAsRead(intent);
+ break;
+ case ACTION_RECEIVED_SMS:
+ // NO-OP
+ break;
+ case ACTION_RECEIVED_MMS:
+ // NO-OP
+ break;
+ case TelephonyManager.ACTION_RESPOND_VIA_MESSAGE:
+ respondViaMessage(intent);
+ break;
+ default:
+ L.w(TAG, "Unsupported action: %s", action);
+ }
+
+ return result;
+ }
+
+ /**
+ * Checks that the intent has all of the required arguments for its requested action.
+ *
+ * @param intent the intent to check
+ * @return true if the intent has all of the required {@link Bundle} args for its action
+ */
+ private static boolean hasRequiredArgs(Intent intent) {
+ switch (intent.getAction()) {
+ case ACTION_REPLY:
+ case ACTION_DISMISS_NOTIFICATION:
+ case ACTION_MARK_AS_READ:
+ if (!intent.hasExtra(EXTRA_CONVERSATION_KEY)) {
+ L.w(TAG, "Intent %s missing conversation-key extra.", intent.getAction());
+ return false;
+ }
+ return true;
+ default:
+ // For unknown actions, default to true. We'll report an error for these later.
+ return true;
+ }
+ }
+
+ /**
+ * Sends a reply, meant to be used from a caller originating from voice input.
+ *
+ * @param intent intent containing {@link BaseNotificationDelegate#EXTRA_CONVERSATION_KEY} and
+ * a {@link RemoteInput} with
+ * {@link BaseNotificationDelegate#EXTRA_REMOTE_INPUT_KEY} resultKey
+ */
+ public void voiceReply(Intent intent) {
+ final ConversationKey conversationKey = intent.getParcelableExtra(EXTRA_CONVERSATION_KEY);
+ final Bundle bundle = RemoteInput.getResultsFromIntent(intent);
+ if (bundle == null) {
+ L.e(TAG, "Dropping voice reply. Received null RemoteInput result!");
+ return;
+ }
+ final CharSequence message = bundle.getCharSequence(EXTRA_REMOTE_INPUT_KEY);
+ L.d(TAG, "voiceReply");
+ if (!TextUtils.isEmpty(message)) {
+ mMessengerDelegate.sendMessage(conversationKey, message.toString());
+ }
+ }
+
+ /**
+ * Clears notification(s) associated with a given sender key.
+ *
+ * @param intent intent containing {@link BaseNotificationDelegate#EXTRA_CONVERSATION_KEY} bundle argument
+ */
+ public void clearNotificationState(Intent intent) {
+ final ConversationKey conversationKey = intent.getParcelableExtra(EXTRA_CONVERSATION_KEY);
+ L.d(TAG, "clearNotificationState");
+ mMessengerDelegate.clearNotifications(key -> key.equals(conversationKey));
+ }
+
+ /**
+ * Mark a conversation associated with a given sender key as read.
+ *
+ * @param intent intent containing {@link BaseNotificationDelegate#EXTRA_CONVERSATION_KEY} bundle argument
+ */
+ public void markAsRead(Intent intent) {
+ final ConversationKey conversationKey = intent.getParcelableExtra(EXTRA_CONVERSATION_KEY);
+ L.d(TAG, "markAsRead");
+ mMessengerDelegate.markAsRead(conversationKey);
+ }
+
+ /**
+ * Respond to a call via text message.
+ *
+ * @param intent intent containing a URI describing the recipient and the URI schema
+ */
+ public void respondViaMessage(Intent intent) {
+ Bundle extras = intent.getExtras();
+ if (extras == null) {
+ L.v(TAG, "Called to send SMS but no extras");
+ return;
+ }
+
+ // TODO: get conversationKey from the recipient's address, and sendMessage() to it.
+ }
+}
diff --git a/src/com/android/car/messenger/impl/receivers/MmsReceiver.java b/src/com/android/car/messenger/MmsReceiver.java
index 6811d23..37cc5ef 100644
--- a/src/com/android/car/messenger/impl/receivers/MmsReceiver.java
+++ b/src/com/android/car/messenger/MmsReceiver.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2020 The Android Open Source Project
+ * Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,16 +14,20 @@
* limitations under the License.
*/
-package com.android.car.messenger.impl.receivers;
+package com.android.car.messenger;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
-import androidx.annotation.NonNull;
-
-/** No-op Receiver that only exists in order to be eligible to be the default SMS app. */
+/**
+ * No-op Receiver that only exists in order to be eligible to be the default SMS app.
+ */
public class MmsReceiver extends BroadcastReceiver {
@Override
- public void onReceive(@NonNull Context context, @NonNull Intent intent) {}
+ public void onReceive(Context context, Intent intent) {
+ Intent startIntent = new Intent(context, MessengerService.class)
+ .setAction(MessengerService.ACTION_RECEIVED_MMS);
+ context.startForegroundService(startIntent);
+ }
}
diff --git a/src/com/android/car/messenger/SmsDatabaseHandler.java b/src/com/android/car/messenger/SmsDatabaseHandler.java
new file mode 100644
index 0000000..a5d0472
--- /dev/null
+++ b/src/com/android/car/messenger/SmsDatabaseHandler.java
@@ -0,0 +1,220 @@
+package com.android.car.messenger;
+
+
+import android.Manifest;
+import android.app.AppOpsManager;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.net.Uri;
+import android.provider.BaseColumns;
+import android.provider.ContactsContract;
+import android.provider.Telephony;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.core.content.ContextCompat;
+
+import com.android.car.messenger.common.Message;
+import com.android.car.messenger.log.L;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+/**
+ * Reads and writes SMS Messages into the Telephony.SMS Database.
+ */
+class SmsDatabaseHandler {
+ private static final String TAG = "CM.SmsDatabaseHandler";
+ private static final int MESSAGE_NOT_FOUND = -1;
+ private static final int DUPLICATE_MESSAGES_FOUND = -2;
+ private static final int DATABASE_ERROR = -3;
+ private static final Uri SMS_URI = Telephony.Sms.CONTENT_URI;
+ private static final String SMS_SELECTION = Telephony.Sms.ADDRESS + "=? AND "
+ + Telephony.Sms.BODY + "=? AND (" + Telephony.Sms.DATE + ">=? OR " + Telephony.Sms.DATE
+ + "<=?)";
+ private static final SimpleDateFormat DATE_FORMATTER = new SimpleDateFormat(
+ "MMM dd,yyyy HH:mm");
+
+ private final ContentResolver mContentResolver;
+ private final boolean mCanWriteToDatabase;
+
+ protected SmsDatabaseHandler(Context context) {
+ mCanWriteToDatabase = canWriteToDatabase(context);
+ mContentResolver = context.getContentResolver();
+ readDatabase(context);
+ }
+
+ protected void addOrUpdate(String deviceAddress, Message message) {
+ if (!mCanWriteToDatabase) {
+ return;
+ }
+
+ int messageIndex = findMessageIndex(deviceAddress, message);
+ switch(messageIndex) {
+ case DUPLICATE_MESSAGES_FOUND:
+ removePreviousAndInsert(deviceAddress, message);
+ L.d(TAG, "Message has more than one duplicate in Telephony Database: %s",
+ message.toString());
+ return;
+ case MESSAGE_NOT_FOUND:
+ mContentResolver.insert(SMS_URI, buildMessageContentValues(deviceAddress, message));
+ return;
+ case DATABASE_ERROR:
+ return;
+ default:
+ update(messageIndex, buildMessageContentValues(deviceAddress, message));
+ }
+ }
+
+ protected void removeMessagesForDevice(String address) {
+ if (!mCanWriteToDatabase) {
+ return;
+ }
+
+ String smsSelection = Telephony.Sms.ADDRESS + "=?";
+ String[] smsSelectionArgs = {address};
+ mContentResolver.delete(SMS_URI, smsSelection, smsSelectionArgs);
+ }
+
+ /**
+ * Reads the Telephony SMS Database, and logs all of the SMS messages that have been received
+ * in the last five minutes.
+ * @param context
+ */
+ protected static void readDatabase(Context context) {
+ if (!Log.isLoggable(TAG, Log.DEBUG)) {
+ return;
+ }
+
+ Long beginningTimeStamp = System.currentTimeMillis() - 300000;
+ String timeStamp = DATE_FORMATTER.format(new Date(beginningTimeStamp));
+ Log.d(TAG,
+ " ------ printing SMSs received after " + timeStamp + "-------- ");
+
+ String smsSelection = Telephony.Sms.DATE + ">=?";
+ String[] smsSelectionArgs = {Long.toString(beginningTimeStamp)};
+ Cursor cursor = context.getContentResolver().query(SMS_URI, null,
+ smsSelection,
+ smsSelectionArgs, null /* sortOrder */);
+ if (cursor != null) {
+ while (cursor.moveToNext()) {
+ String body = cursor.getString(12);
+
+ Date date = new Date(cursor.getLong(4));
+ Log.d(TAG,
+ "_id " + cursor.getInt(0) + " person: " + cursor.getInt(3) + " body: "
+ + body.substring(0, Math.min(body.length(), 17)) + " address: "
+ + cursor.getString(2) + " date: " + DATE_FORMATTER.format(
+ date) + " longDate " + cursor.getLong(4) + " read: "
+ + cursor.getInt(7));
+ }
+ }
+ Log.d(TAG, " ------ end read table --------");
+ }
+
+ /** Removes multiple previous copies, and inserts the new message. **/
+ private void removePreviousAndInsert(String deviceAddress, Message message) {
+ String[] smsSelectionArgs = createSmsSelectionArgs(deviceAddress, message);
+
+ mContentResolver.delete(SMS_URI, SMS_SELECTION, smsSelectionArgs);
+ mContentResolver.insert(SMS_URI, buildMessageContentValues(deviceAddress, message));
+ }
+
+ private int findMessageIndex(String deviceAddress, Message message) {
+ String[] smsSelectionArgs = createSmsSelectionArgs(deviceAddress, message);
+
+ String[] projection = {BaseColumns._ID};
+ Cursor cursor = mContentResolver.query(SMS_URI, projection, SMS_SELECTION,
+ smsSelectionArgs, null /* sortOrder */);
+
+ if (cursor != null && cursor.getCount() != 0) {
+ if (cursor.moveToFirst() && cursor.isLast()) {
+ return getIdOrThrow(cursor);
+ } else {
+ return DUPLICATE_MESSAGES_FOUND;
+ }
+ } else {
+ return MESSAGE_NOT_FOUND;
+ }
+ }
+
+ private int getIdOrThrow(Cursor cursor) {
+ try {
+ int columnIndex = cursor.getColumnIndexOrThrow(BaseColumns._ID);
+ return cursor.getInt(columnIndex);
+ } catch (IllegalArgumentException e) {
+ L.d(TAG, "Could not find _id column: " + e.getMessage());
+ return DATABASE_ERROR;
+ }
+ }
+
+ private void update(int messageIndex, ContentValues value) {
+ final String smsSelection = BaseColumns._ID + "=?";
+ String[] smsSelectionArgs = {Integer.toString(messageIndex)};
+
+ mContentResolver.update(SMS_URI, value, smsSelection, smsSelectionArgs);
+ }
+
+ /** Create the ContentValues object using message info, following SMS columns **/
+ private ContentValues buildMessageContentValues(String deviceAddress, Message message) {
+ ContentValues newMessage = new ContentValues();
+ newMessage.put(Telephony.Sms.BODY, DatabaseUtils.sqlEscapeString(message.getMessageText()));
+ newMessage.put(Telephony.Sms.DATE, message.getReceivedTime());
+ newMessage.put(Telephony.Sms.ADDRESS, deviceAddress);
+ // TODO: if contactId is null, add it.
+ newMessage.put(Telephony.Sms.PERSON,
+ getContactId(mContentResolver,
+ message.getSenderContactUri()));
+ newMessage.put(Telephony.Sms.READ, (message.isReadOnPhone()
+ || message.shouldExcludeFromNotification()));
+ return newMessage;
+ }
+
+ private String[] createSmsSelectionArgs(String deviceAddress, Message message) {
+ String sqlFriendlyMessageText = DatabaseUtils.sqlEscapeString(message.getMessageText());
+ String[] smsSelectionArgs = {deviceAddress, sqlFriendlyMessageText,
+ Long.toString(message.getReceivedTime() - 5000), Long.toString(
+ message.getReceivedTime() + 5000)};
+ return smsSelectionArgs;
+ }
+
+ /** Checks if the application has the needed AppOps permission to write to the Telephony DB. **/
+ private boolean canWriteToDatabase(Context context) {
+ boolean granted = ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_SMS)
+ == PackageManager.PERMISSION_GRANTED;
+
+ AppOpsManager appOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
+ int mode = appOps.checkOpNoThrow(AppOpsManager.OP_WRITE_SMS, android.os.Process.myUid(),
+ context.getPackageName());
+ if (mode != AppOpsManager.MODE_DEFAULT) {
+ granted = (mode == AppOpsManager.MODE_ALLOWED);
+ }
+
+ return granted;
+ }
+
+ // TODO: move out to a shared library.
+ private static int getContactId(ContentResolver cr, String contactUri) {
+ if (TextUtils.isEmpty(contactUri)) {
+ return 0;
+ }
+
+ Uri lookupUri = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI,
+ Uri.encode(contactUri));
+ String[] projection = new String[]{ContactsContract.PhoneLookup._ID};
+
+ try (Cursor cursor = cr.query(lookupUri, projection, null, null, null)) {
+ if (cursor != null && cursor.moveToFirst() && cursor.isLast()) {
+ return cursor.getInt(cursor.getColumnIndex(ContactsContract.PhoneLookup._ID));
+ } else {
+ L.w(TAG, "Unable to find contact id from phone number.");
+ }
+ }
+
+ return 0;
+ }
+}
diff --git a/src/com/android/car/messenger/impl/receivers/SmsReceiver.java b/src/com/android/car/messenger/SmsReceiver.java
index 9150e30..25dbf89 100644
--- a/src/com/android/car/messenger/impl/receivers/SmsReceiver.java
+++ b/src/com/android/car/messenger/SmsReceiver.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2020 The Android Open Source Project
+ * Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,17 +14,21 @@
* limitations under the License.
*/
-package com.android.car.messenger.impl.receivers;
+package com.android.car.messenger;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
-import androidx.annotation.NonNull;
-
-/** No-op Receiver that only exists in order to be eligible to be the default SMS app. */
+/**
+ * No-op Receiver that only exists in order to be eligible to be the default SMS app.
+ */
public class SmsReceiver extends BroadcastReceiver {
@Override
- public void onReceive(@NonNull Context context, @NonNull Intent intent) {}
+ public void onReceive(Context context, Intent intent) {
+ Intent startIntent = new Intent(context, MessengerService.class)
+ .setAction(MessengerService.ACTION_RECEIVED_SMS);
+ context.startForegroundService(startIntent);
+ }
}
diff --git a/src/com/android/car/messenger/bluetooth/BluetoothHelper.java b/src/com/android/car/messenger/bluetooth/BluetoothHelper.java
new file mode 100644
index 0000000..e95b386
--- /dev/null
+++ b/src/com/android/car/messenger/bluetooth/BluetoothHelper.java
@@ -0,0 +1,64 @@
+package com.android.car.messenger.bluetooth;
+
+import android.app.PendingIntent;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothMapClient;
+import android.net.Uri;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * Provides helper methods for performing bluetooth actions.
+ */
+public class BluetoothHelper {
+
+ /**
+ * Returns a (potentially empty) immutable set of bonded (paired) devices.
+ */
+ public static Set<BluetoothDevice> getBondedDevices() {
+ BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+
+ if (adapter != null) {
+ Set<BluetoothDevice> devices = adapter.getBondedDevices();
+ if (devices != null) {
+ return devices;
+ }
+ }
+
+ return Collections.emptySet();
+ }
+
+ /**
+ * Helper method to send an SMS message through bluetooth.
+ *
+ * @param client the MAP Client used to send the message
+ * @param deviceAddress the device used to send the SMS
+ * @param contacts contacts to send the message to
+ * @param message message to send
+ * @param sentIntent callback issued once the message was sent
+ * @param deliveredIntent callback issued once the message was delivered
+ * @return true if the message was enqueued, false on error
+ * @throws IllegalArgumentException if deviceAddress is invalid
+ */
+ public static boolean sendMessage(@NonNull BluetoothMapClient client,
+ String deviceAddress,
+ Uri[] contacts,
+ String message,
+ @Nullable PendingIntent sentIntent,
+ @Nullable PendingIntent deliveredIntent)
+ throws IllegalArgumentException {
+
+ BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+ if (adapter == null) {
+ return false;
+ }
+ BluetoothDevice device = adapter.getRemoteDevice(deviceAddress);
+
+ return client.sendMessage(device, contacts, message, sentIntent, deliveredIntent);
+ }
+}
diff --git a/src/com/android/car/messenger/bluetooth/BluetoothMonitor.java b/src/com/android/car/messenger/bluetooth/BluetoothMonitor.java
new file mode 100644
index 0000000..095474f
--- /dev/null
+++ b/src/com/android/car/messenger/bluetooth/BluetoothMonitor.java
@@ -0,0 +1,329 @@
+package com.android.car.messenger.bluetooth;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothMapClient;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.SdpMasRecord;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Parcelable;
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+import com.android.car.messenger.log.L;
+import java.util.HashSet;
+import java.util.Set;
+
+
+/**
+ * Provides a callback interface for subscribers to be notified of bluetooth MAP/SDP changes.
+ */
+public class BluetoothMonitor {
+ private static final String TAG = "CM.BluetoothMonitor";
+
+ private final Context mContext;
+ private final BluetoothMapReceiver mBluetoothMapReceiver;
+ private final BluetoothSdpReceiver mBluetoothSdpReceiver;
+ private final MapDeviceMonitor mMapDeviceMonitor;
+ private final BluetoothProfile.ServiceListener mMapServiceListener;
+ private BluetoothMapClient mBluetoothMapClient;
+
+ private final Set<OnBluetoothEventListener> mListeners;
+
+ public BluetoothMonitor(@NonNull Context context) {
+ mContext = context;
+ mBluetoothMapReceiver = new BluetoothMapReceiver();
+ mBluetoothSdpReceiver = new BluetoothSdpReceiver();
+ mMapDeviceMonitor = new MapDeviceMonitor();
+ mMapServiceListener = new BluetoothProfile.ServiceListener() {
+ @Override
+ public void onServiceConnected(int profile, BluetoothProfile proxy) {
+ L.d(TAG, "Connected to MAP service!");
+ onMapConnected((BluetoothMapClient) proxy);
+ }
+
+ @Override
+ public void onServiceDisconnected(int profile) {
+ L.d(TAG, "Disconnected from MAP service!");
+ onMapDisconnected();
+ }
+ };
+ mListeners = new HashSet<>();
+ connectToMap();
+ }
+
+ /**
+ * Registers a listener to receive Bluetooth MAP events.
+ * If this listener is already registered, calling this method has no effect.
+ *
+ * @param listener the listener to register
+ * @return true if this listener was not already registered
+ */
+ public boolean registerListener(@NonNull OnBluetoothEventListener listener) {
+ return mListeners.add(listener);
+ }
+
+ /**
+ * Unregisters a listener from receiving Bluetooth MAP events.
+ * If this listener is not registered, calling this method has no effect.
+ *
+ * @param listener the listener to unregister
+ * @return true if the set of registered listeners contained this listener
+ */
+ public boolean unregisterListener(OnBluetoothEventListener listener) {
+ return mListeners.remove(listener);
+ }
+
+ public interface OnBluetoothEventListener {
+ /**
+ * Callback issued when a new message was received.
+ *
+ * @param intent intent containing the message details
+ */
+ void onMessageReceived(Intent intent);
+
+ /**
+ * Callback issued when a new message was sent successfully.
+ *
+ * @param intent intent containing the message details
+ */
+ void onMessageSent(Intent intent);
+
+ /**
+ * Callback issued when a new device has connected to bluetooth.
+ *
+ * @param device the connected device
+ */
+ void onDeviceConnected(BluetoothDevice device);
+
+ /**
+ * Callback issued when a previously connected device has disconnected from bluetooth.
+ *
+ * @param device the disconnected device
+ */
+ void onDeviceDisconnected(BluetoothDevice device);
+
+ /**
+ * Callback issued when a new MAP client has been connected.
+ *
+ * @param client the MAP client
+ */
+ void onMapConnected(BluetoothMapClient client);
+
+ /**
+ * Callback issued when a MAP client has been disconnected.
+ */
+ void onMapDisconnected();
+
+ /**
+ * Callback issued when a new SDP record has been detected.
+ *
+ * @param device the device detected
+ * @param supportsReply true if the device supports SMS replies through bluetooth
+ */
+ void onSdpRecord(BluetoothDevice device, boolean supportsReply);
+ }
+
+ private void onMessageReceived(Intent intent) {
+ mListeners.forEach(listener -> listener.onMessageReceived(intent));
+ }
+
+ private void onMessageSent(Intent intent) {
+ mListeners.forEach(listener -> listener.onMessageSent(intent));
+ }
+
+ private void onDeviceConnected(BluetoothDevice device) {
+ mListeners.forEach(listener -> listener.onDeviceConnected(device));
+ }
+
+ private void onDeviceDisconnected(BluetoothDevice device) {
+ mListeners.forEach(listener -> listener.onDeviceDisconnected(device));
+ }
+
+ private void onMapConnected(BluetoothMapClient client) {
+ mBluetoothMapClient = client;
+ mListeners.forEach(listener -> listener.onMapConnected(client));
+ }
+
+ private void onMapDisconnected() {
+ mBluetoothMapClient = null;
+ mListeners.forEach(listener -> listener.onMapDisconnected());
+ }
+
+ private void onSdpRecord(BluetoothDevice device, boolean supportsReply) {
+ mListeners.forEach(listener -> listener.onSdpRecord(device, supportsReply));
+ }
+
+ /** Connects to the MAP client. */
+ private void connectToMap() {
+ L.d(TAG, "Connecting to MAP service");
+
+ BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+ if (adapter == null) {
+ // This can happen on devices that don't support Bluetooth.
+ L.e(TAG, "BluetoothAdapter is null! Unable to connect to MAP client.");
+ return;
+ }
+
+ if (!adapter.getProfileProxy(mContext, mMapServiceListener, BluetoothProfile.MAP_CLIENT)) {
+ // This *should* never happen. Unless arguments passed are incorrect somehow...
+ L.wtf(TAG, "Unable to get MAP profile!");
+ return;
+ }
+ }
+
+ /**
+ * Performs {@link Context} related cleanup (such as unregistering from receivers).
+ */
+ public void onDestroy() {
+ BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+ if (adapter != null) {
+ adapter.closeProfileProxy(BluetoothProfile.MAP_CLIENT, mBluetoothMapClient);
+ }
+ onMapDisconnected();
+ mListeners.clear();
+ mBluetoothMapReceiver.unregisterReceivers();
+ mBluetoothSdpReceiver.unregisterReceivers();
+ mMapDeviceMonitor.unregisterReceivers();
+ }
+
+ @VisibleForTesting
+ BluetoothProfile.ServiceListener getServiceListener() {
+ return mMapServiceListener;
+ }
+
+ /** Monitors for new device connections and disconnections */
+ private class MapDeviceMonitor extends BroadcastReceiver {
+ MapDeviceMonitor() {
+ L.d(TAG, "Registering Map device monitor");
+
+ IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(BluetoothMapClient.ACTION_CONNECTION_STATE_CHANGED);
+ mContext.registerReceiver(this, intentFilter,
+ android.Manifest.permission.BLUETOOTH, null);
+ }
+
+ void unregisterReceivers() {
+ mContext.unregisterReceiver(this);
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final int STATE_NOT_FOUND = -1;
+ int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, STATE_NOT_FOUND);
+ int previousState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE,
+ STATE_NOT_FOUND);
+
+ BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+
+ if (state == STATE_NOT_FOUND || previousState == STATE_NOT_FOUND || device == null) {
+ L.w(TAG, "Skipping broadcast, missing required extra");
+ return;
+ }
+
+ if (previousState == BluetoothProfile.STATE_CONNECTED
+ && state != BluetoothProfile.STATE_CONNECTED) {
+ L.d(TAG, "Device losing MAP connection: %s", device);
+
+ onDeviceDisconnected(device);
+ }
+
+ if (previousState == BluetoothProfile.STATE_CONNECTING
+ && state == BluetoothProfile.STATE_CONNECTED) {
+ L.d(TAG, "Device connected: %s", device);
+
+ onDeviceConnected(device);
+ }
+ }
+ }
+
+ /** Monitors for new incoming messages and sent-message broadcast. */
+ private class BluetoothMapReceiver extends BroadcastReceiver {
+ BluetoothMapReceiver() {
+ L.d(TAG, "Registering receiver for bluetooth MAP");
+
+ IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(BluetoothMapClient.ACTION_MESSAGE_SENT_SUCCESSFULLY);
+ intentFilter.addAction(BluetoothMapClient.ACTION_MESSAGE_RECEIVED);
+ mContext.registerReceiver(this, intentFilter);
+ }
+
+ void unregisterReceivers() {
+ mContext.unregisterReceiver(this);
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+
+ if (intent == null || intent.getAction() == null) return;
+
+ switch (intent.getAction()) {
+ case BluetoothMapClient.ACTION_MESSAGE_SENT_SUCCESSFULLY:
+ L.d(TAG, "SMS sent successfully.");
+ onMessageSent(intent);
+ break;
+ case BluetoothMapClient.ACTION_MESSAGE_RECEIVED:
+ L.d(TAG, "SMS message received.");
+ onMessageReceived(intent);
+ break;
+ default:
+ L.w(TAG, "Ignoring unknown broadcast %s", intent.getAction());
+ break;
+ }
+ }
+ }
+
+ /** Monitors for new SDP records */
+ private class BluetoothSdpReceiver extends BroadcastReceiver {
+
+ // reply or "upload" feature is indicated by the 3rd bit
+ private static final int REPLY_FEATURE_FLAG_POSITION = 3;
+ private static final int REPLY_FEATURE_MIN_VERSION = 0x102;
+
+ BluetoothSdpReceiver() {
+ L.d(TAG, "Registering receiver for sdp");
+
+ IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(BluetoothDevice.ACTION_SDP_RECORD);
+ mContext.registerReceiver(this, intentFilter);
+ }
+
+ void unregisterReceivers() {
+ mContext.unregisterReceiver(this);
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (BluetoothDevice.ACTION_SDP_RECORD.equals(intent.getAction())) {
+ L.d(TAG, "get SDP record: %s", intent.getExtras());
+
+ Parcelable parcelable = intent.getParcelableExtra(BluetoothDevice.EXTRA_SDP_RECORD);
+ if (!(parcelable instanceof SdpMasRecord)) {
+ L.d(TAG, "not SdpMasRecord: %s", parcelable);
+ return;
+ }
+
+ SdpMasRecord masRecord = (SdpMasRecord) parcelable;
+ BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+ onSdpRecord(device, supportsReply(masRecord));
+ } else {
+ L.w(TAG, "Ignoring unknown broadcast %s", intent.getAction());
+ }
+ }
+
+ private boolean isOn(int input, int position) {
+ return ((input >> position) & 1) == 1;
+ }
+
+ private boolean supportsReply(@NonNull SdpMasRecord masRecord) {
+ final int version = masRecord.getProfileVersion();
+ final int features = masRecord.getSupportedFeatures();
+ // We only consider the device as supporting the reply feature if the version
+ // is 1.02 at minimum and the feature flag is turned on.
+ return version >= REPLY_FEATURE_MIN_VERSION
+ && isOn(features, REPLY_FEATURE_FLAG_POSITION);
+ }
+ }
+}
diff --git a/src/com/android/car/messenger/core/interfaces/AppFactory.java b/src/com/android/car/messenger/core/interfaces/AppFactory.java
deleted file mode 100644
index a88078d..0000000
--- a/src/com/android/car/messenger/core/interfaces/AppFactory.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.messenger.core.interfaces;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-
-import androidx.annotation.NonNull;
-
-/**
- * The AppFactory provides singleton instances to be used throughout the app.
- *
- * <p>The Factory implementation points to the UI core library interfaces.
- *
- * <p>Once the Factory implementation is initialized with {@link #setInstance(AppFactory)}, the
- * library interface implementations can be accessed anywhere throughout the application.
- */
-public abstract class AppFactory {
- @NonNull private static AppFactory sInstance;
- protected static boolean sRegistered;
- protected static boolean sInitialized;
-
- /** Returns the Factory instance for the Application. */
- @NonNull
- public static AppFactory get() {
- return sInstance;
- }
-
- /**
- * Sets the Factory instance.
- *
- * <p>This is called when the application starts, in onCreate of the custom Application class
- */
- protected static void setInstance(@NonNull final AppFactory factory) {
- // Not allowed to call this after real application initialization is complete
- if (sRegistered && sInitialized) {
- return;
- }
- sInstance = factory;
- }
-
- /** Returns context most appropriate for UI context-requiring tasks. */
- @NonNull
- public abstract Context getContext();
-
- /**
- * Perhaps the single most important methods to implement, this provides the data source for the
- * app.
- */
- @NonNull
- public abstract DataModel getDataModel();
-
- /** Returns the shared preference instance for the app */
- @NonNull
- public abstract SharedPreferences getSharedPreferences();
-}
diff --git a/src/com/android/car/messenger/core/interfaces/DataModel.java b/src/com/android/car/messenger/core/interfaces/DataModel.java
deleted file mode 100644
index 3b4ad75..0000000
--- a/src/com/android/car/messenger/core/interfaces/DataModel.java
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.messenger.core.interfaces;
-
-import androidx.lifecycle.LiveData;
-
-import androidx.annotation.NonNull;
-
-import com.android.car.messenger.common.Conversation;
-import com.android.car.messenger.core.models.UserAccount;
-
-import java.util.Collection;
-
-/**
- * This interface allows the UI to communicate with the host app. The methods provides the data and
- * actions needed by the UI library. Message Interface Channel should be implemented by the host
- * app. Method calls are done on the main thread. Extensive data gathering work should be delegated
- * to a background thread and the UI library can be notified once the data is ready via the change
- * listener.
- */
-public interface DataModel {
-
- /**
- * Get list of accounts. Here an account can refer to actual accounts or separate user accounts.
- * Data will be separated in the UI by user accounts.
- */
- @NonNull
- LiveData<Collection<UserAccount>> getAccounts();
-
- /**
- * Get collection of conversations for the given account.
- *
- * @param userAccount The account to which data is being queried. This could be the subscription
- * id matching a sim in multi-account setting or account id with multi-user account
- */
- @NonNull
- LiveData<Collection<Conversation>> getConversations(@NonNull UserAccount userAccount);
-
- /**
- * Returns an observable conversation item, holding only unread messages. since the last known
- * {@link UserAccount#getConnectionTime}.
- *
- * <p>If no unread messages are found for the conversation id, the live data emits no data.
- */
- LiveData<Conversation> getUnreadMessages();
-
- /**
- * Called by UI to mute all notifications for this conversation
- *
- * @param conversationId The unique id for the conversation
- * @param mute The requested mute action, false is to unmute, true is to mute
- */
- void muteConversation(@NonNull String conversationId, boolean mute);
-
- /**
- * Called by UI to mark conversation as read
- *
- * @param conversationId The unique id for the conversation
- */
- void markAsRead(@NonNull String conversationId);
-
- /**
- * Called by UI to send a message
- *
- * @param conversationId The phone number to send message
- * @param message The desired message to send to conversation thread
- */
- void sendMessage(@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/models/ConnectionStatus.java b/src/com/android/car/messenger/core/models/ConnectionStatus.java
deleted file mode 100644
index 375dfde..0000000
--- a/src/com/android/car/messenger/core/models/ConnectionStatus.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.messenger.core.models;
-
-/** Connection status for a user account. */
-public enum ConnectionStatus {
- /**
- * Represents a state when the user account is fully setup to retrieve data. This could be a
- * bluetooth connection or logged in account.
- */
- CONNECTED,
- /**
- * Represents a state when the user account is disconnected or logged out. This could occur when
- * a phone is disconnected or unpaired or a user is logged out from a messaging account
- */
- DISCONNECTED,
-}
diff --git a/src/com/android/car/messenger/core/models/UserAccount.java b/src/com/android/car/messenger/core/models/UserAccount.java
deleted file mode 100644
index 406d60a..0000000
--- a/src/com/android/car/messenger/core/models/UserAccount.java
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.messenger.core.models;
-
-import android.os.Parcel;
-import android.os.Parcelable;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import java.time.Instant;
-
-/**
- * A user account can represent a bluetooth connection to a phone sim or a user account. There can
- * be more than one user accounts during a drive. The user id can be used to retrieve data for that
- * specific account. The user name can be used to display the user the name of the account/account.
- */
-public class UserAccount implements Parcelable {
- private final int mId;
- @Nullable private final String mIccId;
- @NonNull private final String mName;
- @NonNull private final Instant mConnectionTime;
-
- public UserAccount(
- int id, @NonNull String name, @Nullable String iccId, @NonNull Instant connectionTime) {
- mId = id;
- mIccId = iccId;
- mName = name;
- mConnectionTime = connectionTime;
- }
-
- protected UserAccount(Parcel in) {
- this.mId = in.readInt();
- this.mIccId = in.readString();
- this.mName = in.readString();
- this.mConnectionTime = (Instant) in.readSerializable();
- }
-
- @NonNull
- public static final Creator<UserAccount> CREATOR =
- new Creator<UserAccount>() {
- @Override
- public UserAccount createFromParcel(@NonNull Parcel source) {
- return new UserAccount(source);
- }
-
- @Override
- public UserAccount[] newArray(int size) {
- return new UserAccount[size];
- }
- };
-
- /**
- * The user id can be used to retrieve data for that specific account.
- *
- * @return the unique identifier for the user account
- */
- public int getId() {
- return mId;
- }
-
- /**
- * The IccId is a globally unique serial number—a one-of-a-kind signature that identifies the
- * SIM card itself or bluetooth address of the account.
- *
- * <p>For device/account disambiguation for Contact db queries, this field maps to {@link
- * android.provider.ContactsContract.RawContacts#ACCOUNT_NAME} in the Contacts database.
- *
- * @return The id or null, if not set
- */
- @Nullable
- public String getIccId() {
- return mIccId;
- }
-
- /** The display name for the account or account. */
- @NonNull
- public String getName() {
- return mName;
- }
-
- /** Returns the {@link Instant} the car was connected to this {@link UserAccount} */
- @NonNull
- public Instant getConnectionTime() {
- return mConnectionTime;
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeInt(this.mId);
- dest.writeString(mIccId);
- dest.writeString(this.mName);
- dest.writeSerializable(this.mConnectionTime);
- }
-}
diff --git a/src/com/android/car/messenger/core/service/MessengerService.java b/src/com/android/car/messenger/core/service/MessengerService.java
deleted file mode 100644
index 60cbfe5..0000000
--- a/src/com/android/car/messenger/core/service/MessengerService.java
+++ /dev/null
@@ -1,181 +0,0 @@
-/*
- * 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.service;
-
-import static com.android.car.messenger.core.shared.MessageConstants.ACTION_DIRECT_SEND;
-import static com.android.car.messenger.core.shared.MessageConstants.ACTION_MARK_AS_READ;
-import static com.android.car.messenger.core.shared.MessageConstants.ACTION_MUTE;
-import static com.android.car.messenger.core.shared.MessageConstants.ACTION_REPLY;
-
-import android.app.Notification;
-import android.app.NotificationChannel;
-import android.app.NotificationManager;
-import android.app.Service;
-import android.content.Intent;
-import android.media.AudioAttributes;
-import android.os.Binder;
-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 com.android.car.messenger.R;
-import com.android.car.messenger.core.interfaces.AppFactory;
-import com.android.car.messenger.core.shared.NotificationHandler;
-import com.android.car.messenger.core.util.L;
-import com.android.car.messenger.core.util.VoiceUtil;
-
-/** Service responsible for handling messaging events. */
-public class MessengerService extends Service {
- /* ACTIONS */
- /** Used to start this service at boot-complete. Takes no arguments. */
- @NonNull public static final String ACTION_START = "com.android.car.messenger.ACTION_START";
-
- /* EXTRAS */
- /* NOTIFICATIONS */
- @NonNull public static final String MESSAGE_CHANNEL_ID = "MESSAGE_CHANNEL_ID";
- @NonNull public static final String SILENT_MESSAGE_CHANNEL_ID = "SILENT_MESSAGE_CHANNEL_ID";
- @NonNull public static final String APP_RUNNING_CHANNEL_ID = "APP_RUNNING_CHANNEL_ID";
- private static final int SERVICE_STARTED_NOTIFICATION_ID = Integer.MAX_VALUE;
-
- /* Binding boilerplate */
- @NonNull private final IBinder mBinder = new LocalBinder();
-
- /** Local Binder For {@link MessengerService} */
- public class LocalBinder extends Binder {
- /** Returns {@link MessengerService} */
- @NonNull
- public MessengerService getService() {
- return MessengerService.this;
- }
- }
-
- @Override
- @NonNull
- public IBinder onBind(@NonNull Intent intent) {
- return mBinder;
- }
-
- @Override
- public void onCreate() {
- super.onCreate();
- L.d("MessengerService - onCreate");
- AppFactory.get()
- .getDataModel()
- .getUnreadMessages()
- .observeForever(NotificationHandler::postOrRemoveNotification);
- sendServiceRunningNotification();
- }
-
- private void sendServiceRunningNotification() {
- NotificationManager notificationManager = getSystemService(NotificationManager.class);
-
- if (notificationManager == null) {
- L.e("Failed to get NotificationManager instance");
- return;
- }
-
- // Create notification channel for app running notification
- {
- NotificationChannel appRunningNotificationChannel =
- new NotificationChannel(
- APP_RUNNING_CHANNEL_ID,
- getString(R.string.app_running_msg_notification_title),
- NotificationManager.IMPORTANCE_LOW);
- notificationManager.createNotificationChannel(appRunningNotificationChannel);
- }
-
- // Create notification channel for notifications that should be posted silently in the
- // notification center, without a heads up notification.
- {
- NotificationChannel silentNotificationChannel =
- new NotificationChannel(
- SILENT_MESSAGE_CHANNEL_ID,
- getString(R.string.message_channel_description),
- NotificationManager.IMPORTANCE_LOW);
- notificationManager.createNotificationChannel(silentNotificationChannel);
- }
-
- {
- AudioAttributes attributes =
- new AudioAttributes.Builder()
- .setUsage(AudioAttributes.USAGE_NOTIFICATION)
- .build();
- NotificationChannel channel =
- new NotificationChannel(
- MESSAGE_CHANNEL_ID,
- getString(R.string.message_channel_name),
- NotificationManager.IMPORTANCE_HIGH);
- channel.setDescription(getString(R.string.message_channel_description));
- channel.setSound(Settings.System.DEFAULT_NOTIFICATION_URI, attributes);
- notificationManager.createNotificationChannel(channel);
- }
-
- final Notification notification =
- new NotificationCompat.Builder(this, APP_RUNNING_CHANNEL_ID)
- .setSmallIcon(R.drawable.ic_message)
- .setContentTitle(getString(R.string.app_running_msg_notification_title))
- .setContentText(getString(R.string.app_running_msg_notification_content))
- .build();
-
- startForeground(SERVICE_STARTED_NOTIFICATION_ID, notification);
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- L.d("onDestroy");
- }
-
- @Override
- public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
- final int result = START_STICKY;
-
- if (intent == null || intent.getAction() == null) {
- return result;
- }
-
- final String action = intent.getAction();
- switch (action) {
- case ACTION_START:
- // NO-OP
- break;
- case ACTION_REPLY:
- VoiceUtil.voiceReply(intent);
- break;
- case ACTION_MUTE:
- VoiceUtil.mute(intent);
- break;
- case ACTION_MARK_AS_READ:
- VoiceUtil.markAsRead(intent);
- break;
- case ACTION_DIRECT_SEND:
- VoiceUtil.directSend(intent);
- break;
- case TelephonyManager.ACTION_RESPOND_VIA_MESSAGE:
- // Not currently supported. This was added to allow CarMessenger become the default
- // SMS app.
- break;
- default:
- L.w("Unsupported action: " + action);
- }
- return result;
- }
-}
diff --git a/src/com/android/car/messenger/core/service/OnBootReceiver.java b/src/com/android/car/messenger/core/service/OnBootReceiver.java
deleted file mode 100644
index fdadfe0..0000000
--- a/src/com/android/car/messenger/core/service/OnBootReceiver.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * 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.service;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-
-import androidx.annotation.NonNull;
-
-import com.android.car.messenger.core.util.L;
-
-/**
- * Receiver that listens for on boot completed broadcast intent and starts {@link MessengerService}.
- */
-public class OnBootReceiver extends BroadcastReceiver {
-
- @Override
- public void onReceive(@NonNull Context context, @NonNull Intent intent) {
- L.d("BootReceiver received!");
- if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
- context.startService(new Intent(context, MessengerService.class));
- }
- }
-}
diff --git a/src/com/android/car/messenger/core/shared/MessageConstants.java b/src/com/android/car/messenger/core/shared/MessageConstants.java
deleted file mode 100644
index 97f6f88..0000000
--- a/src/com/android/car/messenger/core/shared/MessageConstants.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.messenger.core.shared;
-
-import androidx.annotation.NonNull;
-
-/** Constants. */
-public final class MessageConstants {
-
- private MessageConstants() {}
-
- /** See {@link android.content.res.Resources#getIdentifier(String, String, String)} */
- public static final int INVALID_RES_ID = 0;
-
- /**
- * The key in Default Shared Preferences that maps to a list of conversation ids that are muted
- */
- @NonNull public static final String KEY_MUTED_CONVERSATIONS = "KEY_MUTED_CONVERSATIONS";
-
- /**
- * This is added as an extra in the {@link com.android.car.messenger.common.Conversation} to
- * indicate what the last reply timestamp is, if any
- */
- @NonNull public static final String LAST_REPLY_TIMESTAMP_EXTRA = "LAST_REPLY_TIMESTAMP_EXTRA";
-
- /** Used to reply to message. */
- @NonNull public static final String ACTION_REPLY = "ACTION_REPLY";
-
- /** Used to mark a conversation as read */
- @NonNull public static final String ACTION_MARK_AS_READ = "ACTION_MARK_AS_READ";
-
- /** Used to direct send to a specified phone number */
- @NonNull public static final String ACTION_DIRECT_SEND = "ACTION_DIRECT_SEND";
-
- /** Used to mute a conversation */
- @NonNull public static final String ACTION_MUTE = "ACTION_MUTE";
-
- /* EXTRAS */
- /** Key under which the {@link String} is provided. */
- @NonNull public static final String EXTRA_CONVERSATION_KEY = "EXTRA_CONVERSATION_KEY";
-}
diff --git a/src/com/android/car/messenger/core/shared/NotificationHandler.java b/src/com/android/car/messenger/core/shared/NotificationHandler.java
deleted file mode 100644
index c326c1f..0000000
--- a/src/com/android/car/messenger/core/shared/NotificationHandler.java
+++ /dev/null
@@ -1,140 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.messenger.core.shared;
-
-import android.app.Notification;
-import android.app.NotificationManager;
-import android.content.Context;
-import android.service.notification.StatusBarNotification;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.car.assist.payloadhandlers.ConversationPayloadHandler;
-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.util.VoiceUtil;
-
-/** Useful notification handler for posting messages */
-public class NotificationHandler {
- @NonNull
- private static final String GROUP_TAP_TO_READ_NOTIFICATION =
- "com.android.car.messenger.TAP_TO_READ";
-
- private static final int TAP_TO_READ_SBN_ATTEMPT_LIMIT = 3;
-
- private NotificationHandler() {}
-
- /** Posts, removes or updates a notification based on a conversation */
- public static void postOrRemoveNotification(Conversation conversation) {
- if (conversation.isMuted()) {
- removeNotification(conversation);
- } else {
- postNotification(conversation);
- }
- }
-
- /* Posts or updates a notification based on a conversation */
- private static void postNotification(@NonNull Conversation conversation) {
- Conversation tapToReadConversation = VoiceUtil.createTapToReadConversation(conversation);
- Context context = AppFactory.get().getContext();
- NotificationManager notificationManager =
- context.getSystemService(NotificationManager.class);
- String channelId = MessengerService.MESSAGE_CHANNEL_ID;
- Notification notification =
- ConversationPayloadHandler.createNotificationFromConversation(
- context, channelId, tapToReadConversation, R.drawable.ic_message, null);
-
- notificationManager.notify(tapToReadConversation.getId().hashCode(), notification);
- }
-
- /**
- * Posts a notification in the foreground for Tap To Read
- *
- * <p>This is useful as legacy digital assistant implementations of Tap To Read require a {@link
- * StatusBarNotification} in order to fulfill a tap to read request.
- *
- * <p>This notification is invisible to the user but accessible by digital assistants.
- *
- * @return the StatusBarNotification posted by the system for this notification, or null if not
- * found after a limited attempt at retrieval
- */
- @Nullable
- public static StatusBarNotification postNotificationForLegacyTapToRead(
- @NonNull Conversation tapToReadConversation) {
- Context context = AppFactory.get().getContext();
- // cancel any other notifications within group.
- // There should be only notification in group at a time.
- cancelAllTapToReadNotifications(context);
- // Post as a foreground service:
- // Foreground notifications by system apps with low priority
- // are hidden from user view, which is desired
- Notification notification =
- ConversationPayloadHandler.createNotificationFromConversation(
- context,
- MessengerService.APP_RUNNING_CHANNEL_ID,
- tapToReadConversation,
- context.getApplicationInfo().icon,
- GROUP_TAP_TO_READ_NOTIFICATION);
- int id = (GROUP_TAP_TO_READ_NOTIFICATION + tapToReadConversation.getId()).hashCode();
- NotificationManager notificationManager =
- context.getSystemService(NotificationManager.class);
- notificationManager.notify(id, notification);
-
- // attempt to retrieve the status bar notification based on the notification
- // limit attempts
- int tries = 0;
- StatusBarNotification sbn;
- do {
- sbn = findSBN(notificationManager, id);
- tries++;
- } while (sbn == null && tries < TAP_TO_READ_SBN_ATTEMPT_LIMIT);
- return sbn;
- }
-
- /** Cancels all Tap To Read Notifications */
- public static void cancelAllTapToReadNotifications(@NonNull Context context) {
- NotificationManager notificationManager =
- context.getSystemService(NotificationManager.class);
- for (StatusBarNotification sbn : notificationManager.getActiveNotifications()) {
- if (GROUP_TAP_TO_READ_NOTIFICATION.equals(sbn.getNotification().getGroup())) {
- notificationManager.cancel(sbn.getId());
- }
- }
- }
-
- /** Returns the {@link StatusBarNotification} with desired id, or null if none found */
- private static StatusBarNotification findSBN(
- @NonNull NotificationManager notificationManager, int id) {
- for (StatusBarNotification sbn : notificationManager.getActiveNotifications()) {
- if (sbn.getId() == id) {
- return sbn;
- }
- }
- return null;
- }
-
- /* Removes a notification based on a conversation */
- private static void removeNotification(@NonNull Conversation conversation) {
- Context context = AppFactory.get().getContext();
- NotificationManager notificationManager =
- context.getSystemService(NotificationManager.class);
- notificationManager.cancel(conversation.getId().hashCode());
- }
-}
diff --git a/src/com/android/car/messenger/core/ui/base/MessageListBaseFragment.java b/src/com/android/car/messenger/core/ui/base/MessageListBaseFragment.java
deleted file mode 100644
index e585dd1..0000000
--- a/src/com/android/car/messenger/core/ui/base/MessageListBaseFragment.java
+++ /dev/null
@@ -1,125 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.messenger.core.ui.base;
-
-import android.content.Context;
-import android.os.Bundle;
-import androidx.fragment.app.Fragment;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import androidx.annotation.CallSuper;
-import androidx.annotation.LayoutRes;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.core.content.ContextCompat;
-
-import com.android.car.messenger.R;
-import com.android.car.messenger.core.ui.shared.LoadingFrameLayout;
-import com.android.car.ui.baselayout.Insets;
-import com.android.car.ui.baselayout.InsetsChangedListener;
-import com.android.car.ui.core.CarUi;
-import com.android.car.ui.recyclerview.CarUiRecyclerView;
-import com.android.car.ui.toolbar.Toolbar.State;
-import com.android.car.ui.toolbar.ToolbarController;
-
-/**
- * Base fragment that inflates a {@link RecyclerView}. It handles the top offset for first row item
- * so the list can scroll underneath the top bar.
- */
-public class MessageListBaseFragment extends Fragment implements InsetsChangedListener {
-
- @NonNull protected LoadingFrameLayout mLoadingFrameLayout;
- @NonNull private CarUiRecyclerView mRecyclerView;
- @Nullable protected ToolbarController mToolbar;
-
- @Override
- public View onCreateView(
- LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
- View view = inflater.inflate(getLayoutResource(), container, false);
- mLoadingFrameLayout = view.findViewById(R.id.loading_frame_layout);
- mRecyclerView = view.requireViewById(R.id.list_view);
- mRecyclerView.setLayoutManager(createLayoutManager());
- return view;
- }
-
- /** Layout resource for this fragment. It must contains a RecyclerView with id list_view. */
- @LayoutRes
- protected int getLayoutResource() {
- return R.layout.loading_list_fragment;
- }
-
- /**
- * Creates the layout manager for the recycler view. Default is a {@link LinearLayoutManager}.
- * Child inheriting from this fragment can override to create a different layout manager.
- */
- @NonNull
- protected RecyclerView.LayoutManager createLayoutManager() {
- return new LinearLayoutManager(getContext());
- }
-
- /** Returns the {@link RecyclerView} instance. */
- @NonNull
- protected CarUiRecyclerView getRecyclerView() {
- return mRecyclerView;
- }
-
- @CallSuper
- @Override
- public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
- mToolbar = CarUi.getToolbar(requireActivity());
- // Null check for unit tests to pass
- if (mToolbar != null) {
- setupToolbar(mToolbar);
- }
- Insets insets = CarUi.getInsets(requireActivity());
- // Null check for unit tests to pass
- if (insets != null) {
- onCarUiInsetsChanged(insets);
- }
- }
-
- /** Customizes the tool bar. Can be overridden in subclasses. */
- protected void setupToolbar(@NonNull ToolbarController toolbar) {
- Context context = getContext();
- if (context == null) {
- return;
- }
- toolbar.setTitle(R.string.app_name);
- toolbar.setState(getToolbarState());
- toolbar.setLogo(
- getToolbarState() == State.HOME
- ? ContextCompat.getDrawable(context, context.getApplicationInfo().icon)
- : null);
- }
-
- @NonNull
- protected State getToolbarState() {
- return State.HOME;
- }
-
- @Override
- public void onCarUiInsetsChanged(Insets insets) {
- int listTopPadding =
- requireContext().getResources().getDimensionPixelSize(R.dimen.list_top_padding);
- mRecyclerView.setPadding(0, insets.getTop() + listTopPadding, 0, insets.getBottom());
- }
-}
diff --git a/src/com/android/car/messenger/core/ui/conversationlist/ConversationItemAdapter.java b/src/com/android/car/messenger/core/ui/conversationlist/ConversationItemAdapter.java
deleted file mode 100644
index b54c82c..0000000
--- a/src/com/android/car/messenger/core/ui/conversationlist/ConversationItemAdapter.java
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.messenger.core.ui.conversationlist;
-
-import android.content.Context;
-import androidx.recyclerview.widget.RecyclerView;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import androidx.annotation.NonNull;
-
-import com.android.car.messenger.R;
-import com.android.car.messenger.common.Conversation;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/** Adapter for conversation log list. */
-public class ConversationItemAdapter extends RecyclerView.Adapter<ConversationItemViewHolder> {
- /** Item Click listener for when an item on the UI is tapped */
- public interface OnConversationItemClickListener {
- /** Callback to start tap to read voice interaction for conversation item */
- void onConversationItemClicked(@NonNull Conversation conversation);
- /** Callback to start tap to reply voice interaction for conversation item */
- void onReplyIconClicked(@NonNull Conversation conversation);
- }
-
- @NonNull private final List<UIConversationItem> mUIConversationItems = new ArrayList<>();
- @NonNull private final OnConversationItemClickListener mOnConversationItemClickListener;
-
- public ConversationItemAdapter(
- @NonNull OnConversationItemClickListener onConversationItemClickListener) {
- mOnConversationItemClickListener = onConversationItemClickListener;
- }
-
- /** Sets conversation logs. */
- public void setConversationLogItems(@NonNull List<UIConversationItem> uIConversationItems) {
- mUIConversationItems.clear();
- mUIConversationItems.addAll(uIConversationItems);
- notifyDataSetChanged();
- }
-
- @NonNull
- @Override
- public ConversationItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
- Context context = parent.getContext();
- View rootView =
- LayoutInflater.from(context)
- .inflate(R.layout.conversation_list_item, parent, false);
- return new ConversationItemViewHolder(rootView, mOnConversationItemClickListener);
- }
-
- @Override
- public void onBindViewHolder(@NonNull ConversationItemViewHolder holder, int position) {
- holder.bind(mUIConversationItems.get(position));
- }
-
- @Override
- public void onViewRecycled(@NonNull ConversationItemViewHolder holder) {
- holder.recycle();
- }
-
- @Override
- public int getItemCount() {
- return mUIConversationItems.size();
- }
-}
diff --git a/src/com/android/car/messenger/core/ui/conversationlist/ConversationItemViewHolder.java b/src/com/android/car/messenger/core/ui/conversationlist/ConversationItemViewHolder.java
deleted file mode 100644
index 09f071f..0000000
--- a/src/com/android/car/messenger/core/ui/conversationlist/ConversationItemViewHolder.java
+++ /dev/null
@@ -1,155 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.messenger.core.ui.conversationlist;
-
-import android.graphics.Color;
-import android.graphics.PorterDuff;
-import android.graphics.PorterDuffColorFilter;
-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;
-import com.android.car.messenger.core.interfaces.AppFactory;
-import com.android.car.messenger.core.interfaces.DataModel;
-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.ViewUtils;
-
-/**
- * {@link RecyclerView.ViewHolder} for Conversation Log item, responsible for presenting and
- * resetting the UI on recycle.
- */
-public class ConversationItemViewHolder extends RecyclerView.ViewHolder {
- @NonNull private final DataModel mDataModel;
-
- @NonNull
- private final ConversationItemAdapter.OnConversationItemClickListener
- mOnConversationItemClickListener;
-
- @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 mDotSeparatorView;
- @NonNull private final ImageView mSubtitleIconView;
- @NonNull private final ImageView mMuteActionButton;
- @NonNull private final View mReplyActionButton;
- @NonNull private final View mUnreadIconIndicator;
- @NonNull private final View mDivider;
-
- /** Conversation Item View Holder constructor */
- public ConversationItemViewHolder(
- @NonNull View itemView,
- @NonNull OnConversationItemClickListener onConversationItemClickListener) {
- super(itemView);
- mOnConversationItemClickListener = onConversationItemClickListener;
- 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);
- 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);
- mDivider = itemView.findViewById(R.id.divider);
- mAvatarView.setOutlineProvider(CircularOutputlineProvider.get());
- mUnreadIconIndicator.setOutlineProvider(CircularOutputlineProvider.get());
- mDataModel = AppFactory.get().getDataModel();
- }
-
- /** 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());
- mAvatarView.setImageDrawable(uiData.getAvatar());
- mPlayMessageTouchView.setOnClickListener(null);
- mSubtitleIconView.setImageDrawable(uiData.getSubtitleIcon());
- boolean showDotSeparatorSubtitle =
- !uiData.getReadableTime().isEmpty() && !uiData.getSubtitle().isEmpty();
- ViewUtils.setVisible(mDotSeparatorView, showDotSeparatorSubtitle);
- ViewUtils.setVisible(mSubtitleIconView, uiData.getSubtitleIcon() != null);
- setUpActionButton(uiData);
- setUpTextAppearance(uiData);
- updateMuteButton(uiData.isMuted());
- itemView.setVisibility(View.VISIBLE);
- }
-
- 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);
- 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);
- 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);
- }
-
- /** Recycles views. */
- public void recycle() {
- mPlayMessageTouchView.setOnClickListener(null);
- }
-
- private void setUpActionButton(@NonNull UIConversationItem uiData) {
- ViewUtils.setVisible(mDivider, uiData.shouldShowReplyIcon() || uiData.shouldShowMuteIcon());
- ViewUtils.setVisible(mMuteActionButton, uiData.shouldShowMuteIcon());
- ViewUtils.setVisible(mReplyActionButton, uiData.shouldShowReplyIcon());
- if (uiData.shouldShowReplyIcon()) {
- mReplyActionButton.setEnabled(true);
- }
- if (uiData.shouldShowReplyIcon()) {
- mMuteActionButton.setEnabled(true);
- }
-
- mPlayMessageTouchView.setOnClickListener(
- view ->
- mOnConversationItemClickListener.onConversationItemClicked(
- uiData.getConversation()));
-
- mReplyActionButton.setOnClickListener(
- view ->
- mOnConversationItemClickListener.onReplyIconClicked(
- uiData.getConversation()));
- mMuteActionButton.setOnClickListener(
- view -> {
- boolean mute = !uiData.isMuted();
- mDataModel.muteConversation(uiData.getConversationId(), mute);
- });
- }
-}
diff --git a/src/com/android/car/messenger/core/ui/conversationlist/ConversationListFragment.java b/src/com/android/car/messenger/core/ui/conversationlist/ConversationListFragment.java
deleted file mode 100644
index 4bbbace..0000000
--- a/src/com/android/car/messenger/core/ui/conversationlist/ConversationListFragment.java
+++ /dev/null
@@ -1,172 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.car.messenger.core.ui.conversationlist;
-
-import android.app.Activity;
-import androidx.lifecycle.ViewModelProvider;
-import android.content.Intent;
-import android.os.Bundle;
-import android.view.View;
-
-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.core.models.ConnectionStatus;
-import com.android.car.messenger.core.models.UserAccount;
-import com.android.car.messenger.core.shared.MessageConstants;
-import com.android.car.messenger.core.ui.base.MessageListBaseFragment;
-import com.android.car.messenger.core.util.L;
-import com.android.car.messenger.core.util.VoiceUtil;
-import com.android.car.ui.toolbar.MenuItem;
-
-import java.util.ArrayList;
-
-/** Fragment for Message History/Conversation Metadata List */
-public class ConversationListFragment extends MessageListBaseFragment
- implements ConversationItemAdapter.OnConversationItemClickListener {
- @NonNull
- private static final String BLUETOOTH_SETTING_ACTION = "android.settings.BLUETOOTH_SETTINGS";
-
- @NonNull
- private static final String BLUETOOTH_SETTING_CATEGORY = "android.intent.category.DEFAULT";
-
- @NonNull private static final String KEY_USER_ACCOUNT = "KEY_USER_ACCOUNT";
- @Nullable private ConversationItemAdapter mConversationItemAdapter;
- @Nullable private UserAccount mUserAccount;
-
- @Override
- public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
- if (getArguments() != null) {
- mUserAccount = getArguments().getParcelable(KEY_USER_ACCOUNT);
- }
-
- // Don't recreate the adapter if we already have one, so that the list items
- // will display immediately upon the view being recreated.
- L.d("In View Created, about to load message data");
- if (mConversationItemAdapter == null) {
- mConversationItemAdapter =
- new ConversationItemAdapter(/* onConversationItemClickListener= */ this);
- }
- getRecyclerView().setAdapter(mConversationItemAdapter);
- ConversationListViewModel viewModel =
- new ViewModelProvider(this).get(ConversationListViewModel.class);
-
- viewModel
- .getConversations(mUserAccount)
- .observe(
- this,
- conversationLog -> {
- if (conversationLog.getConnectionStatus()
- != ConnectionStatus.CONNECTED) {
- ConnectionStatus connectionStatus =
- conversationLog.getConnectionStatus();
- if (connectionStatus == ConnectionStatus.DISCONNECTED) {
- // we currently only support bluetooth disconnect.
- // Future work can add other types of disconnect such as login
- handleBluetoothDisconnected();
- }
- } else if (conversationLog.isLoading()) {
- mLoadingFrameLayout.showLoading();
- } else if (conversationLog.getData() == null
- || conversationLog.getData().isEmpty()) {
- mLoadingFrameLayout.showEmpty(
- MessageConstants.INVALID_RES_ID,
- R.string.no_new_messages,
- MessageConstants.INVALID_RES_ID);
- setMenuItems();
- } else {
- mConversationItemAdapter.setConversationLogItems(
- conversationLog.getData());
- mLoadingFrameLayout.showContent();
- setMenuItems();
- }
- });
- }
-
- private void handleBluetoothDisconnected() {
- Intent launchIntent = new Intent();
- launchIntent.setAction(BLUETOOTH_SETTING_ACTION);
- launchIntent.addCategory(BLUETOOTH_SETTING_CATEGORY);
- mLoadingFrameLayout.showError(
- MessageConstants.INVALID_RES_ID,
- R.string.bluetooth_disconnected,
- MessageConstants.INVALID_RES_ID,
- R.string.connect_bluetooth_button_text,
- v -> startActivity(launchIntent),
- true);
- }
-
- private void setMenuItems() {
- Activity activity = getActivity();
- if (activity == null || mUserAccount == null || mToolbar == null) {
- return;
- }
- MenuItem newMessageButton =
- new MenuItem.Builder(activity)
- .setIcon(R.drawable.car_ui_icon_edit)
- .setTinted(true)
- .setShowIconAndTitle(true)
- .setTitle(R.string.new_message)
- .setOnClickListener(
- item ->
- VoiceUtil.voiceRequestGenericCompose(
- activity, mUserAccount))
- .build();
-
- ArrayList<MenuItem> menuItems = new ArrayList<>();
- menuItems.add(newMessageButton);
- mToolbar.setMenuItems(menuItems);
- }
-
- @Override
- public void onConversationItemClicked(@NonNull Conversation conversation) {
- VoiceUtil.voiceRequestReadConversation(requireActivity(), conversation);
- }
-
- @Override
- public void onReplyIconClicked(@NonNull Conversation conversation) {
- VoiceUtil.voiceRequestReplyConversation(requireActivity(), conversation);
- }
-
- /**
- * Get instance of Conversation Log fragment
- *
- * @param userAccount the user device info data will be retrieved for. If null, this fragment
- * shows a disconnect page
- * @return ConversationLogFragment instance
- */
- public static ConversationListFragment newInstance(@Nullable UserAccount userAccount) {
- Bundle args = new Bundle();
- args.putParcelable(KEY_USER_ACCOUNT, userAccount);
- ConversationListFragment fragment = new ConversationListFragment();
- fragment.setArguments(args);
- return fragment;
- }
-
- /**
- * Get unique fragment tag for fragment loading data for user device
- *
- * @param userAccount the user device info data will be retrieved for.
- * @return unique fragment tag
- */
- public static String getFragmentTag(@Nullable UserAccount userAccount) {
- int id = userAccount == null ? -1 : userAccount.getId();
- return ConversationListFragment.class.getName() + id;
- }
-}
diff --git a/src/com/android/car/messenger/core/ui/conversationlist/ConversationListViewModel.java b/src/com/android/car/messenger/core/ui/conversationlist/ConversationListViewModel.java
deleted file mode 100644
index 8ac0851..0000000
--- a/src/com/android/car/messenger/core/ui/conversationlist/ConversationListViewModel.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.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 androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-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;
-
-/** View model for ConversationLogFragment which provides message history live data. */
-public class ConversationListViewModel extends AndroidViewModel {
- @SuppressLint("StaticFieldLeak")
- @NonNull
- private final DataModel mDataModel;
-
- @Nullable private UserAccount mUserAccount;
- @Nullable private LiveData<UIConversationLog> mUIConversationLogLiveData;
-
- public ConversationListViewModel(@NonNull Application application) {
- super(application);
- mDataModel = AppFactory.get().getDataModel();
- }
-
- /**
- * Gets an observable {@link UIConversationLog} for the connected account
- *
- * <p>The observable emits the following: - {@link UIConversationLog#isLoading()} returns true
- * when loading - {@link UIConversationLog#getConnectionStatus()} returns appropriate connection
- * status, such as connected or disconnected - {@link UIConversationLog#getData()} returns a
- * non-null list of {@link UIConversationItem}, or empty if no items found
- */
- @NonNull
- public LiveData<UIConversationLog> getConversations(@Nullable UserAccount userAccount) {
- if (userAccount == null) {
- MediatorLiveData<UIConversationLog> mutableLiveData = new MediatorLiveData<>();
- mutableLiveData.postValue(UIConversationLog.getDisconnectedState());
- return mutableLiveData;
- }
- if (mUserAccount != null
- && mUserAccount.getId() == userAccount.getId()
- && mUIConversationLogLiveData != null) {
- return mUIConversationLogLiveData;
- }
- mUserAccount = userAccount;
- mUIConversationLogLiveData = createUIConversationLog(mUserAccount);
- return mUIConversationLogLiveData;
- }
-
- private LiveData<UIConversationLog> createUIConversationLog(@NonNull UserAccount userAccount) {
- MediatorLiveData<UIConversationLog> mutableLiveData = new MediatorLiveData<>();
- mutableLiveData.postValue(UIConversationLog.getLoadingState());
- mutableLiveData.addSource(
- mDataModel.getConversations(userAccount),
- list -> {
- List<UIConversationItem> data =
- list.stream()
- .map(UIConversationItemConverter::convertToUIConversationItem)
- .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
deleted file mode 100644
index bdd71e7..0000000
--- a/src/com/android/car/messenger/core/ui/conversationlist/UIConversationItem.java
+++ /dev/null
@@ -1,130 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.messenger.core.ui.conversationlist;
-
-import android.graphics.drawable.Drawable;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.car.messenger.common.Conversation;
-
-/** UI Conversation Item represents the UI layer for a Conversation Item row */
-public class UIConversationItem {
-
- @NonNull String mConversationId;
- @NonNull String mTitle;
- @NonNull String mSubtitle;
- @Nullable Drawable mSubtitleIcon;
- @NonNull String mReadableTime;
- @Nullable Drawable mAvatar;
- boolean mIsMuted;
- boolean mShowMuteIcon;
- boolean mShowReplyIcon;
- boolean mUseUnreadTheme;
- @NonNull Conversation mConversation;
-
- public UIConversationItem(
- @NonNull String conversationId,
- @NonNull String title,
- @NonNull String subtitle,
- @Nullable Drawable subtitleIcon,
- @NonNull String readableTime,
- @Nullable Drawable avatar,
- boolean showMuteIcon,
- boolean showReplyIcon,
- boolean useUnreadTheme,
- boolean isMuted,
- @NonNull Conversation conversation) {
- this.mConversationId = conversationId;
- this.mTitle = title;
- this.mSubtitle = subtitle;
- this.mSubtitleIcon = subtitleIcon;
- this.mReadableTime = readableTime;
- this.mAvatar = avatar;
- this.mShowMuteIcon = showMuteIcon;
- this.mShowReplyIcon = showReplyIcon;
- this.mUseUnreadTheme = useUnreadTheme;
- this.mIsMuted = isMuted;
- this.mConversation = conversation;
- }
-
- /** Returns conversation id */
- @NonNull
- public String getConversationId() {
- return mConversationId;
- }
-
- /** Returns human readable title for conversation */
- @NonNull
- public String getTitle() {
- return mTitle;
- }
-
- /** Returns subtitle for the conversation */
- @NonNull
- public String getSubtitle() {
- return mSubtitle;
- }
-
- /**
- * Returns icon to show by the corner of the subtitle. This can be null if nothing should be
- * shown.
- */
- @Nullable
- public Drawable getSubtitleIcon() {
- return mSubtitleIcon;
- }
-
- /** Gets the human readable time in hh::mm */
- @NonNull
- public String getReadableTime() {
- return mReadableTime;
- }
-
- /** Returns the avatar for the conversation */
- @Nullable
- public Drawable getAvatar() {
- return mAvatar;
- }
-
- /** Returns true, if mute icon should be shown, false otherwise */
- public boolean shouldShowMuteIcon() {
- return mShowMuteIcon;
- }
-
- /** Returns true, if conversation is muted or false otherwise */
- public boolean isMuted() {
- return mIsMuted;
- }
-
- /** Returns true, if reply icon should be shown, false otherwise */
- public boolean shouldShowReplyIcon() {
- return mShowReplyIcon;
- }
-
- /** Returns true, if unread theme should be used, false otherwise */
- public boolean shouldUseUnreadTheme() {
- return mUseUnreadTheme;
- }
-
- /** Returns the conversation object */
- @NonNull
- public Conversation getConversation() {
- return mConversation;
- }
-}
diff --git a/src/com/android/car/messenger/core/ui/conversationlist/UIConversationItemConverter.java b/src/com/android/car/messenger/core/ui/conversationlist/UIConversationItemConverter.java
deleted file mode 100644
index 94f597d..0000000
--- a/src/com/android/car/messenger/core/ui/conversationlist/UIConversationItemConverter.java
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.messenger.core.ui.conversationlist;
-
-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;
-import com.android.car.messenger.core.interfaces.AppFactory;
-import com.android.car.messenger.core.util.ConversationUtil;
-
-import java.util.Objects;
-
-/** Util class that converts Conversation Item to UIConversationItem */
-public class UIConversationItemConverter {
-
- private UIConversationItemConverter() {}
-
- /** Converts Conversation Item to UIConversationItem */
- public static UIConversationItem convertToUIConversationItem(Conversation conversation) {
- 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;
- 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);
- }
-
- return new UIConversationItem(
- conversation.getId(),
- Objects.requireNonNull(conversation.getConversationTitle()),
- subtitle,
- subtitleIcon,
- toHumanDisplay(timestamp),
- getConversationAvatar(context, conversation),
- /* showMuteIcon= */ true,
- /* showReplyIcon= */ true,
- isUnread,
- conversation.isMuted(),
- conversation);
- }
-
- @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);
- }
- }
-
- @NonNull
- private static String toHumanDisplay(long timeInMillis) {
- String delegate = "hh:mm aaa";
- return (String) DateFormat.format(delegate, timeInMillis);
- }
-
- @Nullable
- private static Drawable getConversationAvatar(
- @NonNull Context context, @NonNull Conversation conversation) {
- return (conversation.getConversationIcon() != null)
- ? conversation.getConversationIcon().loadDrawable(context)
- : null;
- }
-}
diff --git a/src/com/android/car/messenger/core/ui/conversationlist/UIConversationLog.java b/src/com/android/car/messenger/core/ui/conversationlist/UIConversationLog.java
deleted file mode 100644
index 15f315f..0000000
--- a/src/com/android/car/messenger/core/ui/conversationlist/UIConversationLog.java
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.messenger.core.ui.conversationlist;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.car.messenger.core.models.ConnectionStatus;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/** UI Data Model for presenting a log of conversation items */
-public class UIConversationLog {
-
- @NonNull private ConnectionStatus mConnectionStatus = ConnectionStatus.DISCONNECTED;
- private boolean mIsLoading = false;
- @Nullable private List<UIConversationItem> mData = new ArrayList<>();
-
- public static UIConversationLog getDefault() {
- return new UIConversationLog();
- }
-
- public static UIConversationLog getDisconnectedState() {
- return new UIConversationLog(
- ConnectionStatus.DISCONNECTED, /* isLoading= */ false, /* list= */ null);
- }
-
- public static UIConversationLog getLoadingState() {
- return new UIConversationLog(
- ConnectionStatus.CONNECTED, /* isLoading= */ true, new ArrayList<>());
- }
-
- /** Get Loaded State */
- public static UIConversationLog getLoadedState(@NonNull List<UIConversationItem> data) {
- return new UIConversationLog(ConnectionStatus.CONNECTED, /* isLoading= */ false, data);
- }
-
- private UIConversationLog() {}
-
- public UIConversationLog(
- @NonNull ConnectionStatus connectionStatus,
- boolean isLoading,
- @Nullable List<UIConversationItem> list) {
- mConnectionStatus = connectionStatus;
- mIsLoading = isLoading;
- mData = list;
- }
-
- @NonNull
- public ConnectionStatus getConnectionStatus() {
- return mConnectionStatus;
- }
-
- public boolean isLoading() {
- return mIsLoading;
- }
-
- @Nullable
- public List<UIConversationItem> getData() {
- return mData;
- }
-}
diff --git a/src/com/android/car/messenger/core/ui/launcher/MessageLauncherActivity.java b/src/com/android/car/messenger/core/ui/launcher/MessageLauncherActivity.java
deleted file mode 100644
index 248ac29..0000000
--- a/src/com/android/car/messenger/core/ui/launcher/MessageLauncherActivity.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.messenger.core.ui.launcher;
-
-import androidx.lifecycle.ViewModelProvider;
-import android.os.Bundle;
-import androidx.fragment.app.Fragment;
-import androidx.fragment.app.FragmentActivity;
-
-import androidx.annotation.NonNull;
-
-import com.android.car.messenger.core.models.UserAccount;
-import com.android.car.messenger.core.ui.conversationlist.ConversationListFragment;
-import com.android.car.messenger.core.util.L;
-import com.android.car.ui.baselayout.Insets;
-import com.android.car.ui.baselayout.InsetsChangedListener;
-
-/**
- * This is the launcher activity for the messaging app. This first routes to{@link
- * ConversationListFragment} or displays an error when no {@link UserAccount} are found.
- */
-public class MessageLauncherActivity extends FragmentActivity implements InsetsChangedListener {
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- MessageLauncherViewModel viewModel =
- new ViewModelProvider(this).get(MessageLauncherViewModel.class);
-
- L.d("In onCreate: MessageLauncher");
- viewModel
- .getAccounts()
- .observe(
- this,
- accounts -> {
- L.d("Total number of accounts: " + accounts.size());
- // First version only takes one device until multi-account support is
- // added
- UserAccount primaryAccount =
- !accounts.isEmpty() ? accounts.get(0) : null;
- String fragmentTag =
- ConversationListFragment.getFragmentTag(primaryAccount);
- Fragment fragment =
- getSupportFragmentManager().findFragmentByTag(fragmentTag);
- if (fragment == null) {
- fragment = ConversationListFragment.newInstance(primaryAccount);
- }
- setContentFragment(fragment, fragmentTag);
- });
- }
-
- private void setContentFragment(Fragment fragment, String fragmentTag) {
- getSupportFragmentManager().executePendingTransactions();
- while (getSupportFragmentManager().getBackStackEntryCount() > 0) {
- getSupportFragmentManager().popBackStackImmediate();
- }
- pushContentFragment(fragment, fragmentTag);
- }
-
- private void pushContentFragment(
- @NonNull Fragment topContentFragment, @NonNull String fragmentTag) {
- getSupportFragmentManager()
- .beginTransaction()
- .replace(android.R.id.content, topContentFragment, fragmentTag)
- .addToBackStack(fragmentTag)
- .commit();
- }
-
- @Override
- public void onCarUiInsetsChanged(Insets insets) {
- // Do nothing, this is just a marker that we will handle the insets in fragments.
- // This is only necessary because the fragments are not immediately added to the
- // activity when calling .commit()
- }
-}
diff --git a/src/com/android/car/messenger/core/ui/launcher/MessageLauncherViewModel.java b/src/com/android/car/messenger/core/ui/launcher/MessageLauncherViewModel.java
deleted file mode 100644
index 4b4d96a..0000000
--- a/src/com/android/car/messenger/core/ui/launcher/MessageLauncherViewModel.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.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 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;
-
-/** View model for MessageLauncherActivity */
-public class MessageLauncherViewModel extends AndroidViewModel {
- @NonNull private final DataModel mDataSource;
- @Nullable private LiveData<List<UserAccount>> mAccountsLiveData;
- // We currently only support the primary account until multi-account support is added
- private static final int DEVICE_LIMIT = 1;
-
- public MessageLauncherViewModel(@NonNull Application application) {
- super(application);
- mDataSource = AppFactory.get().getDataModel();
- }
-
- /** Get observable data with list of accounts/user accounts */
- @NonNull
- public LiveData<List<UserAccount>> getAccounts() {
- if (mAccountsLiveData == null) {
- mAccountsLiveData = getAccountList();
- }
- return mAccountsLiveData;
- }
-
- private LiveData<List<UserAccount>> getAccountList() {
- return Transformations.map(
- mDataSource.getAccounts(),
- accountList ->
- accountList.stream().limit(DEVICE_LIMIT).collect(Collectors.toList()));
- }
-}
diff --git a/src/com/android/car/messenger/core/ui/shared/CircularOutputlineProvider.java b/src/com/android/car/messenger/core/ui/shared/CircularOutputlineProvider.java
deleted file mode 100644
index 49577a9..0000000
--- a/src/com/android/car/messenger/core/ui/shared/CircularOutputlineProvider.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.messenger.core.ui.shared;
-
-import android.graphics.Outline;
-import android.view.View;
-import android.view.ViewOutlineProvider;
-
-import androidx.annotation.NonNull;
-
-import com.android.car.messenger.R;
-
-/** OutlineProvider that changes the shape of ImageViews */
-public final class CircularOutputlineProvider extends ViewOutlineProvider {
-
- private CircularOutputlineProvider() {}
-
- @NonNull
- private static final CircularOutputlineProvider INSTANCE = new CircularOutputlineProvider();
-
- /** Gets the singleton instance */
- @NonNull
- public static CircularOutputlineProvider get() {
- return INSTANCE;
- }
-
- @Override
- public void getOutline(View view, Outline outline) {
- float radiusPercent =
- view.getContext()
- .getResources()
- .getFloat(R.dimen.contact_avatar_corner_radius_percent);
- float radius = Math.min(view.getWidth(), view.getHeight()) * radiusPercent;
- outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), radius);
- view.setClipToOutline(true);
- }
-}
diff --git a/src/com/android/car/messenger/core/ui/shared/LetterTileDrawable.java b/src/com/android/car/messenger/core/ui/shared/LetterTileDrawable.java
deleted file mode 100644
index c50c349..0000000
--- a/src/com/android/car/messenger/core/ui/shared/LetterTileDrawable.java
+++ /dev/null
@@ -1,300 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.car.messenger.core.ui.shared;
-
-import android.content.res.Resources;
-import android.content.res.TypedArray;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.ColorFilter;
-import android.graphics.Paint;
-import android.graphics.Paint.Align;
-import android.graphics.PixelFormat;
-import android.graphics.PorterDuff;
-import android.graphics.Rect;
-import android.graphics.Typeface;
-import android.graphics.drawable.Drawable;
-import android.text.TextUtils;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.car.messenger.R;
-
-/**
- * A drawable that encapsulates all the functionality needed to display a letter tile to represent a
- * contact image.
- */
-@SuppressWarnings("StaticAssignmentInConstructor")
-public class LetterTileDrawable extends Drawable {
- /** Letter tile */
- @NonNull private static int[] sColors;
-
- private static int sDefaultColor;
- private static int sTileFontColor;
- private static float sLetterToTileRatio;
- @NonNull private static Drawable sDefaultPersonAvatar;
- @NonNull private static Drawable sDefaultBusinessAvatar;
- @NonNull private static Drawable sDefaultVoicemailAvatar;
-
- /** Reusable components to avoid new allocations */
- @NonNull private static final Paint sPaint = new Paint();
-
- @NonNull private static final Rect sRect = new Rect();
-
- /** Contact type constants */
- public static final int TYPE_PERSON = 1;
-
- public static final int TYPE_BUSINESS = 2;
- public static final int TYPE_VOICEMAIL = 3;
- public static final int TYPE_DEFAULT = TYPE_PERSON;
-
- @NonNull private final Paint mPaint;
-
- @Nullable private String mLetters;
- private int mColor;
- private int mContactType = TYPE_DEFAULT;
- private float mScale = 1.0f;
- private float mOffset = 0.0f;
- private boolean mIsCircle = false;
-
- /** A custom Drawable that draws letters on a colored background. */
- // The use pattern for this constructor is:
- // create LTD, setContactDetails(), and setIsCircular(true) if needed.
- public LetterTileDrawable(@NonNull final Resources res) {
- this(res, null, null);
- }
-
- /** A custom Drawable that draws letters on a colored background. */
- // This constructor allows passing the letters and identifier directly. There is no need to
- // call setContactDetails() again. setIsCircular(true) needs to be called separately if needed.
- public LetterTileDrawable(
- @NonNull final Resources res, @Nullable String letters, @Nullable String identifier) {
- mPaint = new Paint();
- mPaint.setFilterBitmap(true);
- mPaint.setDither(true);
- setScale(0.7f);
-
- if (sColors == null) {
- sDefaultColor = res.getColor(R.color.letter_tile_default_color, null /* theme */);
- TypedArray ta = res.obtainTypedArray(R.array.letter_tile_colors);
- if (ta.length() == 0) {
- // TODO(b/26518438). Looks like robolectric shadow doesn't currently support
- // obtainTypedArray and always returns length 0 array, which will make some code
- // below that does a division by length of sColors choke. Workaround by creating
- // an array of length 1.
- sColors = new int[] {sDefaultColor};
-
- } else {
- sColors = new int[ta.length()];
- for (int i = ta.length() - 1; i >= 0; i--) {
- sColors[i] = ta.getColor(i, sDefaultColor);
- }
- ta.recycle();
- }
-
- sTileFontColor = res.getColor(R.color.letter_tile_font_color, null /* theme */);
- sLetterToTileRatio = res.getFraction(R.fraction.letter_to_tile_ratio, 1, 1);
- // TODO: get images for business and voicemail
- sDefaultPersonAvatar = res.getDrawable(R.drawable.ic_person, null /* theme */);
- sDefaultBusinessAvatar = res.getDrawable(R.drawable.ic_person, null /* theme */);
- sDefaultVoicemailAvatar = res.getDrawable(R.drawable.ic_person, null /* theme */);
- sPaint.setTypeface(
- Typeface.create(
- res.getString(R.string.config_letter_tile_font_family),
- res.getInteger(R.integer.config_letter_tile_text_style)));
- sPaint.setTextAlign(Align.CENTER);
- sPaint.setAntiAlias(true);
- }
-
- setContactDetails(letters, identifier);
- }
-
- @Override
- public void draw(@NonNull final Canvas canvas) {
- final Rect bounds = getBounds();
- if (!isVisible() || bounds.isEmpty()) {
- return;
- }
- // Draw letter tile.
- drawLetterTile(canvas);
- }
-
- /**
- * Draw the drawable onto the canvas at the current bounds taking into account the current
- * scale.
- */
- private void drawDrawableOnCanvas(final Drawable drawable, @NonNull final Canvas canvas) {
- // The drawable should be drawn in the middle of the canvas without changing its width to
- // height ratio.
- final Rect destRect = copyBounds();
-
- // Crop the destination bounds into a square, scaled and offset as appropriate
- final int halfLength = (int) (mScale * Math.min(destRect.width(), destRect.height()) / 2);
-
- destRect.set(
- destRect.centerX() - halfLength,
- (int) (destRect.centerY() - halfLength + mOffset * destRect.height()),
- destRect.centerX() + halfLength,
- (int) (destRect.centerY() + halfLength + mOffset * destRect.height()));
-
- drawable.setAlpha(mPaint.getAlpha());
- drawable.setColorFilter(sTileFontColor, PorterDuff.Mode.SRC_IN);
- drawable.setBounds(destRect);
- drawable.draw(canvas);
- }
-
- private void drawLetterTile(@NonNull final Canvas canvas) {
- // Draw background color.
- sPaint.setColor(mColor);
-
- sPaint.setAlpha(mPaint.getAlpha());
- final Rect bounds = getBounds();
- final int minDimension = Math.min(bounds.width(), bounds.height());
-
- if (mIsCircle) {
- canvas.drawCircle(bounds.centerX(), bounds.centerY(), minDimension / 2, sPaint);
- } else {
- canvas.drawRect(bounds, sPaint);
- }
-
- if (!TextUtils.isEmpty(mLetters)) {
- // Scale text by canvas bounds and user selected scaling factor
- sPaint.setTextSize(mScale * sLetterToTileRatio * minDimension);
- // sPaint.setTextSize(sTileLetterFontSize);
- sPaint.getTextBounds(mLetters, 0, mLetters.length(), sRect);
- sPaint.setColor(sTileFontColor);
-
- // Draw the letter in the canvas, vertically shifted up or down by the user-defined
- // offset
- canvas.drawText(
- mLetters,
- 0,
- mLetters.length(),
- bounds.centerX(),
- bounds.centerY() + mOffset * bounds.height() + sRect.height() / 2,
- sPaint);
- } else {
- // Draw the default image if there is no letter/digit to be drawn
- final Drawable drawable = getDrawablepForContactType(mContactType);
- drawDrawableOnCanvas(drawable, canvas);
- }
- }
-
- public int getColor() {
- return mColor;
- }
-
- /** Returns a deterministic color based on the provided contact identifier string. */
- private int pickColor(@Nullable final String identifier) {
- if (TextUtils.isEmpty(identifier) || mContactType == TYPE_VOICEMAIL) {
- return sDefaultColor;
- }
- // String.hashCode() implementation is not supposed to change across java versions, so
- // this should guarantee the same email address always maps to the same color.
- // The email should already have been normalized by the ContactRequest.
- final int color = Math.abs(identifier.hashCode()) % sColors.length;
- return sColors[color];
- }
-
- @NonNull
- private static Drawable getDrawablepForContactType(int contactType) {
- switch (contactType) {
- case TYPE_BUSINESS:
- return sDefaultBusinessAvatar;
- case TYPE_VOICEMAIL:
- return sDefaultVoicemailAvatar;
- case TYPE_PERSON:
- default:
- return sDefaultPersonAvatar;
- }
- }
-
- @Override
- public void setAlpha(final int alpha) {
- mPaint.setAlpha(alpha);
- }
-
- @Override
- public void setColorFilter(@NonNull final ColorFilter cf) {
- mPaint.setColorFilter(cf);
- }
-
- @Override
- public int getOpacity() {
- return PixelFormat.OPAQUE;
- }
-
- /**
- * Scale the drawn letter tile to a ratio of its default size
- *
- * @param scale The ratio the letter tile should be scaled to as a percentage of its default
- * size, from a scale of 0 to 2.0f. The default is 1.0f.
- */
- public void setScale(float scale) {
- mScale = scale;
- }
-
- /**
- * Assigns the vertical offset of the position of the letter tile to the ContactDrawable
- *
- * @param offset The provided offset must be within the range of -0.5f to 0.5f. If set to -0.5f,
- * the letter will be shifted upwards by 0.5 times the height of the canvas it is being
- * drawn on, which means it will be drawn with the center of the letter starting at the top
- * edge of the canvas. If set to 0.5f, the letter will be shifted downwards by 0.5 times the
- * height of the canvas it is being drawn on, which means it will be drawn with the center
- * of the letter starting at the bottom edge of the canvas. The default is 0.0f.
- */
- public void setOffset(float offset) {
- mOffset = offset;
- }
-
- /**
- * Sets the details.
- *
- * @param letters The letters need to be drawn
- * @param identifier decides the color for the drawable.
- */
- public void setContactDetails(@Nullable String letters, @Nullable String identifier) {
- mLetters = letters;
- mColor = pickColor(identifier);
- }
-
- public void setContactType(int contactType) {
- mContactType = contactType;
- }
-
- public void setIsCircular(boolean isCircle) {
- mIsCircle = isCircle;
- }
-
- /**
- * Convert the drawable to a bitmap.
- *
- * @param size The target size of the bitmap.
- * @return A bitmap representation of the drawable.
- */
- @NonNull
- public Bitmap toBitmap(int size) {
- Bitmap largeIcon = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
- Canvas canvas = new Canvas(largeIcon);
- Rect bounds = getBounds();
- setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
- draw(canvas);
- setBounds(bounds);
- return largeIcon;
- }
-}
diff --git a/src/com/android/car/messenger/core/ui/shared/LoadingFrameLayout.java b/src/com/android/car/messenger/core/ui/shared/LoadingFrameLayout.java
deleted file mode 100644
index a6f0bfa..0000000
--- a/src/com/android/car/messenger/core/ui/shared/LoadingFrameLayout.java
+++ /dev/null
@@ -1,321 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.messenger.core.ui.shared;
-
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.util.AttributeSet;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.FrameLayout;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import androidx.annotation.DrawableRes;
-import androidx.annotation.IntDef;
-import androidx.annotation.LayoutRes;
-import androidx.annotation.MainThread;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.StringRes;
-
-import com.android.car.messenger.R;
-
-/** A widget that supports different {@link State}s: NEW, LOADING, CONTENT, EMPTY OR ERROR. */
-public class LoadingFrameLayout extends FrameLayout {
- private static final int INVALID_RES_ID = 0;
-
- /** Possible states of a service request display. */
- @IntDef({State.NEW, State.LOADING, State.CONTENT, State.ERROR, State.EMPTY})
- public @interface State {
- int NEW = 0;
- int LOADING = 1;
- int CONTENT = 2;
- int ERROR = 3;
- int EMPTY = 4;
- }
-
- @NonNull private final Context mContext;
- @NonNull private ViewContainer mEmptyView;
- @NonNull private ViewContainer mLoadingView;
- @NonNull private ViewContainer mErrorView;
-
- @State private int mState = State.NEW;
-
- public LoadingFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public LoadingFrameLayout(
- @NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- mContext = context;
- TypedArray values =
- context.obtainStyledAttributes(attrs, R.styleable.LoadingFrameLayout, defStyle, 0);
- setLoadingView(
- values.getResourceId(
- R.styleable.LoadingFrameLayout_progressViewLayout,
- R.layout.loading_progress_view));
- setEmptyView();
- setErrorView();
- values.recycle();
- }
-
- @Override
- public void onFinishInflate() {
- super.onFinishInflate();
- // Start with a loading view when inflated from XML.
- showLoading();
- }
-
- private void setLoadingView(int loadingLayoutId) {
- mLoadingView = new ViewContainer(State.LOADING, loadingLayoutId);
- }
-
- private void setEmptyView() {
- mEmptyView = new ViewContainer(State.EMPTY);
- }
-
- private void setErrorView() {
- mErrorView = new ViewContainer(State.ERROR);
- }
-
- /** Shows the loading view, hides other views. */
- @MainThread
- public void showLoading() {
- switchTo(State.LOADING);
- }
-
- /**
- * Shows the error view where the action button is not available and hides other views.
- *
- * @param iconResId drawable resource id used for the top icon. When it is invalid, hide the
- * icon view.
- * @param messageResId string resource id used for the error message. When it is invalid, hide
- * the message view.
- * @param secondaryMessageResId string resource id for the secondary error message. When it is
- * invalid, hide the secondary message view.
- */
- public void showError(
- @DrawableRes int iconResId,
- @StringRes int messageResId,
- @StringRes int secondaryMessageResId) {
- showError(iconResId, messageResId, secondaryMessageResId, INVALID_RES_ID, null, false);
- }
-
- /**
- * Shows the error view, hides other views.
- *
- * @param iconResId drawable resource id used for the top icon.When it is invalid, hide the icon
- * view.
- * @param messageResId string resource id used for the error message. When it is invalid, hide
- * the message view.
- * @param secondaryMessageResId string resource id for the secondary error message. When it is
- * invalid, hide the secondary message view.
- * @param actionButtonTextResId string resource id for the action button.
- * @param actionButtonOnClickListener click listener set on the action button.
- * @param showActionButton boolean flag if the action button will show.
- */
- public void showError(
- @DrawableRes int iconResId,
- @StringRes int messageResId,
- @StringRes int secondaryMessageResId,
- @StringRes int actionButtonTextResId,
- @Nullable OnClickListener actionButtonOnClickListener,
- boolean showActionButton) {
- switchTo(State.ERROR);
- mErrorView.setIcon(iconResId);
- mErrorView.setMessage(messageResId);
- mErrorView.setSecondaryMessage(secondaryMessageResId);
- mErrorView.setActionButtonText(actionButtonTextResId);
- mErrorView.setActionButtonClickListener(actionButtonOnClickListener);
- mErrorView.setActionButtonVisible(showActionButton);
- }
-
- /**
- * Shows the empty view where the action button is not available and hides other views.
- *
- * @param iconResId drawable resource id used for the top icon. When it is invalid, hide the
- * icon view.
- * @param messageResId string resource id used for the empty message. When it is invalid, hide
- * the message view.
- * @param secondaryMessageResId string resource id for the secondary empty message. When it is
- * invalid, hide the secondary message view.
- */
- public void showEmpty(
- @DrawableRes int iconResId,
- @StringRes int messageResId,
- @StringRes int secondaryMessageResId) {
- showEmpty(iconResId, messageResId, secondaryMessageResId, INVALID_RES_ID, null, false);
- }
-
- /**
- * Shows the empty view and hides other views.
- *
- * @param iconResId drawable resource id used for the top icon.When it is invalid, hide the icon
- * view.
- * @param messageResId string resource id used for the empty message. When it is invalid, hide
- * the message view.
- * @param secondaryMessageResId string resource id for the secondary empty message. When it is
- * invalid, hide the secondary message view.
- * @param actionButtonTextResId string resource id for the action button.
- * @param actionButtonOnClickListener click listener set on the action button.
- * @param showActionButton boolean flag if the action button will show.
- */
- public void showEmpty(
- @DrawableRes int iconResId,
- @StringRes int messageResId,
- @StringRes int secondaryMessageResId,
- @StringRes int actionButtonTextResId,
- @Nullable OnClickListener actionButtonOnClickListener,
- boolean showActionButton) {
- mEmptyView.setIcon(iconResId);
- mEmptyView.setMessage(messageResId);
- mEmptyView.setSecondaryMessage(secondaryMessageResId);
- mEmptyView.setActionButtonText(actionButtonTextResId);
- mEmptyView.setActionButtonClickListener(actionButtonOnClickListener);
- mEmptyView.setActionButtonVisible(showActionButton);
- switchTo(State.EMPTY);
- }
-
- /** Shows the content view, hides other views. */
- public void showContent() {
- switchTo(State.CONTENT);
- }
-
- /** Hide all views. */
- public void reset() {
- switchTo(State.NEW);
- }
-
- private void switchTo(@State int state) {
- if (mState != state) {
- ViewUtils.setVisible((View) findViewById(R.id.list_view), state == State.CONTENT);
- mLoadingView.setVisibilityFromState(state);
- mErrorView.setVisibilityFromState(state);
- mEmptyView.setVisibilityFromState(state);
- mState = state;
- }
- }
-
- /**
- * Container for views held by this LoadingFrameLayout. Used for deferring view inflation until
- * the view is about to be shown.
- */
- private class ViewContainer {
- @State private final int mViewState;
- private View mView;
- private ImageView mIconView;
- private TextView mActionButton;
- private TextView mMessageView;
- private TextView mSecondaryMessageView;
-
- private ViewContainer(@State int state) {
- mViewState = state;
- mView = inflateView();
- LoadingFrameLayout.this.addView(mView);
- }
-
- private ViewContainer(@State int state, @LayoutRes int layout) {
- mViewState = state;
- mView = LayoutInflater.from(mContext).inflate(layout, LoadingFrameLayout.this, false);
- LoadingFrameLayout.this.addView(mView);
- }
-
- private View inflateView() {
- View view =
- LayoutInflater.from(mContext)
- .inflate(R.layout.loading_info_view, LoadingFrameLayout.this, false);
- mMessageView = view.findViewById(R.id.loading_info_message);
- mSecondaryMessageView = view.findViewById(R.id.loading_info_secondary_message);
- mIconView = view.findViewById(R.id.loading_info_icon);
- mActionButton = view.findViewById(R.id.loading_info_action_button);
- return view;
- }
-
- public void setVisibilityFromState(@State int newState) {
- if (mViewState == newState) {
- show();
- } else {
- hide();
- }
- }
-
- private void show() {
- mView.setVisibility(View.VISIBLE);
- }
-
- private void hide() {
- if (mView != null) {
- mView.setVisibility(View.GONE);
- mView.clearFocus();
- }
- }
-
- private void setMessage(@StringRes int messageResId) {
- if (mMessageView == null) {
- return;
- }
- if (messageResId != INVALID_RES_ID) {
- mMessageView.setText(messageResId);
- } else {
- ViewUtils.setVisible(mMessageView, false);
- }
- }
-
- private void setSecondaryMessage(@StringRes int secondaryMessageResId) {
- if (mSecondaryMessageView == null) {
- return;
- }
- if (secondaryMessageResId != INVALID_RES_ID) {
- mSecondaryMessageView.setText(secondaryMessageResId);
- } else {
- ViewUtils.setVisible(mSecondaryMessageView, false);
- }
- }
-
- private void setActionButtonClickListener(OnClickListener actionButtonOnClickListener) {
- if (mActionButton == null) {
- return;
- }
- mActionButton.setOnClickListener(actionButtonOnClickListener);
- }
-
- private void setActionButtonText(@StringRes int actionButtonTextResId) {
- if (mActionButton == null) {
- return;
- }
- if (actionButtonTextResId != INVALID_RES_ID) {
- mActionButton.setText(actionButtonTextResId);
- }
- }
-
- private void setActionButtonVisible(boolean visible) {
- ViewUtils.setVisible(mActionButton, visible);
- }
-
- private void setIcon(@DrawableRes int iconResId) {
- if (iconResId != INVALID_RES_ID) {
- if (mIconView != null) {
- mIconView.setImageResource(iconResId);
- }
- } else {
- ViewUtils.setVisible(mIconView, false);
- }
- }
- }
-}
diff --git a/src/com/android/car/messenger/core/ui/shared/ViewUtils.java b/src/com/android/car/messenger/core/ui/shared/ViewUtils.java
deleted file mode 100644
index 0e77b00..0000000
--- a/src/com/android/car/messenger/core/ui/shared/ViewUtils.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.messenger.core.ui.shared;
-
-import android.view.View;
-
-import androidx.annotation.Nullable;
-
-/** Utility methods to operate over views. */
-public class ViewUtils {
-
- private ViewUtils() {}
-
- /** Sets the visibility of the (optional) view to {@link View#VISIBLE} or {@link View#GONE}. */
- public static void setVisible(@Nullable View view, boolean visible) {
- if (view != null) {
- view.setVisibility(visible ? View.VISIBLE : View.GONE);
- }
- }
-}
diff --git a/src/com/android/car/messenger/core/util/ConversationUtil.java b/src/com/android/car/messenger/core/util/ConversationUtil.java
deleted file mode 100644
index b56cbce..0000000
--- a/src/com/android/car/messenger/core/util/ConversationUtil.java
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.messenger.core.util;
-
-import static com.android.car.messenger.core.shared.MessageConstants.LAST_REPLY_TIMESTAMP_EXTRA;
-
-import static java.lang.Math.max;
-
-import android.os.Bundle;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.car.messenger.common.Conversation;
-import com.android.car.messenger.common.Conversation.Message;
-import com.android.car.messenger.common.Conversation.Message.MessageStatus;
-
-/** Conversation Util class for the {@link Conversation} DAO */
-public class ConversationUtil {
- private ConversationUtil() {}
-
- /**
- * Get the last timestamp for the conversation. This could be a reply timestamp or last received
- * message timestamp, whichever is last.
- */
- public static long getConversationTimestamp(@Nullable Conversation conversation) {
- if (conversation == null) {
- return 0L;
- }
- long replyTimestamp = conversation.getExtras().getLong(LAST_REPLY_TIMESTAMP_EXTRA, 0L);
- Message lastMessage = getLastMessage(conversation);
- long lastMessageTimestamp = lastMessage == null ? 0L : lastMessage.getTimestamp();
- return max(replyTimestamp, lastMessageTimestamp);
- }
-
- /** Returns if the {@link Conversation} has been last responded to. */
- public static boolean isReplied(@Nullable Conversation conversation) {
- if (conversation == null) {
- return false;
- }
- long lastReplyTimestamp = getReplyTimestamp(conversation);
- long lastMessageTimestamp = 0L;
- Message lastMessageGroup = ConversationUtil.getLastMessage(conversation);
- if (lastMessageGroup != null) {
- lastMessageTimestamp = lastMessageGroup.getTimestamp();
- }
- return lastReplyTimestamp > lastMessageTimestamp;
- }
-
- /**
- * Returns the last message in the conversation, or null if {@link Conversation#getMessages} is
- * empty
- */
- @Nullable
- public static Message getLastMessage(@Nullable Conversation conversation) {
- if (conversation == null || conversation.getMessages().isEmpty()) {
- return null;
- }
- int size = conversation.getMessages().size();
- return conversation.getMessages().get(size - 1);
- }
-
- /**
- * 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);
- return isReplied(conversation) || lastMessage == null
- ? MessageStatus.MESSAGE_STATUS_NONE
- : lastMessage.getMessageStatus();
- }
-
- /**
- * Sets the Reply timestamp to a {@link Conversation.Builder}
- *
- * @param extras optional, pass an existing bundle to which the reply timestamp will be added
- * 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(
- @NonNull Conversation.Builder conversationBuilder,
- @Nullable Bundle extras,
- long lastReplyTimestamp) {
- if (lastReplyTimestamp > 0L) {
- if (extras == null) {
- extras = new Bundle();
- }
- extras.putLong(LAST_REPLY_TIMESTAMP_EXTRA, lastReplyTimestamp);
- conversationBuilder.setExtras(extras);
- }
- }
-
- /** Gets reply timestamp */
- private static long getReplyTimestamp(@Nullable Conversation conversation) {
- if (conversation == null) {
- return 0L;
- }
- return conversation.getExtras().getLong(LAST_REPLY_TIMESTAMP_EXTRA, 0L);
- }
-}
diff --git a/src/com/android/car/messenger/core/util/L.java b/src/com/android/car/messenger/core/util/L.java
deleted file mode 100644
index 4e2fcb1..0000000
--- a/src/com/android/car/messenger/core/util/L.java
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * 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.util;
-
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-
-/** Util class for logging. */
-public class L {
- @NonNull private static final String TAG = "CarMessenger";
-
- private L() {}
-
- /**
- * Logs verbose level logs if loggable.
- *
- * @param msg the message to log, as a format string
- */
- public static void v(@NonNull String msg) {
- if (Log.isLoggable(TAG, Log.VERBOSE)) {
- Log.v(TAG, msg);
- }
- }
-
- /**
- * Logs debug level logs if loggable.
- *
- * @param msg the message to log, as a format string
- */
- public static void d(@NonNull String msg) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, msg);
- }
- }
-
- /**
- * Logs info level logs if loggable.
- *
- * @param msg the message to log, as a format string
- */
- public static void i(@NonNull String msg) {
- if (Log.isLoggable(TAG, Log.INFO)) {
- Log.i(TAG, msg);
- }
- }
-
- /**
- * Logs warning level logs if loggable.
- *
- * @param msg the message to log, as a format string
- */
- public static void w(@NonNull String msg) {
- if (Log.isLoggable(TAG, Log.WARN)) {
- Log.w(TAG, msg);
- }
- }
-
- /**
- * Logs error level logs.
- *
- * @param msg the message to log, as a format string
- */
- public static void e(@NonNull String msg) {
- Log.e(TAG, msg);
- }
-
- /**
- * Logs warning level logs.
- *
- * @param msg the message to log, as a format string
- * @param e a throwable to log
- */
- public static void e(@NonNull String msg, Throwable e) {
- Log.e(TAG, msg, e);
- }
-
- /**
- * Logs conditions that should never happen.
- *
- * @param msg the message to log, as a format string
- */
- public static void wtf(@NonNull String msg) {
- Log.wtf(TAG, msg);
- }
-
- /**
- * Logs conditions that should never happen.
- *
- * @param e an exception to log
- * @param msg the message to log, as a format string
- */
- public static void wtf(Exception e, @NonNull String msg) {
- Log.wtf(TAG, msg, e);
- }
-}
diff --git a/src/com/android/car/messenger/core/util/VoiceUtil.java b/src/com/android/car/messenger/core/util/VoiceUtil.java
deleted file mode 100644
index 16be671..0000000
--- a/src/com/android/car/messenger/core/util/VoiceUtil.java
+++ /dev/null
@@ -1,257 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.car.messenger.core.util;
-
-import static com.android.car.assist.CarVoiceInteractionSession.KEY_ACTION;
-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;
-import static com.android.car.assist.CarVoiceInteractionSession.VOICE_ACTION_REPLY_CONVERSATION;
-import static com.android.car.assist.CarVoiceInteractionSession.VOICE_ACTION_REPLY_NOTIFICATION;
-import static com.android.car.assist.CarVoiceInteractionSession.VOICE_ACTION_SEND_SMS;
-import static com.android.car.messenger.core.shared.MessageConstants.ACTION_DIRECT_SEND;
-import static com.android.car.messenger.core.shared.MessageConstants.ACTION_MARK_AS_READ;
-import static com.android.car.messenger.core.shared.MessageConstants.ACTION_MUTE;
-import static com.android.car.messenger.core.shared.MessageConstants.ACTION_REPLY;
-import static com.android.car.messenger.core.shared.MessageConstants.EXTRA_CONVERSATION_KEY;
-
-import android.app.Activity;
-import android.app.PendingIntent;
-import android.app.RemoteAction;
-import android.app.RemoteInput;
-import android.content.Context;
-import android.content.Intent;
-import android.graphics.drawable.Icon;
-import android.os.Bundle;
-import android.service.notification.StatusBarNotification;
-import android.text.TextUtils;
-
-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.ConversationAction;
-import com.android.car.messenger.common.Conversation.ConversationAction.ActionType;
-import com.android.car.messenger.core.interfaces.AppFactory;
-import com.android.car.messenger.core.models.UserAccount;
-import com.android.car.messenger.core.service.MessengerService;
-import com.android.car.messenger.core.shared.NotificationHandler;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/** Voice Util classes for requesting voice interactions and responding to voice actions */
-public class VoiceUtil {
-
- private VoiceUtil() {}
-
- /** Requests Voice request to read a conversation */
- public static void voiceRequestReadConversation(
- @NonNull Activity activity, @NonNull Conversation conversation) {
- voiceRequestHelper(
- activity,
- conversation,
- VOICE_ACTION_READ_CONVERSATION,
- VOICE_ACTION_READ_NOTIFICATION);
- }
-
- /** Requests Voice request to reply to a conversation */
- public static void voiceRequestReplyConversation(
- @NonNull Activity activity, @NonNull Conversation conversation) {
- voiceRequestHelper(
- activity,
- conversation,
- VOICE_ACTION_REPLY_CONVERSATION,
- VOICE_ACTION_REPLY_NOTIFICATION);
- }
-
- private static void voiceRequestHelper(
- @NonNull Activity activity,
- @NonNull Conversation conversation,
- @NonNull String conversationAction,
- @NonNull String notificationAction) {
- Bundle args = new Bundle();
- Conversation tapToReadConversation = createTapToReadConversation(conversation);
- // use legacy tap to read by default as support for
- // new api using Conversation class is still very limited and very nascent
- StatusBarNotification sbn =
- NotificationHandler.postNotificationForLegacyTapToRead(tapToReadConversation);
- if (sbn != null) {
- args.putString(KEY_ACTION, notificationAction);
- args.putParcelable(KEY_NOTIFICATION, sbn);
- } else {
- // New API using generic Conversation class
- // is currently limited in support by partner assistants and is currently being phase
- // in.
- args.putString(KEY_ACTION, conversationAction);
- args.putBundle(KEY_CONVERSATION, tapToReadConversation.toBundle());
- }
-
- activity.startLocalVoiceInteraction(args);
- }
-
- /** Requests Voice request to start a generic compose voice interaction */
- public static void voiceRequestGenericCompose(Activity activity, UserAccount userAccount) {
- Bundle bundle = new Bundle();
- bundle.putString(KEY_ACTION, VOICE_ACTION_SEND_SMS);
- bundle.putString(KEY_DEVICE_ADDRESS, userAccount.getIccId());
- bundle.putString(KEY_DEVICE_NAME, userAccount.getName());
- PendingIntent sendIntent =
- createServiceIntent(ACTION_DIRECT_SEND, /* conversationKey= */ null);
- bundle.putParcelable(KEY_SEND_PENDING_INTENT, sendIntent);
- activity.startLocalVoiceInteraction(bundle);
- }
-
- /**
- * Returns a new conversation containing the tap to read pending intents to be transferred over
- * to the Voice Assistant.
- *
- * <p>The conversation object returned remained unmodified.
- *
- * <p>This is important to allow the Assistant have a different instance than the one that
- * powers our UI. We can create new pending intents without modifying the instance the Assistant
- * holds.
- *
- * @return new conversation instance with the same data and pending intents for tap to read.
- */
- public static Conversation createTapToReadConversation(Conversation conversation) {
- Context context = AppFactory.get().getContext();
- String conversationKey = conversation.getId();
- Conversation.Builder builder = conversation.toBuilder();
-
- final int replyIcon = R.drawable.car_ui_icon_reply;
- final String replyString = context.getString(R.string.action_reply);
- PendingIntent replyIntent = createServiceIntent(ACTION_REPLY, conversationKey);
- ConversationAction replyAction =
- new ConversationAction(
- ActionType.ACTION_TYPE_REPLY,
- new RemoteAction(
- Icon.createWithResource(context, replyIcon),
- replyString,
- replyString,
- replyIntent),
- new RemoteInput.Builder(Intent.EXTRA_TEXT).build());
-
- final int markAsReadIcon = android.R.drawable.ic_media_play;
- final String markAsReadString = context.getString(R.string.action_mark_as_read);
- PendingIntent markAsReadIntent = createServiceIntent(ACTION_MARK_AS_READ, conversationKey);
- ConversationAction markAsReadAction =
- new ConversationAction(
- ActionType.ACTION_TYPE_MARK_AS_READ,
- new RemoteAction(
- Icon.createWithResource(context, markAsReadIcon),
- markAsReadString,
- markAsReadString,
- markAsReadIntent),
- null);
-
- final int muteIcon = R.drawable.car_ui_icon_toggle_mute;
- final String muteString = context.getString(R.string.action_mute);
- PendingIntent muteIntent = createServiceIntent(ACTION_MUTE, conversationKey);
- ConversationAction muteAction =
- new ConversationAction(
- ActionType.ACTION_TYPE_MUTE,
- new RemoteAction(
- Icon.createWithResource(context, muteIcon),
- muteString,
- muteString,
- muteIntent),
- null);
-
- List<ConversationAction> actions = new ArrayList<>();
- actions.add(replyAction);
- actions.add(markAsReadAction);
- actions.add(muteAction);
- builder.setActions(actions);
- return builder.build();
- }
-
- private static PendingIntent createServiceIntent(
- @NonNull String action, @Nullable String conversationKey) {
- Context context = AppFactory.get().getContext();
- Bundle bundle = new Bundle();
- if (conversationKey != null) {
- bundle.putString(EXTRA_CONVERSATION_KEY, conversationKey);
- }
- Intent intent =
- new Intent(context, MessengerService.class)
- .setAction(action)
- .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP)
- .setClass(context, MessengerService.class)
- .putExtras(bundle);
-
- int requestCode =
- (conversationKey == null) ? action.hashCode() : conversationKey.hashCode();
- return PendingIntent.getForegroundService(
- context, requestCode, intent, PendingIntent.FLAG_ONE_SHOT);
- }
-
- /** 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. */
- public static void voiceReply(Intent intent) {
- final String conversationKey = intent.getStringExtra(EXTRA_CONVERSATION_KEY);
- final Bundle bundle = RemoteInput.getResultsFromIntent(intent);
- if (bundle == null) {
- L.e("Dropping voice reply. Received null bundle!");
- return;
- }
- final CharSequence message = bundle.getCharSequence(Intent.EXTRA_TEXT);
- L.d("voiceReply: " + message);
- if (!TextUtils.isEmpty(message)) {
- AppFactory.get().getDataModel().sendMessage(conversationKey, message.toString());
- }
- }
-
- /** Mark a conversation associated with a given sender key as read. */
- public static void mute(Intent intent) {
- Bundle extras = intent.getExtras();
- if (extras != null) {
- final String conversationKey = extras.getString(EXTRA_CONVERSATION_KEY);
- L.d("mute");
- AppFactory.get().getDataModel().muteConversation(conversationKey, true);
- }
- }
-
- /** Mark a conversation associated with a given sender key as read. */
- public static void markAsRead(Intent intent) {
- Bundle extras = intent.getExtras();
- if (extras != null) {
- final String conversationKey = extras.getString(EXTRA_CONVERSATION_KEY);
- L.d("marking as read");
- AppFactory.get().getDataModel().markAsRead(conversationKey);
- }
- }
-}
diff --git a/src/com/android/car/messenger/impl/AppFactoryImpl.java b/src/com/android/car/messenger/impl/AppFactoryImpl.java
deleted file mode 100644
index cceb716..0000000
--- a/src/com/android/car/messenger/impl/AppFactoryImpl.java
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.messenger.impl;
-
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.ServiceConnection;
-import android.content.SharedPreferences;
-import android.os.IBinder;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.preference.PreferenceManager;
-
-import com.android.car.messenger.core.interfaces.AppFactory;
-import com.android.car.messenger.core.interfaces.DataModel;
-import com.android.car.messenger.core.service.MessengerService;
-import com.android.car.messenger.impl.datamodels.TelephonyDataModel;
-
-/* App Factory Implementation */
-class AppFactoryImpl extends AppFactory {
- @NonNull private Context mApplicationContext;
- @NonNull private DataModel mDataModel;
- @NonNull private SharedPreferences mSharedPreferences;
- @Nullable private MessengerService mMessengerService;
-
- @NonNull
- private final ServiceConnection mServiceConnection =
- new ServiceConnection() {
- @Override
- public void onServiceConnected(
- @NonNull ComponentName className, @NonNull IBinder service) {
- MessengerService.LocalBinder binder = (MessengerService.LocalBinder) service;
- mMessengerService = binder.getService();
- }
-
- @Override
- public void onServiceDisconnected(@NonNull ComponentName arg0) {
- mMessengerService = null;
- }
- };
-
- private AppFactoryImpl() {}
-
- public static void register(@NonNull final CarMessengerApp application) {
- if (sRegistered && sInitialized) {
- return;
- }
-
- final AppFactoryImpl factory = new AppFactoryImpl();
- AppFactory.setInstance(factory);
- sRegistered = true;
-
- // At this point Factory is published. Services can now get initialized and depend on
- // Factory.get().
- factory.mApplicationContext = application.getApplicationContext();
- factory.mDataModel = new TelephonyDataModel();
- factory.mSharedPreferences =
- PreferenceManager.getDefaultSharedPreferences(factory.mApplicationContext);
-
- // Create Messenger Service
- Intent intent = new Intent(factory.mApplicationContext, MessengerService.class);
- factory.mApplicationContext.bindService(
- intent, factory.mServiceConnection, Context.BIND_AUTO_CREATE);
- }
-
- @Override
- @NonNull
- public Context getContext() {
- // prefer the messenger service context
- // to avoid warnings on using app context for UI constants
- if (mMessengerService != null) {
- return mMessengerService;
- } else {
- return mApplicationContext;
- }
- }
-
- @Override
- @NonNull
- public DataModel getDataModel() {
- return mDataModel;
- }
-
- @Override
- @NonNull
- public SharedPreferences getSharedPreferences() {
- return mSharedPreferences;
- }
-}
diff --git a/src/com/android/car/messenger/impl/CarMessengerApp.java b/src/com/android/car/messenger/impl/CarMessengerApp.java
deleted file mode 100644
index 4ed9440..0000000
--- a/src/com/android/car/messenger/impl/CarMessengerApp.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.messenger.impl;
-
-import android.app.Application;
-import android.os.Handler;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.car.messenger.core.util.L;
-
-import java.lang.Thread.UncaughtExceptionHandler;
-
-/** The application object */
-public class CarMessengerApp extends Application implements UncaughtExceptionHandler {
- @Nullable private static UncaughtExceptionHandler sSystemUncaughtExceptionHandler;
-
- @Override
- public void onCreate() {
- super.onCreate();
- AppFactoryImpl.register(this);
- sSystemUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
- Thread.setDefaultUncaughtExceptionHandler(this);
- }
-
- @Override
- public void onLowMemory() {
- super.onLowMemory();
- L.d("onLowMemory");
- }
-
- @Override
- public void uncaughtException(final Thread thread, final Throwable ex) {
- final boolean background = getMainLooper().getThread() != thread;
- if (background) {
- L.e("Uncaught exception in background thread " + thread, ex);
- final Handler handler = new Handler(getMainLooper());
- handler.post(
- () -> nullSafeUncaughtException(thread, ex));
- } else {
- nullSafeUncaughtException(thread, ex);
- }
- }
-
- private static void nullSafeUncaughtException(@NonNull Thread thread, @NonNull Throwable ex) {
- if (sSystemUncaughtExceptionHandler != null) {
- sSystemUncaughtExceptionHandler.uncaughtException(thread, ex);
- }
- }
-}
diff --git a/src/com/android/car/messenger/impl/common/ProjectionStateListener.java b/src/com/android/car/messenger/impl/common/ProjectionStateListener.java
deleted file mode 100644
index cb3b26d..0000000
--- a/src/com/android/car/messenger/impl/common/ProjectionStateListener.java
+++ /dev/null
@@ -1,166 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.messenger.impl.common;
-
-import android.bluetooth.BluetoothDevice;
-import android.car.Car;
-import android.car.CarProjectionManager;
-import android.car.projection.ProjectionStatus;
-import android.content.Context;
-import android.os.Bundle;
-import android.os.Parcelable;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.car.messenger.core.util.L;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * {@link ProjectionStatus} listener that exposes APIs to detect whether a projection application is
- * active.
- */
-public class ProjectionStateListener {
- @NonNull
- static final String PROJECTION_STATUS_EXTRA_DEVICE_STATE =
- "android.car.projection.DEVICE_STATE";
-
- @Nullable private CarProjectionManager mCarProjectionManager = null;
-
- @NonNull
- private final CarProjectionManager.ProjectionStatusListener mListener =
- (state, packageName, details) -> {
- mProjectionState = state;
- mProjectionDetails = details;
- };
-
- @Nullable private Car mCar;
-
- private int mProjectionState = ProjectionStatus.PROJECTION_STATE_INACTIVE;
- @NonNull private List<ProjectionStatus> mProjectionDetails = new ArrayList<>();
-
- public ProjectionStateListener(@NonNull Context context) {
- Car.createCar(
- context,
- /* handler= */ null,
- Car.CAR_WAIT_TIMEOUT_DO_NOT_WAIT,
- (car, ready) -> {
- mCar = car;
- mCarProjectionManager =
- (CarProjectionManager) mCar.getCarManager(Car.PROJECTION_SERVICE);
- if (mCarProjectionManager != null) {
- mCarProjectionManager.registerProjectionStatusListener(mListener);
- }
- });
- }
-
- /** Unregisters the listener. Should be called when the caller's lifecycle is ending. */
- public void destroy() {
- if (mCarProjectionManager != null) {
- mCarProjectionManager.unregisterProjectionStatusListener(mListener);
- }
- if (mCar != null) {
- mCar.disconnect();
- mCar = null;
- }
- mProjectionState = ProjectionStatus.PROJECTION_STATE_INACTIVE;
- mProjectionDetails = Collections.emptyList();
- }
-
- /**
- * Returns {@code true} if the input device currently has a projection app running in the
- * foreground.
- *
- * @param bluetoothAddress of the device that should be checked. If null, return whether any
- * device is currently running a projection app in the foreground.
- */
- public boolean isProjectionInActiveForeground(@Nullable String bluetoothAddress) {
- if (bluetoothAddress == null) {
- L.i("returning non-device-specific projection status");
- return isProjectionInActiveForeground();
- }
-
- if (!isProjectionInActiveForeground()) {
- return false;
- }
-
- for (ProjectionStatus status : mProjectionDetails) {
- if (!status.isActive()) {
- // Don't suppress UI for packages that aren't actively projecting.
- L.d("skip non-projecting package " + status.getPackageName());
- continue;
- }
-
- for (ProjectionStatus.MobileDevice device : status.getConnectedMobileDevices()) {
- if (!device.isProjecting()) {
- // Don't suppress UI for devices that aren't foreground.
- L.d("skip non-projecting device " + device.getName());
- continue;
- }
-
- Bundle extras = device.getExtras();
- if (extras.getInt(
- PROJECTION_STATUS_EXTRA_DEVICE_STATE,
- ProjectionStatus.PROJECTION_STATE_ACTIVE_FOREGROUND)
- != ProjectionStatus.PROJECTION_STATE_ACTIVE_FOREGROUND) {
- L.d("skip device " + device.getName() + " - not foreground");
- continue;
- }
-
- Parcelable projectingBluetoothDevice =
- extras.getParcelable(BluetoothDevice.EXTRA_DEVICE);
- L.d("Device " + device.getName() + " has BT device " + projectingBluetoothDevice);
-
- if (projectingBluetoothDevice == null) {
- L.i(
- "Suppressing message notification - device "
- + device
- + " is projection, and does not specify a Bluetooth address");
- return true;
- } else if (!(projectingBluetoothDevice instanceof BluetoothDevice)) {
- L.e(
- "Device "
- + device
- + " has bad EXTRA_DEVICE value "
- + projectingBluetoothDevice
- + " - treating as unspecified");
- return true;
- } else if (bluetoothAddress.equals(
- ((BluetoothDevice) projectingBluetoothDevice).getAddress())) {
- L.i(
- "Suppressing message notification - device "
- + device
- + "is projecting, and message is coming from device's"
- + " Bluetooth address"
- + bluetoothAddress);
- return true;
- }
- }
- }
-
- // No projecting apps want to suppress this device, so let it through.
- return false;
- }
-
- /** Returns {@code true} if a projection app is active in the foreground. */
- private boolean isProjectionInActiveForeground() {
- return mProjectionState == ProjectionStatus.PROJECTION_STATE_ACTIVE_FOREGROUND;
- }
-}
diff --git a/src/com/android/car/messenger/impl/datamodels/ContentProviderLiveData.java b/src/com/android/car/messenger/impl/datamodels/ContentProviderLiveData.java
deleted file mode 100644
index 24a2d97..0000000
--- a/src/com/android/car/messenger/impl/datamodels/ContentProviderLiveData.java
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.messenger.impl.datamodels;
-
-import androidx.lifecycle.MutableLiveData;
-import android.content.Context;
-import android.database.ContentObserver;
-import android.net.Uri;
-
-import androidx.annotation.NonNull;
-
-import com.android.car.messenger.core.interfaces.AppFactory;
-
-/**
- * Abstract class for Content Provider live data implementations
- *
- * @param <T> the class type emitted from the live data to observers
- */
-public abstract class ContentProviderLiveData<T> extends MutableLiveData<T> {
- @NonNull
- private final ContentObserver mContentObserver =
- new ContentObserver(null) {
- @Override
- public void onChange(boolean selfChange) {
- onDataChange();
- }
- };
-
- @NonNull private final Uri[] mUris;
- private boolean mIsRegistered = false;
-
- /** Constructor that takes in a list of content provider uris to observe */
- public ContentProviderLiveData(@NonNull Uri... uris) {
- mUris = uris;
- }
-
- @Override
- protected void onActive() {
- if (!mIsRegistered) {
- for (Uri uri : mUris) {
- getContext()
- .getContentResolver()
- .registerContentObserver(
- uri, /* notifyForDescendants =*/ true, mContentObserver);
- mIsRegistered = true;
- }
- }
- }
-
- @Override
- protected void onInactive() {
- getContext().getContentResolver().unregisterContentObserver(mContentObserver);
- mIsRegistered = false;
- }
-
- /** Get Context for use in Live Data */
- @NonNull
- public Context getContext() {
- return AppFactory.get().getContext();
- }
-
- /** Abstract method called on data change */
- public abstract void onDataChange();
-}
diff --git a/src/com/android/car/messenger/impl/datamodels/ConversationItemLiveData.java b/src/com/android/car/messenger/impl/datamodels/ConversationItemLiveData.java
deleted file mode 100644
index 3d8184d..0000000
--- a/src/com/android/car/messenger/impl/datamodels/ConversationItemLiveData.java
+++ /dev/null
@@ -1,284 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.messenger.impl.datamodels;
-
-import static android.provider.Telephony.ThreadsColumns.READ;
-
-import static java.util.stream.Collectors.joining;
-
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.database.Cursor;
-import android.database.CursorIndexOutOfBoundsException;
-import android.graphics.Bitmap;
-import android.provider.Telephony.Mms;
-import android.provider.Telephony.MmsSms;
-import android.provider.Telephony.Sms;
-import androidx.core.graphics.drawable.IconCompat;
-import android.util.Pair;
-
-import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.core.app.Person;
-
-import com.android.car.messenger.common.Conversation;
-import com.android.car.messenger.common.Conversation.Message;
-import com.android.car.messenger.core.interfaces.AppFactory;
-import com.android.car.messenger.core.shared.MessageConstants;
-import com.android.car.messenger.core.util.ConversationUtil;
-import com.android.car.messenger.core.util.L;
-import com.android.car.messenger.impl.datamodels.ConversationItemLiveData.ConversationChangeSet;
-import com.android.car.messenger.impl.datamodels.util.AvatarUtil;
-import com.android.car.messenger.impl.datamodels.util.ContactUtils;
-import com.android.car.messenger.impl.datamodels.util.CursorUtils;
-import com.android.car.messenger.impl.datamodels.util.MessageUtils;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-/**
- * Conversation Item Observer for listening for changes for the Conversation Item Metadata
- *
- * <p>Returns a Conversation object holding the most relevant metadata for the Conversation.
- *
- * <p>{@link Conversation#getMessages() holds the relevant messages to read out to the user,
- * in this priority:
- * <ul>
- * <li>Unread messages
- * <li>Read messages after last reply
- * </ul>
- * <p>Given that the Content observer notifies for each cahgne to the telephony database,
- * for optimization reasons, database search is limited number to
- * the {@link ConversationItemLiveData#ITEM_LIMIT}.
- */
-class ConversationItemLiveData extends ContentProviderLiveData<ConversationChangeSet> {
- @NonNull private final String mConversationId;
- private static final int ITEM_LIMIT = 10;
- @NonNull private static final String COMMA_SEPARATOR = ", ";
-
- @NonNull
- private final SharedPreferences.OnSharedPreferenceChangeListener mPreferenceChangeListener =
- (sharedPreferences, key) -> onSharedPreferenceChangedInternal(key);
-
- /** Constructor that takes in a conversation id */
- ConversationItemLiveData(@NonNull String conversationId) {
- super(Sms.CONTENT_URI, Mms.CONTENT_URI, MmsSms.CONTENT_URI);
- mConversationId = conversationId;
- }
-
- @Override
- protected void onActive() {
- super.onActive();
- SharedPreferences sharedPrefs = AppFactory.get().getSharedPreferences();
- sharedPrefs.registerOnSharedPreferenceChangeListener(mPreferenceChangeListener);
- if (getValue() == null) {
- onDataChange();
- }
- }
-
- @Override
- protected void onInactive() {
- super.onInactive();
- SharedPreferences sharedPrefs = AppFactory.get().getSharedPreferences();
- sharedPrefs.unregisterOnSharedPreferenceChangeListener(mPreferenceChangeListener);
- }
-
- // Handles on Data Change
- @Override
- public void onDataChange() {
- try {
- onDataChangeInternal();
- } catch (CursorIndexOutOfBoundsException e) {
- L.w(
- "Error fetching conversation item details. Posting null to trigger caller "
- + "to remove conversation");
- postValue(null);
- }
- }
-
- private void onDataChangeInternal() {
- if (!verifyChangeOccurred()) {
- return;
- }
- Conversation conversation = getValue() == null ? null : getValue().mConversation;
- Conversation.Builder conversationBuilder =
- conversation == null
- ? initConversationBuilder(mConversationId)
- : conversation.toBuilder();
- Cursor messagesCursor =
- CursorUtils.getMessagesCursor(mConversationId, ITEM_LIMIT, /* offset= */ 0);
- // messages to read: first get unread messages
- List<Message> messagesToRead = MessageUtils.getUnreadMessages(messagesCursor);
- int unreadCount = messagesToRead.size();
- long lastReplyTimestamp = 0L;
-
- // if no unread messages and notify level is all, get read messages
- if (messagesToRead.isEmpty()) {
- Pair<List<Message>, Long> readMessagesAndReplyTimestamp =
- MessageUtils.getReadMessagesAndReplyTimestamp(messagesCursor);
- messagesToRead = readMessagesAndReplyTimestamp.first;
- lastReplyTimestamp = readMessagesAndReplyTimestamp.second;
- }
- updateConversation(conversationBuilder, messagesToRead, unreadCount, lastReplyTimestamp);
- }
-
- private boolean verifyChangeOccurred() {
- // get the last message after offset to see if any change exists
- // check the last message with only the connection time offset
- Cursor messagesCursor =
- CursorUtils.getMessagesCursor(mConversationId, /* limit= */ 1, /* offset= */ 0);
- if (messagesCursor == null || !messagesCursor.moveToFirst()) {
- return false;
- }
- Conversation conversation = getValue() == null ? null : getValue().mConversation;
- Message prevLastMessage = ConversationUtil.getLastMessage(conversation);
- Message message = MessageUtils.parseCurrentMessage(messagesCursor);
- // checks to see if any change has been made to the last previous message,
- // if status changes to read, message status will be different
- // if status changes to replied messages, note: only timestamp will change,
- // as we don't add reply messages to the message list
- return message != null
- && (prevLastMessage == null
- || prevLastMessage.getMessageStatus() != message.getMessageStatus()
- || ConversationUtil.getConversationTimestamp(conversation)
- != message.getTimestamp());
- }
-
- @NonNull
- private Conversation.Builder initConversationBuilder(@NonNull String conversationId) {
- String userName = ContactUtils.DRIVER_NAME;
- Conversation.Builder builder =
- new Conversation.Builder(
- new Person.Builder().setName(userName).build(), conversationId);
- setConversationIconTitleAndParticipants(conversationId, builder);
- builder.setMuted(getMutedList().contains(conversationId));
- return builder;
- }
-
- private void setConversationIconTitleAndParticipants(
- @NonNull String conversationId, Conversation.Builder builder) {
- List<CharSequence> participantNames = new ArrayList<>();
- List<Bitmap> participantIcons = new ArrayList<>();
- List<Person> participants =
- ContactUtils.getRecipients(
- conversationId,
- (name, bitmap) -> {
- participantNames.add(name);
- participantIcons.add(bitmap);
- });
- builder.setParticipants(participants);
- builder.setConversationTitle(participantNames.stream().collect(joining(COMMA_SEPARATOR)));
- Bitmap bitmap = AvatarUtil.createGroupAvatar(getContext(), participantIcons);
- if (bitmap != null) {
- builder.setConversationIcon(IconCompat.createWithBitmap(bitmap));
- }
- }
-
- private void updateConversation(
- @NonNull Conversation.Builder conversationBuilder,
- @NonNull List<Message> messages,
- int unreadCount,
- long lastReplyTimestamp) {
- conversationBuilder.setMessages(messages).setUnreadCount(unreadCount);
- ConversationUtil.setReplyTimestampAsAnExtra(
- conversationBuilder, /* extras= */ null, lastReplyTimestamp);
- Conversation conversation = conversationBuilder.build();
- postValue(new ConversationChangeSet(conversation, NotifyLevel.NEW_OR_UPDATED_MESSAGE));
- }
-
- @NonNull
- private static Set<String> getMutedList() {
- SharedPreferences sharedPreferences = AppFactory.get().getSharedPreferences();
- return sharedPreferences.getStringSet(
- MessageConstants.KEY_MUTED_CONVERSATIONS, new HashSet<>());
- }
-
- private void onSharedPreferenceChangedInternal(@NonNull String key) {
- Conversation item = getValue() == null ? null : getValue().mConversation;
- if (!MessageConstants.KEY_MUTED_CONVERSATIONS.equals(key) || item == null) {
- return;
- }
- Set<String> list = getMutedList();
- boolean isMuted = list.contains(mConversationId);
- boolean wasPreviouslyMuted = item.isMuted();
- if (isMuted == wasPreviouslyMuted) {
- return;
- }
- Conversation.Builder builder = item.toBuilder();
- builder.setMuted(isMuted);
- postValue(
- new ConversationChangeSet(
- /* conversation= */ builder.build(), NotifyLevel.METADATA));
- }
-
- /** Mark conversation as Read */
- public static void markAsRead(@NonNull String conversationId) {
- L.d("markAsRead for conversationId: " + conversationId);
- Context context = AppFactory.get().getContext();
- ContentValues values = new ContentValues();
- values.put(READ, 1);
- context.getContentResolver()
- .update(CursorUtils.getConversationUri(conversationId), values, /* extras= */ null);
- }
-
- public static class ConversationChangeSet {
-
- @NonNull private final Conversation mConversation;
- @NotifyLevel private final int mChange;
-
- private ConversationChangeSet(@NonNull Conversation conversation, @NotifyLevel int change) {
- mConversation = conversation;
- mChange = change;
- }
-
- @NonNull
- public Conversation getConversation() {
- return mConversation;
- }
-
- @NotifyLevel
- public int getChange() {
- return mChange;
- }
- }
-
- /** Indicates the granularity of changes the observer wishes to observe */
- @IntDef(
- value = {
- NotifyLevel.METADATA,
- NotifyLevel.NEW_OR_UPDATED_MESSAGE,
- })
- @Retention(RetentionPolicy.SOURCE)
- public @interface NotifyLevel {
- /**
- * When set, this indicates that the change included the metadata of the Conversation, such
- * as the title, avatar or mute changes
- */
- int METADATA = 0;
-
- /**
- * When set, this indicates that the change included new or updated messages as part of the
- * change set.
- */
- int NEW_OR_UPDATED_MESSAGE = 1;
- }
-}
diff --git a/src/com/android/car/messenger/impl/datamodels/ConversationsPerDeviceLiveData.java b/src/com/android/car/messenger/impl/datamodels/ConversationsPerDeviceLiveData.java
deleted file mode 100644
index fddb9fc..0000000
--- a/src/com/android/car/messenger/impl/datamodels/ConversationsPerDeviceLiveData.java
+++ /dev/null
@@ -1,163 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.messenger.impl.datamodels;
-
-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 android.content.ContentResolver;
-import android.database.Cursor;
-import android.net.Uri;
-import android.telephony.SubscriptionInfo;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.car.messenger.core.interfaces.AppFactory;
-import com.android.car.messenger.core.util.L;
-import com.android.car.messenger.impl.datamodels.ConversationsPerDeviceLiveData.ConversationIdChangeList;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Set;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-/** Returns {@link ConversationIdChangeList}, which holds the Conversation Ids Per SIM */
-class ConversationsPerDeviceLiveData extends ContentProviderLiveData<ConversationIdChangeList> {
- @NonNull private static final Uri URI = CONTENT_CONVERSATIONS_URI;
-
- @NonNull
- private static final String[] PROJECTION = {
- SUBSCRIPTION_ID, THREAD_ID,
- };
-
- @NonNull private final ContentResolver mContentResolver;
- private final int mAccountId;
-
- ConversationsPerDeviceLiveData(int accountId) {
- super(URI);
- mContentResolver = AppFactory.get().getContext().getContentResolver();
- mAccountId = accountId;
- }
-
- @Nullable
- private Cursor getCursor() {
- return mContentResolver.query(
- URI,
- PROJECTION,
- /* selection= */ SUBSCRIPTION_ID + "=" + mAccountId,
- /* selectionArgs= */ null,
- /* sortOrder= */ null,
- /* cancellationSignal= */ null);
- }
-
- @Override
- protected void onActive() {
- super.onActive();
- if (getValue() == null) {
- onDataChange();
- }
- }
-
- @Override
- public void onDataChange() {
- Cursor cursor = getCursor();
- if (cursor == null || !cursor.moveToFirst()) {
- L.d("No conversation data found. Setting LiveData to null");
- postValue(new ConversationIdChangeList());
- return;
- }
-
- ArrayList<String> currentConversationIds = new ArrayList<>();
- do {
- String conversationId = cursor.getString(cursor.getColumnIndex(THREAD_ID));
- currentConversationIds.add(conversationId);
- } while (cursor.moveToNext());
-
- // get updated changes
- Collection<String> prevConversationIds = getValueOrEmpty().getAllConversationIds();
- Set<String> newConversations = getDifference(currentConversationIds, prevConversationIds);
- Set<String> removedConversations =
- getDifference(prevConversationIds, currentConversationIds);
-
- if (newConversations.isEmpty() && removedConversations.isEmpty()) {
- return;
- }
-
- ConversationIdChangeList changeList = new ConversationIdChangeList();
- changeList.mConversationIds = currentConversationIds;
- changeList.mAddedConversationIds = newConversations;
- changeList.mRemovedConversationIds = removedConversations;
-
- postValue(changeList);
- }
-
- @NonNull
- private ConversationIdChangeList getValueOrEmpty() {
- ConversationIdChangeList changeList = getValue();
- if (changeList == null) {
- changeList = new ConversationIdChangeList();
- }
- return changeList;
- }
-
- /**
- * Returns a set that contains a difference between the two lists - firstList - secondList =
- * result
- *
- * <p>This essentially points out which items or changes are not present in firstList.
- */
- @NonNull
- private static Set<String> getDifference(
- @NonNull Collection<String> firstList, @NonNull Collection<String> secondList) {
- return firstList.stream()
- .filter(it -> !secondList.contains(it))
- .collect(Collectors.toSet());
- }
-
- /**
- * Holds the list of conversation ids per {@link SubscriptionInfo#getSubscriptionId()}
- * Additional information such as which specific conversation ids have changed is also provided.
- */
- public static class ConversationIdChangeList {
- @NonNull private Collection<String> mConversationIds = new ArrayList<>();
- @NonNull private Collection<String> mRemovedConversationIds = new ArrayList<>();
- @NonNull private Collection<String> mAddedConversationIds = new ArrayList<>();
-
- private ConversationIdChangeList() {}
-
- /* Returns the list of added conversation Ids */
- @NonNull
- public Collection<String> getAllConversationIds() {
- return mConversationIds;
- }
-
- /* Returns the list of added conversation Ids */
- @NonNull
- public Stream<String> getRemovedConversationIds() {
- return mRemovedConversationIds.stream();
- }
-
- /* Returns the list of removed conversation Ids */
- @NonNull
- public Stream<String> getAddedConversationIds() {
- return mAddedConversationIds.stream();
- }
- }
-}
diff --git a/src/com/android/car/messenger/impl/datamodels/TelephonyDataModel.java b/src/com/android/car/messenger/impl/datamodels/TelephonyDataModel.java
deleted file mode 100644
index 7ae4a1f..0000000
--- a/src/com/android/car/messenger/impl/datamodels/TelephonyDataModel.java
+++ /dev/null
@@ -1,315 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.messenger.impl.datamodels;
-
-import static com.android.car.messenger.core.shared.MessageConstants.KEY_MUTED_CONVERSATIONS;
-
-import static java.util.Comparator.comparingLong;
-
-import androidx.lifecycle.LiveData;
-import androidx.lifecycle.MediatorLiveData;
-import androidx.lifecycle.Observer;
-import androidx.lifecycle.Transformations;
-import android.content.SharedPreferences;
-import android.net.Uri;
-import android.provider.Telephony;
-import android.telephony.SmsManager;
-import android.util.Pair;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.car.messenger.common.Conversation;
-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.ConversationUtil;
-import com.android.car.messenger.core.util.L;
-import com.android.car.messenger.impl.common.ProjectionStateListener;
-import com.android.car.messenger.impl.datamodels.ConversationItemLiveData.ConversationChangeSet;
-import com.android.car.messenger.impl.datamodels.ConversationItemLiveData.NotifyLevel;
-import com.android.car.messenger.impl.datamodels.ConversationsPerDeviceLiveData.ConversationIdChangeList;
-import com.android.car.messenger.impl.datamodels.UserAccountLiveData.UserAccountChangeList;
-
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-/** Queries the telephony data model to retrieve the SMS/MMS messages */
-public class TelephonyDataModel implements DataModel {
-
- @NonNull
- private final HashMap<String, LiveData<ConversationChangeSet>> mConvoIdToConversationLiveData =
- new HashMap<>();
-
- @NonNull
- private final HashMap<Integer, ConversationsPerDeviceLiveData>
- mAccountIdToConversationListLiveData = new HashMap<>();
-
- @NonNull private final HashMap<String, Conversation> mConversationMap = new HashMap<>();
-
- @NonNull
- private static final Comparator<Conversation> sConversationComparator =
- comparingLong(ConversationUtil::getConversationTimestamp).reversed();
-
- @NonNull
- ProjectionStateListener mProjectionStateListener =
- new ProjectionStateListener(AppFactory.get().getContext());
-
- @NonNull
- @Override
- public LiveData<Collection<UserAccount>> getAccounts() {
- return Transformations.map(
- UserAccountLiveData.getInstance(), UserAccountChangeList::getAccounts);
- }
-
- @NonNull
- @Override
- public LiveData<Collection<Conversation>> getConversations(@NonNull UserAccount userAccount) {
- MediatorLiveData<Collection<Conversation>> liveData = new MediatorLiveData<>();
- subscribeToConversationItemChanges(
- userAccount,
- liveData,
- /* onConversationItemChanged= */ conversationChangeSet -> {
- mConversationMap.put(
- conversationChangeSet.getConversation().getId(),
- conversationChangeSet.getConversation());
- liveData.postValue(
- mConversationMap.values().stream()
- .sorted(sConversationComparator)
- .collect(Collectors.toList()));
- },
- /* onConversationRemoved= */ conversationId -> {
- mConversationMap.remove(conversationId);
- liveData.postValue(
- mConversationMap.values().stream()
- .sorted(sConversationComparator)
- .collect(Collectors.toList()));
- },
- /* onEmpty= */ onEmpty -> liveData.postValue(new ArrayList<>()));
-
- return liveData;
- }
-
- @NonNull
- @Override
- public LiveData<Conversation> getUnreadMessages() {
- MediatorLiveData<Conversation> liveData = new MediatorLiveData<>();
- subscribeToUserAccountConversationItem(
- liveData,
- conversationChangeSet -> {
- UserAccount userAccount = conversationChangeSet.first;
- ConversationChangeSet conversationItemChangeSet = conversationChangeSet.second;
- Conversation conversation = conversationItemChangeSet.getConversation();
- if (conversation.getUnreadCount() > 0
- && conversationItemChangeSet.getChange()
- == NotifyLevel.NEW_OR_UPDATED_MESSAGE
- && userAccount
- .getConnectionTime()
- .isBefore(
- Instant.ofEpochMilli(
- ConversationUtil.getConversationTimestamp(
- conversation)))) {
- if (hasProjectionInForeground(userAccount)) {
- L.d("Ignoring new message as projection is in foreground");
- return;
- }
- liveData.postValue(conversation);
- }
- });
- return liveData;
- }
-
- @Override
- public void muteConversation(@NonNull String conversationId, boolean mute) {
- SharedPreferences sharedPreferences = AppFactory.get().getSharedPreferences();
- Set<String> mutedConversations =
- sharedPreferences.getStringSet(KEY_MUTED_CONVERSATIONS, new HashSet<>());
- Set<String> finalSet = new HashSet<>(mutedConversations);
- if (mute) {
- finalSet.add(conversationId);
- } else {
- finalSet.remove(conversationId);
- }
- sharedPreferences.edit().putStringSet(KEY_MUTED_CONVERSATIONS, finalSet).apply();
- }
-
- @Override
- public void markAsRead(@NonNull String conversationId) {
- ConversationItemLiveData.markAsRead(conversationId);
- }
-
- @Override
- public void sendMessage(@NonNull String conversationId, @NonNull String message) {
- L.d("Sending a message to a conversation");
- String destination =
- Uri.withAppendedPath(Telephony.Threads.CONTENT_URI, conversationId).toString();
- SmsManager.getDefault()
- .sendTextMessage(
- destination,
- /* scAddress= */ null,
- message,
- /* sentIntent= */ null,
- /* 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);
- }
-
- /** Avoids crash when adding source */
- private <T, S> void safeAddSource(
- @NonNull MediatorLiveData<T> liveData,
- @NonNull LiveData<S> source,
- @NonNull Observer<? super S> onChanged) {
- try {
- liveData.addSource(source, onChanged);
- } catch (IllegalArgumentException e) {
- L.w("We are already subscribed to the source, ignoring.");
- }
- }
-
- private <T> void subscribeToUserAccountConversationItem(
- @NonNull MediatorLiveData<T> liveData,
- @NonNull
- Observer<? super Pair<UserAccount, ConversationChangeSet>>
- onConversationChanged) {
- safeAddSource(
- liveData,
- UserAccountLiveData.getInstance(),
- userAccountChangeList -> {
- userAccountChangeList
- .getAddedAccounts()
- .forEach(
- userAccount ->
- subscribeToConversationItemChanges(
- userAccount,
- liveData,
- it ->
- onConversationChanged.onChanged(
- new Pair<>(userAccount, it)),
- /* onConversationRemoved= */ null,
- /* onEmpty= */ null));
-
- userAccountChangeList
- .getRemovedAccounts()
- .forEach(
- userAccount ->
- liveData.removeSource(
- getConversationIds(userAccount.getId())));
- });
- }
-
- private <T> void subscribeToConversationItemChanges(
- @NonNull UserAccount userAccount,
- @NonNull MediatorLiveData<T> liveData,
- @NonNull Observer<? super ConversationChangeSet> onConversationItemChanged,
- @Nullable Observer<String> onConversationRemoved,
- @Nullable Observer<Boolean> onEmpty) {
- safeAddSource(
- liveData,
- getConversationIds(userAccount.getId()),
- conversationIdChangeList ->
- subscribeToConversationItemChanges(
- conversationIdChangeList,
- liveData,
- onConversationItemChanged,
- onConversationRemoved,
- onEmpty));
- }
-
- private <T> void subscribeToConversationItemChanges(
- @NonNull ConversationIdChangeList conversationIdChangeList,
- @NonNull MediatorLiveData<T> liveData,
- @NonNull Observer<? super ConversationChangeSet> onConversationItemChanged,
- @Nullable Observer<String> onConversationRemoved,
- @Nullable Observer<Boolean> onEmpty) {
- conversationIdChangeList
- .getAddedConversationIds()
- .forEach(
- conversationId ->
- safeAddSource(
- liveData,
- getConversationItem(conversationId),
- it -> {
- if (it == null) {
- if (onConversationRemoved != null) {
- onConversationRemoved.onChanged(conversationId);
- }
- return;
- }
- onConversationItemChanged.onChanged(it);
- }));
-
- conversationIdChangeList
- .getRemovedConversationIds()
- .forEach(
- conversationId -> {
- liveData.removeSource(getConversationItem(conversationId));
- if (onConversationRemoved != null) {
- onConversationRemoved.onChanged(conversationId);
- }
- });
-
- if (conversationIdChangeList.getAllConversationIds().isEmpty()) {
- L.d("No conversation lists found for user account.");
- if (onEmpty != null) {
- onEmpty.onChanged(true);
- }
- }
- }
-
- private boolean hasProjectionInForeground(@NonNull UserAccount userAccount) {
- return mProjectionStateListener.isProjectionInActiveForeground(userAccount.getIccId());
- }
-
- @NonNull
- private ConversationsPerDeviceLiveData getConversationIds(int accountId) {
- return mAccountIdToConversationListLiveData.computeIfAbsent(
- accountId, it -> new ConversationsPerDeviceLiveData(accountId));
- }
-
- @NonNull
- private LiveData<ConversationChangeSet> getConversationItem(@NonNull String conversationId) {
- return mConvoIdToConversationLiveData.computeIfAbsent(
- conversationId, it -> new ConversationItemLiveData(conversationId));
- }
-}
diff --git a/src/com/android/car/messenger/impl/datamodels/UserAccountLiveData.java b/src/com/android/car/messenger/impl/datamodels/UserAccountLiveData.java
deleted file mode 100644
index ab09cbc..0000000
--- a/src/com/android/car/messenger/impl/datamodels/UserAccountLiveData.java
+++ /dev/null
@@ -1,188 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.car.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 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;
-import java.util.List;
-import java.util.Set;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-/**
- * Get the UserAccount of the currently active remote bluetooth SIM(s). The records will be sorted
- * by {@link SubscriptionInfo#getSimSlotIndex} then by {@link SubscriptionInfo#getSubscriptionId}.
- *
- * <p>Requires Permission: {@link android.Manifest.permission#READ_PHONE_STATE READ_PHONE_STATE} or
- * that the calling app has carrier privileges (see {@link TelephonyManager#hasCarrierPrivileges}).
- * In the latter case, only records accessible to the calling app are returned.
- *
- * <p>Listens for changes via {@link OnSubscriptionsChangedListener} and the latest data can be
- * observed and retrieved via {@link LiveData#getValue()}.
- *
- * <p>Sorts list of the currently {@link SubscriptionInfo} records available on the device
- *
- * <ul>
- * <li>If the list is empty then there are no {@link SubscriptionInfo} records currently
- * available.
- * <li>if the list is non-empty the list is sorted by {@link SubscriptionInfo#getSimSlotIndex}
- * then by {@link SubscriptionInfo#getSubscriptionId}.
- * </ul>
- */
-public class UserAccountLiveData extends LiveData<UserAccountChangeList> {
- @NonNull private final SubscriptionManager mSubscriptionManager;
-
- @NonNull
- private final OnSubscriptionsChangedListener mOnChangeListener =
- new OnSubscriptionsChangedListener() {
- @Override
- public void onSubscriptionsChanged() {
- loadValue();
- }
- };
-
- @Nullable private static UserAccountLiveData sInstance;
-
- private UserAccountLiveData() {
- Context context = AppFactory.get().getContext();
- mSubscriptionManager = context.getSystemService(SubscriptionManager.class);
- mSubscriptionManager.addOnSubscriptionsChangedListener(mOnChangeListener);
- }
-
- /** Gets the instance of {@link UserAccountLiveData} */
- @NonNull
- public static UserAccountLiveData getInstance() {
- if (sInstance == null) {
- sInstance = new UserAccountLiveData();
- }
- return sInstance;
- }
-
- @Override
- protected void onActive() {
- if (getValue() == null) {
- loadValue();
- }
- }
-
- private void loadValue() {
- List<UserAccount> accounts =
- mSubscriptionManager.getActiveSubscriptionInfoList().stream()
- .map(
- it -> {
- int subscriptionId = it.getSubscriptionId();
- String iccId = it.getIccId();
- String displayName = it.getDisplayName().toString();
- return new UserAccount(
- subscriptionId, displayName, iccId, Instant.now());
- })
- .collect(Collectors.toList());
-
- // get the removed accounts and added accounts.
- Collection<UserAccount> prevUserAccounts = getValueOrEmpty().mAccounts;
- Set<UserAccount> addedAccounts = getDifference(accounts, prevUserAccounts);
- Set<UserAccount> removedAccounts = getDifference(prevUserAccounts, accounts);
-
- if (addedAccounts.isEmpty() && removedAccounts.isEmpty()) {
- return;
- }
-
- UserAccountChangeList newAccountChangeList = new UserAccountChangeList();
- newAccountChangeList.mAccounts = accounts;
- newAccountChangeList.mAddedAccounts = addedAccounts;
- newAccountChangeList.mRemovedAccounts = removedAccounts;
- postValue(newAccountChangeList);
- }
-
- /**
- * Returns User Account with the given iccId
- *
- * @param iccId Maps to the {@link SubscriptionInfo#getIccId()}
- */
- @Nullable
- public static UserAccount getUserAccount(@NonNull String iccId) {
- Collection<UserAccount> userAccounts = getValueOrEmpty().getAccounts();
- for (UserAccount account : userAccounts) {
- if (iccId.equals(account.getIccId())) {
- return account;
- }
- }
- return null;
- }
-
- /**
- * Returns a list that contains a difference between the two lists - firstList - secondList =
- * result This essentially points out which items or changes are not present in firstList.
- */
- @NonNull
- public static Set<UserAccount> getDifference(
- @NonNull Collection<UserAccount> firstList,
- @NonNull Collection<UserAccount> secondList) {
- return firstList.stream()
- .filter(it -> secondList.stream().noneMatch(item -> item.getId() == it.getId()))
- .collect(Collectors.toSet());
- }
-
- /** A list of {@link UserAccount} with information on what changed */
- public static class UserAccountChangeList {
- @NonNull private Collection<UserAccount> mAccounts = new ArrayList<>();
- @NonNull private Collection<UserAccount> mRemovedAccounts = new ArrayList<>();
- @NonNull private Collection<UserAccount> mAddedAccounts = new ArrayList<>();
-
- /** Get all user accounts */
- @NonNull
- public Collection<UserAccount> getAccounts() {
- return mAccounts;
- }
-
- /** Get removed accounts for this change list */
- @NonNull
- public Stream<UserAccount> getRemovedAccounts() {
- return mRemovedAccounts.stream();
- }
-
- /** Gets added accounts for this change list */
- @NonNull
- public Stream<UserAccount> getAddedAccounts() {
- return mAddedAccounts.stream();
- }
- }
-
- /** Returns the value or empty changelist, if value is null */
- @NonNull
- private static UserAccountChangeList getValueOrEmpty() {
- UserAccountChangeList value = sInstance != null ? sInstance.getValue() : null;
- if (value == null) {
- value = new UserAccountChangeList();
- }
- return value;
- }
-}
diff --git a/src/com/android/car/messenger/impl/datamodels/util/AvatarUtil.java b/src/com/android/car/messenger/impl/datamodels/util/AvatarUtil.java
deleted file mode 100644
index fbd9818..0000000
--- a/src/com/android/car/messenger/impl/datamodels/util/AvatarUtil.java
+++ /dev/null
@@ -1,412 +0,0 @@
-/*
- * 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.impl.datamodels.util;
-
-import static java.lang.Math.min;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.Bitmap.Config;
-import android.graphics.BitmapShader;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Matrix;
-import android.graphics.Paint;
-import android.graphics.Path;
-import android.graphics.RectF;
-import android.graphics.Shader.TileMode;
-import android.text.TextUtils;
-
-import androidx.annotation.ColorInt;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.car.messenger.R;
-import com.android.car.messenger.core.ui.shared.LetterTileDrawable;
-
-import java.util.List;
-
-/**
- * Avatar Utils for generating conversation and contact avatars
- *
- * <p>For historical context, AvatarUtil is derived from Android Messages implementation of group
- * avatars particularly from these sources:
- *
- * <p>AvatarGroupRequestDescriptor#generateDestRectArray:
- * packages/apps/Messaging/src/com/android/messaging/datamodel/media/AvatarGroupRequestDescriptor
- *
- * <p>CompositeImageRequest#loadMediaInternal:
- * packages/apps/Messaging/src/com/android/messaging/datamodel/media/CompositeImageRequest
- *
- * <p>ImageUtils#drawBitmapWithCircleOnCanvas:
- * packages/apps/Messaging/src/com/android/messaging/util/ImageUtils.java
- *
- * <p>Current implementation is close to reference. However, future iterations can diverge.
- */
-public final class AvatarUtil {
-
- private AvatarUtil() {}
-
- private static final class GroupAvatarConfigs {
- int mWidth;
- int mHeight;
- int mMaximumGroupSize;
- int mBackgroundColor;
- int mStrokeColor;
- boolean mFillBackground;
- }
-
- /**
- * Supports creating a group avatar: a minimum of 1 avatar and a maximum of four avatars are
- * supported. Any avatars beyond the 4th index is ignored.
- */
- @Nullable
- public static Bitmap createGroupAvatar(
- @NonNull Context context, @Nullable List<Bitmap> participantsIcon) {
- if (participantsIcon == null || participantsIcon.isEmpty()) {
- return null;
- }
-
- GroupAvatarConfigs groupAvatarConfigs = new GroupAvatarConfigs();
- Resources resources = context.getResources();
- int size = resources.getDimensionPixelSize(R.dimen.conversation_avatar_width);
- groupAvatarConfigs.mWidth = size;
- groupAvatarConfigs.mHeight = size;
- groupAvatarConfigs.mMaximumGroupSize =
- resources.getInteger(R.integer.group_avatar_max_group_size);
- groupAvatarConfigs.mBackgroundColor =
- resources.getColor(R.color.group_avatar_background_color, context.getTheme());
- groupAvatarConfigs.mStrokeColor =
- resources.getColor(R.color.group_avatar_stroke_color, context.getTheme());
- groupAvatarConfigs.mFillBackground =
- context.getResources().getBoolean(R.bool.group_avatar_fill_background);
-
- if (participantsIcon.size() == 1 || groupAvatarConfigs.mMaximumGroupSize == 1) {
- return participantsIcon.get(0);
- }
-
- return createGroupAvatarBitmap(participantsIcon, groupAvatarConfigs);
- }
-
- /**
- * Resolves person avatar to either the provided bitmap clipped into a circle or a letter
- * drawable
- */
- @Nullable
- public static Bitmap resolvePersonAvatar(
- @NonNull Context context, @Nullable Bitmap bitmap, @Nullable CharSequence name) {
- if (bitmap != null) {
- return AvatarUtil.createClippedCircle(bitmap);
- } else {
- return createLetterTile(context, name);
- }
- }
-
- /**
- * Create a {@link Bitmap} for the given name
- *
- * @param name will decide the color for the drawable. If null, a default color will be used.
- */
- @Nullable
- private static Bitmap createLetterTile(@NonNull Context context, @Nullable CharSequence name) {
- if (TextUtils.isEmpty(name)) {
- return null;
- }
- char firstInitial = name.charAt(0);
- String letters = Character.isLetter(firstInitial) ? Character.toString(firstInitial) : null;
- LetterTileDrawable drawable =
- new LetterTileDrawable(context.getResources(), letters, name.toString());
- int size = context.getResources().getDimensionPixelSize(R.dimen.conversation_avatar_width);
- return drawable.toBitmap(size);
- }
-
- /** Returns a circle-clipped bitmap */
- @NonNull
- private static Bitmap createClippedCircle(Bitmap bitmap) {
- final int width = bitmap.getWidth();
- final int height = bitmap.getHeight();
- final Bitmap outputBitmap = Bitmap.createBitmap(width, height, Config.ARGB_8888);
-
- final Path path = new Path();
- path.addCircle(
- (float) (width / 2),
- (float) (height / 2),
- (float) min(width, (height / 2)),
- Path.Direction.CCW);
-
- final Canvas canvas = new Canvas(outputBitmap);
- canvas.clipPath(path);
- canvas.drawBitmap(bitmap, 0, 0, null);
- return outputBitmap;
- }
-
- /** Creates a group avatar bitmap */
- @NonNull
- private static Bitmap createGroupAvatarBitmap(
- @NonNull List<Bitmap> participantsIcon, GroupAvatarConfigs groupAvatarConfigs) {
- int width = groupAvatarConfigs.mWidth;
- int height = groupAvatarConfigs.mHeight;
- Bitmap bitmap = createOrReuseBitmap(width, height, Color.TRANSPARENT);
- Canvas canvas = new Canvas(bitmap);
- RectF[] rect =
- generateDestRectArray(
- width,
- height,
- /* cropToCircle= */ true,
- min(participantsIcon.size(), groupAvatarConfigs.mMaximumGroupSize));
-
- for (int i = 0; i < rect.length; i++) {
- RectF avatarDestOnGroup = rect[i];
- // Draw the bitmap into a smaller size with a circle mask.
- Bitmap resourceBitmap = participantsIcon.get(i);
- RectF resourceRect =
- new RectF(
- /* left= */ 0,
- /* top= */ 0,
- resourceBitmap.getWidth(),
- resourceBitmap.getHeight());
- Bitmap smallCircleBitmap =
- createOrReuseBitmap(
- Math.round(avatarDestOnGroup.width()),
- Math.round(avatarDestOnGroup.height()),
- Color.TRANSPARENT);
- RectF smallCircleRect =
- new RectF(
- /* left= */ 0,
- /* top= */ 0,
- smallCircleBitmap.getWidth(),
- smallCircleBitmap.getHeight());
- Canvas smallCircleCanvas = new Canvas(smallCircleBitmap);
- drawBitmapWithCircleOnCanvas(
- resourceBitmap,
- smallCircleCanvas,
- resourceRect,
- smallCircleRect,
- groupAvatarConfigs.mFillBackground,
- groupAvatarConfigs.mBackgroundColor,
- groupAvatarConfigs.mStrokeColor);
- Matrix matrix = new Matrix();
- matrix.setRectToRect(smallCircleRect, avatarDestOnGroup, Matrix.ScaleToFit.FILL);
- canvas.drawBitmap(smallCircleBitmap, matrix, new Paint(Paint.ANTI_ALIAS_FLAG));
- }
-
- return bitmap;
- }
-
- /**
- * Given the source bitmap and a canvas, draws the bitmap through a circular mask. Only draws a
- * circle with diameter equal to the destination width.
- *
- * @param bitmap The source bitmap to draw.
- * @param canvas The canvas to draw it on.
- * @param source The source bound of the bitmap.
- * @param dest The destination bound on the canvas.
- * @param fillBackground when set, fill the circle with backgroundColor
- * @param strokeColor draw a border outside the circle with strokeColor
- */
- private static void drawBitmapWithCircleOnCanvas(
- @NonNull Bitmap bitmap,
- @NonNull Canvas canvas,
- @NonNull RectF source,
- @NonNull RectF dest,
- boolean fillBackground,
- int backgroundColor,
- int strokeColor) {
- // Draw bitmap through shader first.
- final BitmapShader shader = new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP);
- final Matrix matrix = new Matrix();
-
- // Fit bitmap to bounds.
- matrix.setRectToRect(source, dest, Matrix.ScaleToFit.CENTER);
-
- shader.setLocalMatrix(matrix);
- Paint bitmapPaint = new Paint();
-
- bitmapPaint.setAntiAlias(true);
- if (fillBackground) {
- bitmapPaint.setColor(backgroundColor);
- canvas.drawCircle(dest.centerX(), dest.centerX(), dest.width() / 2f, bitmapPaint);
- }
-
- bitmapPaint.setShader(shader);
- canvas.drawCircle(dest.centerX(), dest.centerX(), dest.width() / 2f, bitmapPaint);
- bitmapPaint.setShader(null);
-
- if (strokeColor != Color.TRANSPARENT) {
- final Paint stroke = new Paint();
- stroke.setAntiAlias(true);
- stroke.setColor(strokeColor);
- stroke.setStyle(Paint.Style.STROKE);
- final float strokeWidth = 6f;
- stroke.setStrokeWidth(strokeWidth);
- canvas.drawCircle(
- dest.centerX(),
- dest.centerX(),
- /* radius= */ dest.width() / 2f - stroke.getStrokeWidth() / 2f,
- stroke);
- }
- }
-
- @NonNull
- private static Bitmap createOrReuseBitmap(int width, int height, @ColorInt int background) {
- Bitmap bitmap =
- Bitmap.createBitmap(width, height, /* Bitmap.Config= */ Bitmap.Config.ARGB_8888);
- bitmap.eraseColor(background);
- return bitmap;
- }
-
- /**
- * Generates an array of {@link RectF} which represents where each of the individual avatar
- * should be located in the final group avatar image. The location of each avatar depends on the
- * size of the group and the size of the overall group avatar size. If we're cropping to a
- * circle, inset the rects so the circle surrounds all the mini-avatars.
- */
- public static RectF[] generateDestRectArray(
- int desiredWidth, int desiredHeight, boolean cropToCircle, int groupSize) {
- float halfWidth = desiredWidth / 2F;
- float halfHeight = desiredHeight / 2F;
-
- // If we're cropping to a circle, calculate an inset so that all the mini-avatars will fit
- // inside the circle.
- float inset =
- cropToCircle ? (float) ((Math.hypot(halfWidth, halfHeight) - halfWidth) / 2f) : 0F;
- RectF[] destArray = new RectF[groupSize];
- switch (groupSize) {
- case 2:
- /*
- * +-------+
- * | 0 | |
- * +-------+
- * | | 1 |
- * +-------+ *
- * We want two circles which touches in the center. To get this we know that
- * the diagonal
- * of the overall group avatar is squareRoot(2) * w We also know that the two
- * circles
- * touches the at the center of the overall group avatar and the distance from
- * the center of
- * the circle to the corner of the group avatar is radius * squareRoot(2).
- * Therefore, the
- * following emerges.
- *
- * w * squareRoot(2) = 2 (radius + radius * squareRoot(2)) Solving for radius
- * we get: d =
- * 2 * radius = ( squareRoot(2) / (squareRoot(2) + 1)) * w d = (2 - squareRoot(2)
- * ) * w
- */
- float diameter = (float) ((2 - Math.sqrt(2)) * ((float) desiredWidth - inset));
- destArray[0] = new RectF(inset, inset, diameter, diameter);
- destArray[1] =
- new RectF(
- /* left= */ (float) desiredWidth - diameter,
- /* top= */ (float) desiredHeight - diameter,
- /* right= */ (float) desiredWidth - inset,
- /* bottom= */ (float) desiredHeight - inset);
- break;
- case 3:
- /*
- * +-------+
- * | | 0 | |
- * +-------+
- * | 1 | 2 |
- * +-------+
- * i0
- * |\
- * a | \ c
- * --- i2
- * b
- *
- * a = radius * squareRoot(3) due to the triangle being a 30-60-90 right
- * triangle. b =
- * radius of circle c = 2 * radius of circle
- *
- * All three of the images are circles and therefore image zero will not touch
- * image one
- * or image two. Move image zero down so it touches image one and image two. This
- * can be
- * done by keeping image zero in the center and moving it down slightly. The
- * amount to move
- * down can be calculated by solving a right triangle. We know that the center x
- * of image
- * two to the center x of image zero is the radius of the circle, this is the
- * length of edge
- * b. Also we know that the distance from image zero to image two's center is 2 *
- * radius,
- * edge c. From this we know that the distance from center y of image two to
- * center y of
- * image one, edge a, is equal to radius * squareRoot(3) due to this triangle
- * being a
- * 30-60-90 right triangle.
- */
- float quarterWidth = (float) desiredWidth / 4F;
- float threeQuarterWidth = 3 * quarterWidth;
- float radius = cropToCircle ? (halfHeight - inset) / 2 : (float) desiredHeight / 4F;
- float imageTwoCenterY = (float) desiredHeight - radius;
- float lengthOfEdgeA = (float) (radius * Math.sqrt(3));
- float imageZeroCenterY = imageTwoCenterY - lengthOfEdgeA;
- float imageZeroTop = imageZeroCenterY - radius - 2 * inset;
- float imageZeroBottom = imageZeroCenterY + radius - 2 * inset;
- destArray[0] =
- new RectF(
- quarterWidth, imageZeroTop,
- threeQuarterWidth, imageZeroBottom);
- destArray[1] =
- new RectF(
- inset,
- /* top= */ halfHeight - inset,
- halfWidth,
- /* bottom= */ (float) desiredHeight - 2 * inset);
- destArray[2] =
- new RectF(
- halfWidth,
- /* top= */ halfHeight - inset,
- /* right= */ (float) desiredWidth - inset,
- /* bottom= */ (float) desiredHeight - 2 * inset);
- break;
- default:
- /*
- * +-------+
- * | 0 | 1 |
- * +-------+
- * | 2 | 3 |
- * +-------+
- */
- destArray[0] = new RectF(inset, inset, halfWidth, halfHeight);
- destArray[1] =
- new RectF(
- halfWidth,
- inset,
- /* right= */ (float) desiredWidth - inset,
- halfHeight);
- destArray[2] =
- new RectF(
- inset,
- halfHeight,
- halfWidth,
- /* bottom= */ (float) desiredHeight - inset);
- destArray[3] =
- new RectF(
- halfWidth,
- halfHeight,
- /* right= */ (float) desiredWidth - inset,
- /* bottom= */ (float) desiredHeight - inset);
- break;
- }
- return destArray;
- }
-}
diff --git a/src/com/android/car/messenger/impl/datamodels/util/ContactUtils.java b/src/com/android/car/messenger/impl/datamodels/util/ContactUtils.java
deleted file mode 100644
index 26f1906..0000000
--- a/src/com/android/car/messenger/impl/datamodels/util/ContactUtils.java
+++ /dev/null
@@ -1,176 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.car.messenger.impl.datamodels.util;
-
-import static android.provider.BaseColumns._ID;
-import static android.provider.ContactsContract.PhoneLookup.CONTENT_FILTER_URI;
-import static android.provider.Telephony.ThreadsColumns.RECIPIENT_IDS;
-
-import android.content.ContentUris;
-import android.content.Context;
-import android.content.res.AssetFileDescriptor;
-import android.database.Cursor;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.net.Uri;
-import android.provider.ContactsContract;
-import android.provider.Telephony.MmsSms;
-import android.text.TextUtils;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.core.app.Person;
-
-import com.android.car.messenger.core.interfaces.AppFactory;
-import com.android.car.messenger.core.util.L;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.function.BiConsumer;
-
-/** Contact Utils for getting information on a contact */
-public class ContactUtils {
- @NonNull
- private static final Uri SINGLE_CANONICAL_ADDRESS_URI =
- MmsSms.CONTENT_URI.buildUpon().appendPath("canonical-address").build();
-
- @NonNull private static final String RECIPIENT_SPLIT_SEPARATOR = " ";
- @NonNull public static final String DRIVER_NAME = "Driver";
-
- @NonNull
- private static final String[] PROJECTION =
- new String[] {
- ContactsContract.PhoneLookup.DISPLAY_NAME,
- ContactsContract.PhoneLookup.CONTACT_ID,
- _ID,
- ContactsContract.PhoneLookup.PHOTO_ID,
- ContactsContract.PhoneLookup.PHOTO_THUMBNAIL_URI,
- ContactsContract.PhoneLookup.PHOTO_FILE_ID,
- ContactsContract.PhoneLookup.PHOTO_URI
- };
-
- private ContactUtils() {}
- /**
- * Get the list of recipients as {@link Person} for the given conversation id
- *
- * @param conversationId The conversation id to retrieve the list of participants
- * @param processParticipant A nullable method to further process an individual participant
- */
- public static List<Person> getRecipients(
- @NonNull String conversationId,
- @Nullable BiConsumer<String, Bitmap> processParticipant) {
- String[] recipientIds = getRecipientIds(conversationId);
- List<Person> participants = new ArrayList<>();
- Context context = AppFactory.get().getContext();
- for (String contactId : recipientIds) {
- long contactIdLong = Long.parseLong(contactId);
- String number = getCanonicalAddressesFromRecipientIds(context, contactIdLong);
- if (number == null) {
- L.e("No phone number found for contactId: " + contactId);
- continue;
- }
- Person person = getPerson(context, number, processParticipant);
- participants.add(person);
- }
-
- return participants;
- }
-
- private static String[] getRecipientIds(@NonNull String conversationId) {
- Cursor threadCursor = CursorUtils.getThreadCursor(conversationId);
- threadCursor.moveToFirst();
- return threadCursor
- .getString(threadCursor.getColumnIndex(RECIPIENT_IDS))
- .split(RECIPIENT_SPLIT_SEPARATOR);
- }
-
- /**
- * Get Profile information for the contact, including the contact name and the contact avatar if
- * available
- */
- @NonNull
- static Person getPerson(
- @NonNull Context context,
- @NonNull String phoneNo,
- @Nullable BiConsumer<String, Bitmap> processParticipant) {
- String name = phoneNo;
- Bitmap bitmap = null;
-
- Uri uri = CONTENT_FILTER_URI.buildUpon().appendEncodedPath(Uri.encode(phoneNo)).build();
- Cursor cursor = CursorUtils.simpleQueryWithProjection(context, uri, PROJECTION);
- if (cursor != null && cursor.moveToFirst()) {
- name =
- cursor.getString(
- cursor.getColumnIndex(ContactsContract.PhoneLookup.DISPLAY_NAME));
- String thumbnailPath =
- cursor.getString(cursor.getColumnIndex(ContactsContract.PhoneLookup.PHOTO_URI));
-
- if (thumbnailPath != null && processParticipant != null) {
- try {
- Uri thumbnailUri = Uri.parse(thumbnailPath);
- AssetFileDescriptor fd =
- context.getContentResolver().openAssetFileDescriptor(thumbnailUri, "r");
- if (fd != null) {
- InputStream stream = fd.createInputStream();
- bitmap = BitmapFactory.decodeStream(stream);
- fd.close();
- }
- } catch (IOException e) {
- L.e(e.toString());
- }
- }
- }
-
- if (cursor != null && !cursor.isClosed()) {
- cursor.close();
- }
-
- // don't include icon when building out the Person class in order
- // to reduce the size of individual messages, instead pass it to the caller
- // to build out avatar for the entire conversation
- if (processParticipant != null) {
- bitmap = AvatarUtil.resolvePersonAvatar(context, bitmap, name);
- processParticipant.accept(name, bitmap);
- }
-
- return new Person.Builder().setUri(phoneNo).setName(name).build();
- }
-
- @Nullable
- private static String getCanonicalAddressesFromRecipientIds(
- @NonNull Context context, long contactId) {
- Cursor cursor =
- CursorUtils.simpleQuery(
- context,
- ContentUris.withAppendedId(SINGLE_CANONICAL_ADDRESS_URI, contactId));
- if (cursor != null) {
- try {
- if (cursor.moveToFirst()) {
- String rawNumber = cursor.getString(0);
- if (!TextUtils.isEmpty(rawNumber)) {
- return rawNumber;
- }
- }
- } finally {
- cursor.close();
- }
- }
- L.d("No canonical address found for recipient id");
- return null;
- }
-}
diff --git a/src/com/android/car/messenger/impl/datamodels/util/CursorUtils.java b/src/com/android/car/messenger/impl/datamodels/util/CursorUtils.java
deleted file mode 100644
index 0292ebe..0000000
--- a/src/com/android/car/messenger/impl/datamodels/util/CursorUtils.java
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.car.messenger.impl.datamodels.util;
-
-import static android.provider.BaseColumns._ID;
-import static android.provider.Telephony.BaseMmsColumns.CONTENT_TYPE;
-import static android.provider.Telephony.MmsSms.CONTENT_CONVERSATIONS_URI;
-import static android.provider.Telephony.TextBasedSmsColumns.ADDRESS;
-import static android.provider.Telephony.TextBasedSmsColumns.BODY;
-import static android.provider.Telephony.TextBasedSmsColumns.SUBSCRIPTION_ID;
-import static android.provider.Telephony.TextBasedSmsColumns.THREAD_ID;
-import static android.provider.Telephony.TextBasedSmsColumns.TYPE;
-import static android.provider.Telephony.ThreadsColumns.DATE;
-import static android.provider.Telephony.ThreadsColumns.READ;
-import static android.provider.Telephony.ThreadsColumns.RECIPIENT_IDS;
-
-import android.content.ContentResolver;
-import android.content.Context;
-import android.database.Cursor;
-import android.net.Uri;
-import android.provider.Telephony;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.car.messenger.core.interfaces.AppFactory;
-
-/** Cursor Utils to get quick cursor or uri telephony information */
-public class CursorUtils {
- private CursorUtils() {}
- // This URI provides all the metadata for a thread.
- // Appending simple=true is important to get the recipient_ids for the conversation.
- @NonNull
- public static final Uri THREAD_INFO_URI =
- CONTENT_CONVERSATIONS_URI.buildUpon().appendQueryParameter("simple", "true").build();
-
- @NonNull protected static final String[] THREAD_INFO_PROJECTION = {_ID, RECIPIENT_IDS, READ};
-
- @NonNull
- protected static final String[] CONTENT_CONVERSATION_PROJECTION = {
- _ID, TYPE, DATE, READ, CONTENT_TYPE, BODY, ADDRESS, THREAD_ID, SUBSCRIPTION_ID
- };
-
- @NonNull
- private static final String DEFAULT_SORT_ORDER = Telephony.TextBasedSmsColumns.DATE + " DESC";
-
- /**
- * Get simplified thread cursor with metadata information on the thread, such as recipient ids
- */
- @Nullable
- public static Cursor getThreadCursor(@NonNull String threadId) {
- Context context = AppFactory.get().getContext();
- ContentResolver contentResolver = context.getContentResolver();
- return contentResolver.query(
- THREAD_INFO_URI,
- THREAD_INFO_PROJECTION,
- _ID + "=" + threadId,
- /* selectionArgs= */ null,
- DEFAULT_SORT_ORDER);
- }
-
- /**
- * Get the message cursor in descending order for
- *
- * @param conversationId The conversation or thread id for the conversation
- * @param limit The maximum number of message rows to fetch
- * @param offset The starting point in timestamp in millisecond to fetch for data
- */
- @Nullable
- public static Cursor getMessagesCursor(@NonNull String conversationId, int limit, long offset) {
- Context context = AppFactory.get().getContext();
- ContentResolver contentResolver = context.getContentResolver();
- return contentResolver.query(
- getConversationUri(conversationId),
- CONTENT_CONVERSATION_PROJECTION,
- DATE + " > " + offset,
- /* selectionArgs= */ null,
- DEFAULT_SORT_ORDER + " LIMIT " + limit);
- }
-
- /** Gets the Conversation Uri for the Conversation with specified conversationId */
- @NonNull
- public static Uri getConversationUri(@NonNull String conversationId) {
- return CONTENT_CONVERSATIONS_URI.buildUpon().appendPath(conversationId).build();
- }
-
- /** Returns a cursor query with the uri provided, with no filtering or projection */
- @Nullable
- public static Cursor simpleQuery(@NonNull Context context, @NonNull Uri uri) {
- return context.getContentResolver().query(uri, null, null, null, null);
- }
-
- /** Returns a cursor query given a uri and projection */
- @Nullable
- public static Cursor simpleQueryWithProjection(
- @NonNull Context context, @NonNull Uri uri, @Nullable String[] projection) {
- return context.getContentResolver().query(uri, projection, null, null, null);
- }
-
- /** Returns a cursor query given a uri and selection */
- @Nullable
- public static Cursor simpleQueryWithSelection(
- @NonNull Context context, @NonNull Uri uri, @Nullable String selection) {
- return context.getContentResolver().query(uri, null, selection, null, null);
- }
-}
diff --git a/src/com/android/car/messenger/impl/datamodels/util/MessageUtils.java b/src/com/android/car/messenger/impl/datamodels/util/MessageUtils.java
deleted file mode 100644
index 3aa4c86..0000000
--- a/src/com/android/car/messenger/impl/datamodels/util/MessageUtils.java
+++ /dev/null
@@ -1,185 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.messenger.impl.datamodels.util;
-
-import static com.android.car.messenger.common.Conversation.Message.MessageStatus.MESSAGE_STATUS_NONE;
-import static com.android.car.messenger.common.Conversation.Message.MessageStatus.MESSAGE_STATUS_READ;
-import static com.android.car.messenger.common.Conversation.Message.MessageStatus.MESSAGE_STATUS_UNREAD;
-
-import static java.util.Comparator.comparingLong;
-
-import android.content.Context;
-import android.database.Cursor;
-import android.provider.Telephony.TextBasedSmsColumns;
-import android.util.Pair;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.core.app.Person;
-
-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.common.Conversation.Message.MessageType;
-import com.android.car.messenger.core.interfaces.AppFactory;
-import com.android.car.messenger.core.util.L;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.Function;
-
-/** Message Parser that provides useful static methods to parse 1-1 and Group MMS messages. */
-public final class MessageUtils {
-
- /**
- * Gets all unread messages in cursor
- *
- * @param messagesCursor The messageCursor in descending order
- */
- @NonNull
- public static List<Message> getUnreadMessages(@Nullable Cursor messagesCursor) {
- List<Message> unreadMessages = new ArrayList<>();
- MessageUtils.forEachDesc(
- messagesCursor,
- message -> {
- if (message.getMessageStatus() == MessageStatus.MESSAGE_STATUS_UNREAD) {
- unreadMessages.add(message);
- return true;
- }
- return false;
- });
- unreadMessages.sort(comparingLong(Message::getTimestamp));
- return unreadMessages;
- }
-
- /**
- * Gets Read Messages and Reply Timestamp.
- *
- * @param messagesCursor MessageCursor in descending order
- */
- @NonNull
- public static Pair<List<Message>, Long> getReadMessagesAndReplyTimestamp(
- @Nullable Cursor messagesCursor) {
- List<Message> readMessages = new ArrayList<>();
- AtomicReference<Long> lastReply = new AtomicReference<>(0L);
- MessageUtils.forEachDesc(
- messagesCursor,
- message -> {
- // Desired impact: 4. Reply -> 3. Messages -> 2. Reply -> 1 Messages (stop
- // parsing at 2.)
- // lastReply references 4., messages references 3.
- // Desired impact: 3. Messages -> 2. Reply -> 1. Messages (stop parsing at 2.)
- // lastReply references 2., messages references 3.
- int messageStatus = message.getMessageStatus();
- if (message.getMessageType() == MessageType.MESSAGE_TYPE_SENT) {
- lastReply.set(message.getTimestamp());
- return readMessages.isEmpty();
- } else {
- if (messageStatus == MessageStatus.MESSAGE_STATUS_READ
- || messageStatus == MessageStatus.MESSAGE_STATUS_NONE) {
- readMessages.add(message);
- return true;
- }
- }
- return false;
- });
- readMessages.sort(comparingLong(Message::getTimestamp));
- return new Pair<>(readMessages, lastReply.get());
- }
-
- /**
- * Parses each message in the cursor and returns the item for further processing
- *
- * @param messageCursor The message cursor to be parsed for SMS and MMS messages
- * @param processor A consumer that takes in the {@link Message} and returns true for the method
- * to continue parsing the cursor or false to return.
- */
- public static void forEachDesc(
- @Nullable Cursor messageCursor, @NonNull Function<Message, Boolean> processor) {
- if (messageCursor == null || !messageCursor.moveToFirst()) {
- return;
- }
- Context context = AppFactory.get().getContext();
- boolean moveToNext = true;
- boolean hasBeenRepliedTo = false;
- do {
- Message message;
- try {
- message = parseMessageAtPoint(context, messageCursor, hasBeenRepliedTo);
- } catch (IllegalArgumentException e) {
- e.printStackTrace();
- L.d("Message was not able to be parsed. Skipping.");
- continue;
- }
- if (message.getMessageType() == MessageType.MESSAGE_TYPE_SENT) {
- hasBeenRepliedTo = true;
- }
- moveToNext = processor.apply(message);
- } while (messageCursor.moveToNext() && moveToNext);
- }
-
- /**
- * Parses each message in the cursor and returns the item for further processing
- *
- * @param messageCursor The message cursor to be parsed for SMS and MMS messages and returns
- * true for the method to continue parsing the cursor or false to return.
- */
- @Nullable
- public static Message parseCurrentMessage(@NonNull Cursor messageCursor) {
- Message message = null;
- Context context = AppFactory.get().getContext();
- try {
- message = parseMessageAtPoint(context, messageCursor, false);
- } catch (IllegalArgumentException e) {
- e.printStackTrace();
- L.d("Message was not able to be parsed. Skipping.");
- }
- return message;
- }
-
- /**
- * Parses message at the point in cursor.
- *
- * @throws IllegalArgumentException if desired columns are missing.
- * @see CursorUtils#CONTENT_CONVERSATION_PROJECTION
- */
- @NonNull
- private static Conversation.Message parseMessageAtPoint(
- @NonNull Context context, @NonNull Cursor cursor, boolean userHasReplied) {
- MmsSmsMessage msg =
- MmsUtils.isMms(cursor)
- ? MmsUtils.parseMms(context, cursor)
- : SmsUtils.parseSms(cursor);
- Person person =
- ContactUtils.getPerson(context, msg.mPhoneNumber, /* processParticipant= */ null);
- Conversation.Message message =
- new Conversation.Message(msg.mBody, msg.mDate.toEpochMilli(), person);
- if (msg.mType == TextBasedSmsColumns.MESSAGE_TYPE_SENT) {
- message.setMessageType(MessageType.MESSAGE_TYPE_SENT);
- message.setMessageStatus(MESSAGE_STATUS_NONE);
- } else {
- int status =
- (msg.mRead || userHasReplied) ? MESSAGE_STATUS_READ : MESSAGE_STATUS_UNREAD;
- message.setMessageType(MessageType.MESSAGE_TYPE_INBOX);
- message.setMessageStatus(status);
- }
- return message;
- }
-
- private MessageUtils() {}
-}
diff --git a/src/com/android/car/messenger/impl/datamodels/util/MmsSmsMessage.java b/src/com/android/car/messenger/impl/datamodels/util/MmsSmsMessage.java
deleted file mode 100644
index cb76308..0000000
--- a/src/com/android/car/messenger/impl/datamodels/util/MmsSmsMessage.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.messenger.impl.datamodels.util;
-
-import java.time.Instant;
-
-/** MmsSmsMessage to hold metadata common to MMS and SMS Messages */
-class MmsSmsMessage {
- protected MmsSmsMessage() {}
-
- String mId;
- long mThreadId;
- String mPhoneNumber;
- String mBody;
- int mType;
- int mSubscriptionId;
- Instant mDate;
- boolean mRead;
-}
diff --git a/src/com/android/car/messenger/impl/datamodels/util/MmsUtils.java b/src/com/android/car/messenger/impl/datamodels/util/MmsUtils.java
deleted file mode 100644
index 4513f78..0000000
--- a/src/com/android/car/messenger/impl/datamodels/util/MmsUtils.java
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.messenger.impl.datamodels.util;
-
-import static android.provider.BaseColumns._ID;
-import static android.provider.Telephony.BaseMmsColumns.CONTENT_TYPE;
-import static android.provider.Telephony.TextBasedSmsColumns.TYPE;
-
-import android.content.Context;
-import android.database.Cursor;
-import android.net.Uri;
-import android.provider.Telephony.Mms.Addr;
-import android.provider.Telephony.Mms.Part;
-import android.provider.Telephony.Sms;
-
-import androidx.annotation.NonNull;
-
-import java.text.MessageFormat;
-import java.time.Instant;
-
-/** MMS Utils for parsing MMS Telephony Content */
-class MmsUtils {
-
- @NonNull public static final String FORMAT_CONTENT_MMS_PART = "content://mms/{0}/part";
- @NonNull public static final String FORMAT_CONTENT_MMS_ADDR = "content://mms/{0}/addr";
- @NonNull public static final String FORMAT_TYPE_AND_MSG_ID = "type={0} AND msg_id={1}";
-
- private MmsUtils() {}
-
- @NonNull static final String MMS_CONTENT_TYPE = "application/vnd.wap.multipart.related";
- private static final int ORIGINATOR_ADDRESS_TYPE = 137;
-
- /** Returns true, if item on cursor position is an MMS message */
- static Boolean isMms(@NonNull Cursor cursor) {
- String contentType = cursor.getString(cursor.getColumnIndex(CONTENT_TYPE));
- return MMS_CONTENT_TYPE.equals(contentType);
- }
-
- /**
- * Returns the parsed result as {link @MmsSmsMessage}
- *
- * @throws IllegalArgumentException if desired columns are missing.
- * @see CursorUtils#CONTENT_CONVERSATION_PROJECTION
- */
- @NonNull
- static MmsSmsMessage parseMms(@NonNull Context context, @NonNull Cursor cursor) {
- MmsSmsMessage message = new MmsSmsMessage();
- message.mId = cursor.getString(cursor.getColumnIndex(_ID));
- message.mThreadId = cursor.getInt(cursor.getColumnIndex(Sms.THREAD_ID));
- message.mType = cursor.getInt(cursor.getColumnIndex(TYPE));
- message.mSubscriptionId = cursor.getInt(cursor.getColumnIndex(Sms.SUBSCRIPTION_ID));
- message.mDate = Instant.ofEpochSecond(cursor.getLong(cursor.getColumnIndex(Sms.DATE)));
- message.mRead = cursor.getInt(cursor.getColumnIndex(Sms.READ)) == 1;
- message.mPhoneNumber = getOriginator(context, message.mId);
- message.mBody = getMmsBody(context, message.mId);
- return message;
- }
-
- private static String getMmsBody(@NonNull Context context, @NonNull String id) {
- String uriStr = MessageFormat.format(FORMAT_CONTENT_MMS_PART, id);
- Uri uriAddress = Uri.parse(uriStr);
- Cursor cursor = CursorUtils.simpleQuery(context, uriAddress);
- StringBuilder stringBuilder = new StringBuilder();
- while (cursor != null && cursor.moveToNext()) {
- stringBuilder.append(cursor.getString(cursor.getColumnIndex(Part.TEXT)));
- stringBuilder.append(" ");
- }
- return stringBuilder.toString();
- }
-
- @NonNull
- private static String getOriginator(@NonNull Context context, @NonNull String id) {
- String selection =
- MessageFormat.format(FORMAT_TYPE_AND_MSG_ID, ORIGINATOR_ADDRESS_TYPE, id);
- String uriStr = MessageFormat.format(FORMAT_CONTENT_MMS_ADDR, id);
- Cursor cursor = CursorUtils.simpleQueryWithSelection(context, Uri.parse(uriStr), selection);
- String phoneNum = "";
- if (cursor != null && cursor.moveToFirst()) {
- cursor.moveToFirst();
- phoneNum = cursor.getString(cursor.getColumnIndex(Addr.ADDRESS));
- cursor.close();
- }
- return phoneNum;
- }
-}
diff --git a/src/com/android/car/messenger/impl/datamodels/util/SmsUtils.java b/src/com/android/car/messenger/impl/datamodels/util/SmsUtils.java
deleted file mode 100644
index c7bd3dd..0000000
--- a/src/com/android/car/messenger/impl/datamodels/util/SmsUtils.java
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.messenger.impl.datamodels.util;
-
-import static android.provider.BaseColumns._ID;
-
-import android.database.Cursor;
-import android.provider.Telephony.Sms;
-
-import androidx.annotation.NonNull;
-
-import java.time.Instant;
-
-/** SMS Utils for parsing SMS Telephony Content */
-class SmsUtils {
-
- SmsUtils() {}
-
- /**
- * Returns the parsed sms result as a {@link MmsSmsMessage}
- *
- * @throws IllegalArgumentException if desired columns are missing.
- * @see CursorUtils#CONTENT_CONVERSATION_PROJECTION
- */
- @NonNull
- static MmsSmsMessage parseSms(@NonNull Cursor cursor) {
- int threadIdIndex = cursor.getColumnIndex(Sms.THREAD_ID);
- int recipientsIndex = cursor.getColumnIndex(Sms.ADDRESS);
- int bodyIndex = cursor.getColumnIndex(Sms.BODY);
- int subscriptionIdIndex = cursor.getColumnIndex(Sms.SUBSCRIPTION_ID);
- int dateIndex = cursor.getColumnIndex(Sms.DATE);
- int typeIndex = cursor.getColumnIndex(Sms.TYPE);
- int readIndex = cursor.getColumnIndex(Sms.READ);
-
- MmsSmsMessage message = new MmsSmsMessage();
- message.mThreadId = cursor.getInt(threadIdIndex);
- message.mPhoneNumber = cursor.getString(recipientsIndex);
- message.mBody = cursor.getString(bodyIndex);
- message.mSubscriptionId = cursor.getInt(subscriptionIdIndex);
- message.mType = cursor.getInt(typeIndex);
- message.mDate = Instant.ofEpochMilli(cursor.getLong(dateIndex));
- message.mRead = cursor.getInt(readIndex) == 1;
- message.mId = cursor.getString(cursor.getColumnIndex(_ID));
- return message;
- }
-}
diff --git a/src/com/android/car/messenger/log/L.java b/src/com/android/car/messenger/log/L.java
new file mode 100644
index 0000000..3d42c28
--- /dev/null
+++ b/src/com/android/car/messenger/log/L.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.messenger.log;
+
+import android.os.Build;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Util class for logging.
+ */
+public class L {
+
+ /**
+ * Logs verbose level logs if loggable.
+ *
+ * @param tag logging tag
+ * @param msg the message to log, as a format string
+ * @param args arguments referenced by the format string
+ */
+ public static void v(String tag, @NonNull String msg, Object... args) {
+ if (Log.isLoggable(tag, Log.VERBOSE) || Build.IS_DEBUGGABLE) {
+ Log.v(tag, String.format(msg, args));
+ }
+ }
+
+ /**
+ * Logs debug level logs if loggable.
+ *
+ * @param tag logging tag
+ * @param msg the message to log, as a format string
+ * @param args arguments referenced by the format string
+ */
+ public static void d(String tag, @NonNull String msg, Object... args) {
+ if (Log.isLoggable(tag, Log.DEBUG) || Build.IS_DEBUGGABLE) {
+ Log.d(tag, String.format(msg, args));
+ }
+ }
+
+ /**
+ * Logs info level logs if loggable.
+ *
+ * @param tag logging tag
+ * @param msg the message to log, as a format string
+ * @param args arguments referenced by the format string
+ */
+ public static void i(String tag, @NonNull String msg, Object... args) {
+ if (Log.isLoggable(tag, Log.INFO) || Build.IS_DEBUGGABLE) {
+ Log.i(tag, String.format(msg, args));
+ }
+ }
+
+ /**
+ * Logs warning level logs if loggable.
+ *
+ * @param tag logging tag
+ * @param msg the message to log, as a format string
+ * @param args arguments referenced by the format string
+ */
+ public static void w(String tag, @NonNull String msg, Object... args) {
+ if (Log.isLoggable(tag, Log.WARN) || Build.IS_DEBUGGABLE) {
+ Log.w(tag, String.format(msg, args));
+ }
+ }
+
+ /**
+ * Logs error level logs.
+ *
+ * @param tag logging tag
+ * @param msg the message to log, as a format string
+ * @param args arguments referenced by the format string
+ */
+ public static void e(String tag, @NonNull String msg, Object... args) {
+ Log.e(tag, String.format(msg, args));
+ }
+
+ /**
+ * Logs warning level logs.
+ *
+ * @param tag logging tag
+ * @param e an exception to log
+ * @param msg the message to log, as a format string
+ * @param args arguments referenced by the format string
+ */
+ public static void e(String tag, Exception e, @NonNull String msg, Object... args) {
+ Log.e(tag, String.format(msg, args), e);
+ }
+
+ /**
+ * Logs conditions that should never happen.
+ *
+ * @param tag logging tag
+ * @param msg the message to log, as a format string
+ * @param args arguments referenced by the format string
+ */
+ public static void wtf(String tag, @NonNull String msg, Object... args) {
+ Log.wtf(tag, String.format(msg, args));
+ }
+
+ /**
+ * Logs conditions that should never happen.
+ *
+ * @param tag logging tag
+ * @param e an exception to log
+ * @param msg the message to log, as a format string
+ * @param args arguments referenced by the format string
+ */
+ public static void wtf(String tag, Exception e, @NonNull String msg, Object... args) {
+ Log.wtf(tag, String.format(msg, args), e);
+ }
+}
diff --git a/tests/robotests/Android.bp b/tests/robotests/Android.bp
new file mode 100644
index 0000000..342dfe6
--- /dev/null
+++ b/tests/robotests/Android.bp
@@ -0,0 +1,22 @@
+//############################################################
+// Car Messenger Robolectric test target. #
+//############################################################
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_robolectric_test {
+ name: "CarMessengerRoboTests",
+
+ srcs: ["src/**/*.java"],
+
+ java_resource_dirs: ["config"],
+
+ // Include the testing libraries
+ libs: [
+ "testng",
+ ],
+
+ instrumentation_for: "CarMessengerApp",
+}
diff --git a/res/layout/list_fragment.xml b/tests/robotests/AndroidManifest.xml
index 5dfe1e7..152fc5d 100644
--- a/res/layout/list_fragment.xml
+++ b/tests/robotests/AndroidManifest.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- Copyright (C) 2020 The Android Open Source Project
+ Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,9 +14,10 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<com.android.car.ui.recyclerview.CarUiRecyclerView
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/list_view"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:clipToPadding="false" />
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ coreApp="true"
+ package="com.android.car.messenger.robotests">
+ <application/>
+
+</manifest>
diff --git a/tests/robotests/config/robolectric.properties b/tests/robotests/config/robolectric.properties
new file mode 100644
index 0000000..fab7251
--- /dev/null
+++ b/tests/robotests/config/robolectric.properties
@@ -0,0 +1 @@
+sdk=NEWEST_SDK
diff --git a/tests/robotests/readme.md b/tests/robotests/readme.md
new file mode 100644
index 0000000..84f52b5
--- /dev/null
+++ b/tests/robotests/readme.md
@@ -0,0 +1,6 @@
+Unit test suite for CarMessengerApp using Robolectric.
+
+```
+$ croot
+$ make RunCarMessengerRoboTests -j96
+``` \ No newline at end of file
diff --git a/tests/robotests/src/com/android/car/messenger/MessengerDelegateTest.java b/tests/robotests/src/com/android/car/messenger/MessengerDelegateTest.java
new file mode 100644
index 0000000..8068fb9
--- /dev/null
+++ b/tests/robotests/src/com/android/car/messenger/MessengerDelegateTest.java
@@ -0,0 +1,168 @@
+package com.android.car.messenger;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.when;
+
+import android.app.AppOpsManager;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothMapClient;
+import android.content.Context;
+import android.content.Intent;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.Shadows;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.ShadowBluetoothAdapter;
+
+import java.util.Arrays;
+import java.util.HashSet;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = {ShadowBluetoothAdapter.class})
+public class MessengerDelegateTest {
+
+ private static final String BLUETOOTH_ADDRESS_ONE = "FA:F8:14:CA:32:39";
+ private static final String BLUETOOTH_ADDRESS_TWO = "FA:F8:33:44:32:39";
+
+ @Mock
+ private BluetoothDevice mMockBluetoothDeviceOne;
+ @Mock
+ private BluetoothDevice mMockBluetoothDeviceTwo;
+ @Mock
+ AppOpsManager mMockAppOpsManager;
+
+ private Context mContext = RuntimeEnvironment.application;
+ private MessageNotificationDelegate mMessengerDelegate;
+ private ShadowBluetoothAdapter mShadowBluetoothAdapter;
+ private Intent mMessageOneIntent;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ // Add AppOps permissions required to write to Telephony.SMS database.
+ when(mMockAppOpsManager.checkOpNoThrow(anyInt(), anyInt(), anyString())).thenReturn(
+ AppOpsManager.MODE_DEFAULT);
+ Shadows.shadowOf(RuntimeEnvironment.application)
+ .setSystemService(Context.APP_OPS_SERVICE, mMockAppOpsManager);
+
+ when(mMockBluetoothDeviceOne.getAddress()).thenReturn(BLUETOOTH_ADDRESS_ONE);
+ when(mMockBluetoothDeviceTwo.getAddress()).thenReturn(BLUETOOTH_ADDRESS_TWO);
+ mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
+
+ createMockMessages();
+ mMessengerDelegate = new MessageNotificationDelegate(mContext);
+ mMessengerDelegate.onDeviceConnected(mMockBluetoothDeviceOne);
+ }
+
+ @Test
+ public void testDeviceConnections() {
+ assertThat(mMessengerDelegate.mBtDeviceAddressToConnectionTimestamp).containsKey(
+ BLUETOOTH_ADDRESS_ONE);
+ assertThat(mMessengerDelegate.mBtDeviceAddressToConnectionTimestamp).hasSize(1);
+
+ mMessengerDelegate.onDeviceConnected(mMockBluetoothDeviceTwo);
+ assertThat(mMessengerDelegate.mBtDeviceAddressToConnectionTimestamp).containsKey(
+ BLUETOOTH_ADDRESS_TWO);
+ assertThat(mMessengerDelegate.mBtDeviceAddressToConnectionTimestamp).hasSize(2);
+
+ mMessengerDelegate.onDeviceConnected(mMockBluetoothDeviceOne);
+ assertThat(mMessengerDelegate.mBtDeviceAddressToConnectionTimestamp).hasSize(2);
+ }
+
+ @Test
+ public void testDeviceConnection_hasCorrectTimestamp() {
+ long timestamp = System.currentTimeMillis();
+ mMessengerDelegate.onDeviceConnected(mMockBluetoothDeviceTwo);
+
+ long deviceConnectionTimestamp =
+ mMessengerDelegate.mBtDeviceAddressToConnectionTimestamp.get(BLUETOOTH_ADDRESS_TWO);
+
+ // Sometimes there is slight flakiness in the timestamps.
+ assertThat(deviceConnectionTimestamp-timestamp).isLessThan(5L);
+ }
+
+ @Test
+ public void testOnDeviceDisconnected_notConnectedDevice() {
+ mMessengerDelegate.onDeviceDisconnected(mMockBluetoothDeviceTwo);
+
+ assertThat(mMessengerDelegate.mBtDeviceAddressToConnectionTimestamp).containsKey(
+ BLUETOOTH_ADDRESS_ONE);
+ assertThat(mMessengerDelegate.mBtDeviceAddressToConnectionTimestamp).hasSize(1);
+ }
+
+ @Test
+ public void testOnDeviceDisconnected_connectedDevice() {
+ mMessengerDelegate.onDeviceConnected(mMockBluetoothDeviceTwo);
+
+ mMessengerDelegate.onDeviceDisconnected(mMockBluetoothDeviceOne);
+
+ assertThat(mMessengerDelegate.mBtDeviceAddressToConnectionTimestamp).containsKey(
+ BLUETOOTH_ADDRESS_TWO);
+ assertThat(mMessengerDelegate.mBtDeviceAddressToConnectionTimestamp).hasSize(1);
+ }
+
+ @Test
+ public void testOnDeviceDisconnected_connectedDevice_withMessages() {
+ // Disconnect a connected device, and ensure its messages are removed.
+ mMessengerDelegate.onMessageReceived(mMessageOneIntent);
+ mMessengerDelegate.onDeviceDisconnected(mMockBluetoothDeviceOne);
+
+ assertThat(mMessengerDelegate.mBtDeviceAddressToConnectionTimestamp).isEmpty();
+ assertThat(mMessengerDelegate.mGeneratedGroupConversationTitles).isEmpty();
+ assertThat(mMessengerDelegate.mSenderToLargeIconBitmap).isEmpty();
+ assertThat(mMessengerDelegate.mUriToSenderNameMap).isEmpty();
+ }
+
+ @Test
+ public void testOnDeviceDisconnected_notConnectedDevice_withMessagesFromConnectedDevice() {
+ // Disconnect a not connected device, and ensure device one's messages are still saved.
+ mMessengerDelegate.onMessageReceived(mMessageOneIntent);
+ mMessengerDelegate.onDeviceDisconnected(mMockBluetoothDeviceTwo);
+
+ assertThat(mMessengerDelegate.mBtDeviceAddressToConnectionTimestamp).hasSize(1);
+ assertThat(mMessengerDelegate.mUriToSenderNameMap).hasSize(1);
+ }
+
+ @Test
+ public void testConnectedDevices_areNotAddedFromBTAdapterBondedDevices() {
+ mShadowBluetoothAdapter.setBondedDevices(
+ new HashSet<>(Arrays.asList(mMockBluetoothDeviceTwo)));
+ mMessengerDelegate = new MessageNotificationDelegate(mContext);
+
+ assertThat(mMessengerDelegate.mBtDeviceAddressToConnectionTimestamp).isEmpty();
+ }
+
+ private Intent createMessageIntent(BluetoothDevice device, String handle, String senderUri,
+ String senderName, String messageText, Long timestamp, boolean isReadOnPhone) {
+ Intent intent = new Intent();
+ intent.setAction(BluetoothMapClient.ACTION_MESSAGE_RECEIVED);
+ intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
+ intent.putExtra(BluetoothMapClient.EXTRA_MESSAGE_HANDLE, handle);
+ intent.putExtra(BluetoothMapClient.EXTRA_SENDER_CONTACT_URI, senderUri);
+ intent.putExtra(BluetoothMapClient.EXTRA_SENDER_CONTACT_NAME, senderName);
+ intent.putExtra(BluetoothMapClient.EXTRA_MESSAGE_READ_STATUS, isReadOnPhone);
+ intent.putExtra(android.content.Intent.EXTRA_TEXT, messageText);
+ if (timestamp != null) {
+ intent.putExtra(BluetoothMapClient.EXTRA_MESSAGE_TIMESTAMP, timestamp);
+ }
+ return intent;
+ }
+
+ private void createMockMessages() {
+ mMessageOneIntent= createMessageIntent(mMockBluetoothDeviceOne, "mockHandle",
+ "510-111-2222", "testSender",
+ "Hello", /* timestamp= */ null, /* isReadOnPhone */ false);
+ }
+}
diff --git a/tests/robotests/src/com/android/car/messenger/bluetooth/BluetoothHelperTest.java b/tests/robotests/src/com/android/car/messenger/bluetooth/BluetoothHelperTest.java
new file mode 100644
index 0000000..7e9e9ac
--- /dev/null
+++ b/tests/robotests/src/com/android/car/messenger/bluetooth/BluetoothHelperTest.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.messenger.bluetooth;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertThrows;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothMapClient;
+
+import com.android.car.messenger.testutils.ShadowBluetoothAdapter;
+
+import com.google.common.collect.Sets;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
+
+import java.util.Collections;
+import java.util.Set;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = {ShadowBluetoothAdapter.class})
+public class BluetoothHelperTest {
+
+ private static final String BLUETOOTH_ADDRESS_ONE = "FA:F8:14:CA:32:39";
+ private static final String BLUETOOTH_ADDRESS_TWO = "FA:F8:33:44:32:39";
+
+ private BluetoothMapClient mMockMapClient;
+ @Mock
+ private BluetoothDevice mMockDeviceOne;
+ @Mock
+ private BluetoothDevice mMockDeviceTwo;
+
+ @Before
+ public void setUp() {
+ Answer returnTrue = new Answer() {
+ public Object answer(InvocationOnMock invocation) {
+ return true;
+ }
+ };
+ MockitoAnnotations.initMocks(this);
+ when(mMockDeviceOne.getAddress()).thenReturn(BLUETOOTH_ADDRESS_ONE);
+ when(mMockDeviceTwo.getAddress()).thenReturn(BLUETOOTH_ADDRESS_TWO);
+ mMockMapClient = Mockito.mock(BluetoothMapClient.class, returnTrue);
+
+ BluetoothAdapter.getDefaultAdapter().enable();
+ }
+
+ @After
+ public void tearDown() {
+ ShadowBluetoothAdapter.reset();
+ }
+
+ @Test
+ public void testGetPairedDevices_nullAdapter() {
+ ShadowBluetoothAdapter.setBluetoothEnabled(false);
+
+ assertThat(BluetoothHelper.getBondedDevices()).isEmpty();
+ }
+
+ @Test
+ public void testGetPairedDevices_noBondedDevices() {
+ getShadowBluetoothAdapter().setBondedDevices(Collections.emptySet());
+
+ assertThat(BluetoothHelper.getBondedDevices()).isEmpty();
+ }
+
+ @Test
+ public void testGetPairedDevices_bondedDevices() {
+ getShadowBluetoothAdapter().setBondedDevices(
+ Sets.newHashSet(mMockDeviceOne, mMockDeviceTwo));
+
+ Set<BluetoothDevice> bondedDevices = BluetoothHelper.getBondedDevices();
+ assertThat(bondedDevices).hasSize(2);
+ assertThat(bondedDevices).contains(mMockDeviceOne);
+ assertThat(bondedDevices).contains(mMockDeviceTwo);
+ }
+
+ @Test
+ public void testSendMessage_nullAdapter() {
+ ShadowBluetoothAdapter.setBluetoothEnabled(false);
+
+ assertThat(BluetoothHelper.sendMessage(mMockMapClient, BLUETOOTH_ADDRESS_ONE, null,
+ "test message", null, null)).isFalse();
+ }
+
+ @Test
+ public void testSendMessage_existingDevice() {
+ getShadowBluetoothAdapter().addRemoteDevice(mMockDeviceOne);
+
+ assertThat(BluetoothHelper.sendMessage(mMockMapClient, BLUETOOTH_ADDRESS_ONE, null,
+ "test message", null, null)).isTrue();
+ }
+
+ @Test
+ public void testSendMessage_invalidDeviceAddress() {
+ getShadowBluetoothAdapter().addRemoteDevice(mMockDeviceTwo);
+
+ assertThrows(IllegalArgumentException.class,
+ () -> BluetoothHelper.sendMessage(mMockMapClient, BLUETOOTH_ADDRESS_ONE, null,
+ "test message", null, null));
+ }
+
+ private ShadowBluetoothAdapter getShadowBluetoothAdapter() {
+ return (ShadowBluetoothAdapter)
+ Shadow.extract(
+ BluetoothAdapter.getDefaultAdapter());
+ }
+}
diff --git a/tests/robotests/src/com/android/car/messenger/bluetooth/BluetoothMonitorTest.java b/tests/robotests/src/com/android/car/messenger/bluetooth/BluetoothMonitorTest.java
new file mode 100644
index 0000000..addb69f
--- /dev/null
+++ b/tests/robotests/src/com/android/car/messenger/bluetooth/BluetoothMonitorTest.java
@@ -0,0 +1,56 @@
+package com.android.car.messenger.bluetooth;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.verify;
+
+import android.bluetooth.BluetoothMapClient;
+import android.bluetooth.BluetoothProfile;
+import android.content.Context;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(RobolectricTestRunner.class)
+public class BluetoothMonitorTest {
+
+ @Mock
+ BluetoothMapClient mockMapClient;
+ @Mock
+ BluetoothMonitor.OnBluetoothEventListener mockBluetoothEventListener;
+
+ private Context mContext;
+ private BluetoothMonitor mBluetoothMonitor;
+ private BluetoothProfile.ServiceListener mServiceListener;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mContext = RuntimeEnvironment.application;
+ mBluetoothMonitor = new BluetoothMonitor(mContext);
+ mServiceListener = mBluetoothMonitor.getServiceListener();
+ mBluetoothMonitor.registerListener(mockBluetoothEventListener);
+ }
+
+ @Test
+ public void testServiceListener() {
+ mServiceListener.onServiceConnected(BluetoothProfile.MAP_CLIENT, mockMapClient);
+ verify(mockBluetoothEventListener).onMapConnected(mockMapClient);
+
+ mServiceListener.onServiceDisconnected(BluetoothProfile.MAP_CLIENT);
+ verify(mockBluetoothEventListener).onMapDisconnected();
+ }
+
+ @Test
+ public void testRegisterListener() {
+ assertThat(mBluetoothMonitor.registerListener(mockBluetoothEventListener)).isFalse();
+ assertThat(mBluetoothMonitor.unregisterListener(mockBluetoothEventListener)).isTrue();
+ assertThat(mBluetoothMonitor.registerListener(mockBluetoothEventListener)).isTrue();
+ mBluetoothMonitor.onDestroy();
+ assertThat(mBluetoothMonitor.registerListener(mockBluetoothEventListener)).isTrue();
+ }
+}
diff --git a/tests/robotests/src/com/android/car/messenger/testutils/ShadowBluetoothAdapter.java b/tests/robotests/src/com/android/car/messenger/testutils/ShadowBluetoothAdapter.java
new file mode 100644
index 0000000..8554ef5
--- /dev/null
+++ b/tests/robotests/src/com/android/car/messenger/testutils/ShadowBluetoothAdapter.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.messenger.testutils;
+
+import android.annotation.Nullable;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.Resetter;
+import org.robolectric.shadows.ShadowApplication;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@Implements(BluetoothAdapter.class)
+public class ShadowBluetoothAdapter extends org.robolectric.shadows.ShadowBluetoothAdapter {
+
+ private static boolean mBluetoothEnabled = true;
+ private static Map<String, BluetoothDevice> mDeviceAddressToDeviceMap = new HashMap<>();
+
+ @Nullable
+ @Implementation
+ protected static synchronized BluetoothAdapter getDefaultAdapter() {
+ if (mBluetoothEnabled) {
+ return (BluetoothAdapter) ShadowApplication.getInstance().getBluetoothAdapter();
+ }
+ return null;
+ }
+
+ public static void setBluetoothEnabled(boolean enabled) {
+ mBluetoothEnabled = enabled;
+ }
+
+ public static void addRemoteDevice(BluetoothDevice device) {
+ mDeviceAddressToDeviceMap.put(device.getAddress(), device);
+ }
+
+ @Implementation
+ protected BluetoothDevice getRemoteDevice(String address) throws IllegalArgumentException {
+ if (!mDeviceAddressToDeviceMap.containsKey(address)) {
+ throw new IllegalArgumentException();
+ }
+ return mDeviceAddressToDeviceMap.get(address);
+ }
+
+ @Resetter
+ public static void reset() {
+ mBluetoothEnabled = true;
+ mDeviceAddressToDeviceMap.clear();
+ }
+}