summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorXin Li <delphij@google.com>2020-08-27 10:16:46 -0700
committerXin Li <delphij@google.com>2020-08-27 10:16:46 -0700
commitd07994ac9db451db4a047e2178987ffaab6fdf13 (patch)
treecf021df6043c34c183ad40ccf0db0033d7002d83
parent68538c52302c4670060cf68120da97c17f925158 (diff)
parent99b73806aceeadd04edb8ada16e19150a6c05f18 (diff)
downloadplatform_packages_apps_Car_CompanionDeviceSupport-d07994ac9db451db4a047e2178987ffaab6fdf13.tar.gz
platform_packages_apps_Car_CompanionDeviceSupport-d07994ac9db451db4a047e2178987ffaab6fdf13.tar.bz2
platform_packages_apps_Car_CompanionDeviceSupport-d07994ac9db451db4a047e2178987ffaab6fdf13.zip
Merge Android R (rvc-dev-plus-aosp-without-vendor@6692709)
Bug: 166295507 Merged-In: I0573e1cc0e0a32ca9eaf0ba3504a6bf446afabb2 Change-Id: Idba575ac3010c6f441e7037a02876d46c917d995
-rw-r--r--Android.bp4
-rw-r--r--AndroidManifest.xml17
-rw-r--r--res/drawable/ic_connection_indicator.xml25
-rw-r--r--res/layout/add_associated_device_fragment.xml54
-rw-r--r--res/layout/associated_device_detail_fragment.xml113
-rw-r--r--res/layout/base_activity.xml9
-rw-r--r--res/layout/confirm_pairing_code_fragment.xml2
-rw-r--r--res/layout/trusted_device_detail_fragment.xml45
-rw-r--r--res/values-land/dimens.xml2
-rw-r--r--res/values/colors.xml4
-rw-r--r--res/values/dimens.xml33
-rw-r--r--res/values/strings.xml10
-rw-r--r--res/values/styles.xml28
-rw-r--r--res/values/themes.xml2
-rw-r--r--src/com/android/car/companiondevicesupport/activity/AssociatedDeviceDetailFragment.java51
-rw-r--r--src/com/android/car/companiondevicesupport/activity/AssociationActivity.java25
-rw-r--r--src/com/android/car/companiondevicesupport/feature/RemoteFeature.java40
-rw-r--r--src/com/android/car/companiondevicesupport/feature/calendarsync/CalendarCleaner.java11
-rw-r--r--src/com/android/car/companiondevicesupport/feature/calendarsync/CalendarImporter.java51
-rw-r--r--src/com/android/car/companiondevicesupport/feature/notificationmsg/NotificationMsgDelegate.java171
-rw-r--r--src/com/android/car/companiondevicesupport/feature/notificationmsg/NotificationMsgFeature.java2
-rw-r--r--src/com/android/car/companiondevicesupport/feature/notificationmsg/NotificationMsgService.java3
-rw-r--r--src/com/android/car/companiondevicesupport/feature/trust/ui/TrustedDeviceActivity.java13
-rw-r--r--tests/unit/AndroidManifest.xml10
-rw-r--r--tests/unit/src/com/android/car/companiondevicesupport/feature/calendarsync/CalendarCleanerTest.java27
-rw-r--r--tests/unit/src/com/android/car/companiondevicesupport/feature/calendarsync/CalendarImporterTest.java37
-rw-r--r--tests/unit/src/com/android/car/companiondevicesupport/feature/notificationmsg/NotificationMsgDelegateTest.java785
-rw-r--r--tests/unit/src/com/android/car/companiondevicesupport/feature/notificationmsg/NotificationMsgFeatureTest.java159
-rw-r--r--tests/unit/src/com/android/car/companiondevicesupport/feature/notificationmsg/OWNERS3
29 files changed, 1453 insertions, 283 deletions
diff --git a/Android.bp b/Android.bp
index ed40892..16d2b70 100644
--- a/Android.bp
+++ b/Android.bp
@@ -64,7 +64,6 @@ android_library {
static_libs: [
"CompanionDeviceSupport-aidl",
"CompanionDeviceSupport-proto",
- "EncryptionRunner-lib",
"androidx-constraintlayout_constraintlayout",
"androidx-constraintlayout_constraintlayout-solver",
"androidx.legacy_legacy-support-v4",
@@ -75,10 +74,11 @@ android_library {
"com.google.android.material_material",
"companion-feature-calendarsync-protos",
"connected-device-lib",
+ "encryption-runner",
],
plugins: [
- "car-androidx-room-compiler",
+ "androidx.room_room-compiler-plugin",
],
}
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 4d4ddb3..ab9da21 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -29,11 +29,13 @@
<!-- Needed for the calendar sync feature -->
<uses-permission android:name="android.permission.WRITE_CALENDAR"/>
+ <!-- Needed to post messaging notifications on behalf of other apps -->
+ <uses-permission android:name="android.permission.SUBSTITUTE_NOTIFICATION_APP_NAME"/>
+
<application
android:label="@string/app_name"
android:theme="@style/CompanionDeviceSupportTheme"
- android:directBootAware="true"
- xmlns:tools="http://schemas.android.com/tools">
+ android:directBootAware="true">
<service
android:name=".service.CompanionDeviceSupportService"
android:singleUser="true"
@@ -48,7 +50,8 @@
</intent-filter>
</service>
<activity android:name=".activity.AssociationActivity"
- android:exported="true">
+ android:exported="true"
+ android:launchMode="singleInstance">
<intent-filter>
<action android:name="com.android.car.companiondevicesupport.ASSOCIATION_ACTIVITY" />
<action android:name="com.android.settings.action.EXTRA_SETTINGS" />
@@ -116,13 +119,5 @@
<meta-data android:name="com.android.settings.category"
android:value="com.android.settings.category.ia.security" />
</activity>
-
- <!-- Workaround for b/113294940 -->
- <provider
- android:name="androidx.lifecycle.ProcessLifecycleOwnerInitializer"
- tools:replace="android:authorities"
- android:authorities="${applicationId}.lifecycle"
- android:exported="false"
- android:multiprocess="true" />
</application>
</manifest>
diff --git a/res/drawable/ic_connection_indicator.xml b/res/drawable/ic_connection_indicator.xml
new file mode 100644
index 0000000..35e3ab3
--- /dev/null
+++ b/res/drawable/ic_connection_indicator.xml
@@ -0,0 +1,25 @@
+<?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.
+ -->
+
+<shape
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="oval">
+ <solid android:color="?android:attr/textColorPrimary" />
+ <size
+ android:width="@dimen/connection_indicator_size"
+ android:height="@dimen/connection_indicator_size" />
+</shape> \ No newline at end of file
diff --git a/res/layout/add_associated_device_fragment.xml b/res/layout/add_associated_device_fragment.xml
index 044fd66..d068a41 100644
--- a/res/layout/add_associated_device_fragment.xml
+++ b/res/layout/add_associated_device_fragment.xml
@@ -71,26 +71,35 @@
app:layout_constraintTop_toBottomOf="@+id/add_associated_device_instruction"
app:layout_constraintStart_toEndOf="@+id/start_guideline"
app:layout_constraintEnd_toStartOf="@+id/end_guideline"
- style="@style/InstructionStepItem">
+ android:minHeight="@dimen/list_item_height">
<ImageView
android:id="@+id/install_app_icon"
- android:layout_width="@dimen/icon_size"
- android:layout_height="@dimen/icon_size"
- android:layout_marginStart="@dimen/icon_margin_start"
+ android:layout_width="@dimen/list_item_icon_size"
+ android:layout_height="@dimen/list_item_icon_size"
+ android:layout_gravity="center"
+ android:scaleType="fitXY"
android:src="@drawable/ic_smartphone"
app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/install_app_icon_end_guideline"
app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintBottom_toBottomOf="parent"
- style="@style/AssociationDetailIcon"/>
+ app:layout_constraintBottom_toBottomOf="parent"/>
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/install_app_icon_end_guideline"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:layout_constraintGuide_begin="@dimen/car_ui_list_item_icon_container_width" />
<TextView
android:id="@+id/install_app_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
- android:layout_marginStart="@dimen/text_margin_start"
- android:layout_marginEnd="@dimen/text_margin_end"
+ android:layout_marginStart="@dimen/list_item_text_margin_start"
+ android:layout_marginEnd="@dimen/list_item_text_margin_end"
+ android:layout_marginBottom="@dimen/list_item_content_margin_bottom"
+ android:layout_marginTop="@dimen/list_item_content_margin_top"
android:text="@string/associated_device_install_app"
android:textAppearance="@style/AssociationInstruction"
- app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintStart_toEndOf="@+id/install_app_icon_end_guideline"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
@@ -103,26 +112,35 @@
app:layout_constraintStart_toEndOf="@+id/start_guideline"
app:layout_constraintEnd_toStartOf="@+id/end_guideline"
app:layout_constraintTop_toBottomOf="@+id/install_app_instruction"
- style="@style/InstructionStepItem">
+ android:minHeight="@dimen/list_item_height">
<ImageView
android:id="@+id/select_device_icon"
- android:layout_width="@dimen/icon_size"
- android:layout_height="@dimen/icon_size"
- android:layout_marginStart="@dimen/icon_margin_start"
+ android:layout_width="@dimen/list_item_icon_size"
+ android:layout_height="@dimen/list_item_icon_size"
+ android:layout_gravity="center"
+ android:scaleType="fitXY"
android:src="@drawable/ic_directions_car"
app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/select_device_icon_end_guideline"
app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintBottom_toBottomOf="parent"
- style="@style/AssociationDetailIcon"/>
+ app:layout_constraintBottom_toBottomOf="parent"/>
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/select_device_icon_end_guideline"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:layout_constraintGuide_begin="@dimen/car_ui_list_item_icon_container_width" />
<TextView
android:id="@+id/associated_device_select_device"
android:layout_width="0dp"
android:layout_height="wrap_content"
- android:layout_marginStart="@dimen/text_margin_start"
- android:layout_marginEnd="@dimen/text_margin_end"
+ android:layout_marginStart="@dimen/list_item_text_margin_start"
+ android:layout_marginEnd="@dimen/list_item_text_margin_end"
+ android:layout_marginBottom="@dimen/list_item_content_margin_bottom"
+ android:layout_marginTop="@dimen/list_item_content_margin_top"
android:text="@string/associated_device_select_device"
android:textAppearance="@style/AssociationInstruction"
- app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintStart_toEndOf="@+id/select_device_icon_end_guideline"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
diff --git a/res/layout/associated_device_detail_fragment.xml b/res/layout/associated_device_detail_fragment.xml
index f5f6a53..b0738be 100644
--- a/res/layout/associated_device_detail_fragment.xml
+++ b/res/layout/associated_device_detail_fragment.xml
@@ -43,16 +43,29 @@
app:layout_constraintTop_toBottomOf="@+id/device_icon"
app:layout_constraintBottom_toTopOf="@+id/connection_status"/>
- <TextView
+ <LinearLayout
android:id="@+id/connection_status"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="center_vertical"
android:layout_marginTop="@dimen/device_detail_margin"
- android:textAppearance="@style/AssociationMessage"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/device_name"
- app:layout_constraintBottom_toTopOf="@+id/connection_button"/>
+ app:layout_constraintBottom_toTopOf="@+id/connection_button">
+ <ImageView
+ android:id="@+id/connection_status_indicator"
+ android:layout_height="@dimen/connection_indicator_size"
+ android:layout_width="@dimen/connection_indicator_size"
+ android:layout_marginEnd="@dimen/connection_indicator_margin_end"
+ android:src="@drawable/ic_connection_indicator"/>
+ <TextView
+ android:id="@+id/connection_status_text"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:textAppearance="@style/AssociationMessage"/>
+ </LinearLayout>
<androidx.constraintlayout.widget.Guideline
android:id="@+id/start_guideline"
@@ -66,30 +79,40 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/device_detail_button_margin_top"
+ android:background="@drawable/car_ui_list_item_background"
+ android:clickable="true"
+ android:minHeight="@dimen/list_item_height"
app:layout_constraintStart_toEndOf="@+id/start_guideline"
app:layout_constraintEnd_toStartOf="@+id/end_guideline"
app:layout_constraintTop_toBottomOf="@+id/connection_status"
- app:layout_constraintBottom_toTopOf="@+id/remove_button"
- style="@style/DeviceDetailButton">
+ app:layout_constraintBottom_toTopOf="@+id/remove_button">
<ImageView
android:id="@+id/connection_icon"
- android:layout_width="@dimen/icon_size"
- android:layout_height="@dimen/icon_size"
- android:layout_marginStart="@dimen/icon_margin_start"
+ android:layout_width="@dimen/list_item_icon_size"
+ android:layout_height="@dimen/list_item_icon_size"
+ android:scaleType="fitXY"
android:src="@drawable/ic_phonelink_erase"
app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/connection_icon_end_guideline"
app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintBottom_toBottomOf="parent"
- style="@style/AssociationDetailIcon"/>
+ app:layout_constraintBottom_toBottomOf="parent"/>
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/connection_icon_end_guideline"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:layout_constraintGuide_begin="@dimen/car_ui_list_item_icon_container_width" />
<TextView
android:id="@+id/connection_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
- android:layout_marginStart="@dimen/text_margin_start"
- android:layout_marginEnd="@dimen/text_margin_end"
+ android:layout_marginStart="@dimen/list_item_text_margin_start"
+ android:layout_marginEnd="@dimen/list_item_text_margin_end"
+ android:layout_marginBottom="@dimen/list_item_content_margin_bottom"
+ android:layout_marginTop="@dimen/list_item_content_margin_top"
android:text="@string/connection"
android:textAppearance="@style/AssociationTitle"
- app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintStart_toEndOf="@+id/connection_icon_end_guideline"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
@@ -99,30 +122,41 @@
android:id="@+id/remove_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
+ android:background="@drawable/car_ui_list_item_background"
+ android:clickable="true"
+ android:minHeight="@dimen/list_item_height"
app:layout_constraintStart_toEndOf="@+id/start_guideline"
app:layout_constraintEnd_toStartOf="@+id/end_guideline"
app:layout_constraintTop_toBottomOf="@+id/connection_button"
- app:layout_constraintBottom_toTopOf="@+id/divider"
- style="@style/DeviceDetailButton">
+ app:layout_constraintBottom_toTopOf="@+id/divider">
<ImageView
android:id="@+id/delete_icon"
- android:layout_width="@dimen/icon_size"
- android:layout_height="@dimen/icon_size"
- android:layout_marginStart="@dimen/icon_margin_start"
+ android:layout_width="@dimen/list_item_icon_size"
+ android:layout_height="@dimen/list_item_icon_size"
+ android:layout_gravity="center"
+ android:scaleType="fitXY"
android:src="@drawable/ic_delete"
app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/delete_icon_end_guideline"
app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintBottom_toBottomOf="parent"
- style="@style/AssociationDetailIcon"/>
+ app:layout_constraintBottom_toBottomOf="parent"/>
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/delete_icon_end_guideline"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:layout_constraintGuide_begin="@dimen/car_ui_list_item_icon_container_width" />
<TextView
android:id="@+id/remove_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
- android:layout_marginStart="@dimen/text_margin_start"
- android:layout_marginEnd="@dimen/text_margin_end"
+ android:layout_marginStart="@dimen/list_item_text_margin_start"
+ android:layout_marginEnd="@dimen/list_item_text_margin_end"
+ android:layout_marginBottom="@dimen/list_item_content_margin_bottom"
+ android:layout_marginTop="@dimen/list_item_content_margin_top"
android:text="@string/forget_title"
android:textAppearance="@style/AssociationTitle"
- app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintStart_toEndOf="@+id/delete_icon_end_guideline"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
@@ -131,7 +165,7 @@
<View
android:id="@+id/divider"
android:layout_width="0dp"
- android:layout_height="1dp"
+ android:layout_height="@dimen/list_item_divider_width"
android:layout_marginTop="@dimen/device_detail_divider_margin"
app:layout_constraintStart_toEndOf="@+id/start_guideline"
app:layout_constraintEnd_toStartOf="@+id/end_guideline"
@@ -144,29 +178,40 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/device_detail_divider_margin"
+ android:background="@drawable/car_ui_list_item_background"
+ android:clickable="true"
+ android:minHeight="@dimen/list_item_height"
app:layout_constraintTop_toBottomOf="@+id/divider"
app:layout_constraintStart_toEndOf="@+id/start_guideline"
- app:layout_constraintEnd_toStartOf="@+id/end_guideline"
- style="@style/DeviceDetailButton">
+ app:layout_constraintEnd_toStartOf="@+id/end_guideline">
<ImageView
android:id="@+id/trusted_device_icon"
- android:layout_width="@dimen/icon_size"
- android:layout_height="@dimen/icon_size"
- android:layout_marginStart="@dimen/icon_margin_start"
+ android:layout_width="@dimen/list_item_icon_size"
+ android:layout_height="@dimen/list_item_icon_size"
+ android:layout_gravity="center"
+ android:scaleType="fitXY"
android:src="@drawable/ic_lock"
app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/trusted_device_icon_end_guideline"
app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintBottom_toBottomOf="parent"
- style="@style/AssociationDetailIcon"/>
+ app:layout_constraintBottom_toBottomOf="parent"/>
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/trusted_device_icon_end_guideline"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:layout_constraintGuide_begin="@dimen/car_ui_list_item_icon_container_width" />
<TextView
android:id="@+id/trusted_device_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
- android:layout_marginStart="@dimen/text_margin_start"
- android:layout_marginEnd="@dimen/text_margin_end"
+ android:layout_marginStart="@dimen/list_item_text_margin_start"
+ android:layout_marginEnd="@dimen/list_item_text_margin_end"
+ android:layout_marginBottom="@dimen/list_item_content_margin_bottom"
+ android:layout_marginTop="@dimen/list_item_content_margin_top"
android:text="@string/trusted_device_feature_title"
android:textAppearance="@style/AssociationTitle"
- app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintStart_toEndOf="@+id/trusted_device_icon_end_guideline"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
diff --git a/res/layout/base_activity.xml b/res/layout/base_activity.xml
index a654563..830ccd0 100644
--- a/res/layout/base_activity.xml
+++ b/res/layout/base_activity.xml
@@ -20,19 +20,12 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
- <com.android.car.ui.toolbar.Toolbar
- android:id="@+id/toolbar"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- app:layout_constraintTop_toTopOf="parent"
- app:state="subpage"/>
-
<FrameLayout
android:id="@+id/fragment_container"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintTop_toBottomOf="@id/toolbar"
+ app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/res/layout/confirm_pairing_code_fragment.xml b/res/layout/confirm_pairing_code_fragment.xml
index 7af0612..263b187 100644
--- a/res/layout/confirm_pairing_code_fragment.xml
+++ b/res/layout/confirm_pairing_code_fragment.xml
@@ -50,7 +50,7 @@
android:id="@+id/pairing_code"
android:layout_height="@dimen/list_item_height"
android:layout_width="wrap_content"
- android:layout_marginTop="@dimen/pairing_code_top_margin"
+ android:layout_marginTop="@dimen/pairing_code_margin_top"
android:layout_marginStart="@dimen/car_ui_margin"
android:layout_marginEnd="@dimen/car_ui_margin"
android:gravity="center"
diff --git a/res/layout/trusted_device_detail_fragment.xml b/res/layout/trusted_device_detail_fragment.xml
index 6371aff..f2b1274 100644
--- a/res/layout/trusted_device_detail_fragment.xml
+++ b/res/layout/trusted_device_detail_fragment.xml
@@ -20,22 +20,23 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
- <FrameLayout
+ <TextView
android:id="@+id/trusted_device_instruction"
- android:layout_width="match_parent"
+ android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/car_ui_margin"
android:layout_marginEnd="@dimen/car_ui_margin"
- android:gravity="center_horizontal"
- style="@style/DeviceListItem">
- <TextView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginStart="@dimen/no_icon_text_margin_start"
- android:layout_marginEnd="@dimen/text_margin_end"
- android:text="@string/trusted_device_feature_instruction"
- android:textAppearance="@style/TrustedDeviceMessage"/>
- </FrameLayout>
+ android:minHeight="@dimen/list_item_height"
+ android:gravity="center_vertical"
+ android:paddingStart="@dimen/list_item_text_no_icon_margin_start"
+ android:paddingEnd="@dimen/list_item_text_margin_end"
+ android:paddingBottom="@dimen/list_item_content_margin_bottom"
+ android:paddingTop="@dimen/list_item_content_margin_top"
+ android:text="@string/trusted_device_feature_instruction"
+ android:textAppearance="@style/TrustedDeviceMessage"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"/>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/connection_button"
@@ -43,27 +44,33 @@
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/car_ui_margin"
android:layout_marginEnd="@dimen/car_ui_margin"
+ android:minHeight="@dimen/list_item_height"
app:layout_constraintTop_toBottomOf="@+id/trusted_device_instruction"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
- style="@style/DeviceListItem">
+ app:layout_constraintEnd_toEndOf="parent">
<TextView
android:id="@+id/trusted_device_item_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
- android:layout_marginStart="@dimen/no_icon_text_margin_start"
- android:layout_marginEnd="@dimen/text_margin_end"
+ android:layout_marginStart="@dimen/list_item_text_no_icon_margin_start"
+ android:layout_marginEnd="@dimen/list_item_text_margin_end"
android:textAppearance="@style/TrustedDeviceTitle"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintEnd_toStartOf="@+id/trusted_device_switch"
+ app:layout_constraintEnd_toStartOf="@+id/switch_start_guideline"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/switch_start_guideline"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:layout_constraintGuide_end="@dimen/car_ui_list_item_icon_container_width" />
<Switch
android:id="@+id/trusted_device_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginEnd="@dimen/text_margin_end"
- app:layout_constraintStart_toEndOf="@+id/trusted_device_item_title"
+ android:layout_gravity="center"
+ app:layout_constraintStart_toEndOf="@+id/switch_start_guideline"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
diff --git a/res/values-land/dimens.xml b/res/values-land/dimens.xml
index 5e25502..29e29fb 100644
--- a/res/values-land/dimens.xml
+++ b/res/values-land/dimens.xml
@@ -21,5 +21,5 @@
<dimen name="device_detail_margin_top">@dimen/car_ui_padding_1</dimen>
<dimen name="device_detail_margin">@dimen/car_ui_padding_1</dimen>
<dimen name="device_detail_button_margin_top">@dimen/car_ui_padding_1</dimen>
- <dimen name="device_detail_divider_margin">@dimen/car_ui_padding_1</dimen>
+ <dimen name="device_detail_divider_margin">0dp</dimen>
</resources>
diff --git a/res/values/colors.xml b/res/values/colors.xml
index ba7d0f4..c1a7a7a 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -15,5 +15,9 @@
-->
<resources>
+ <color name="connection_color_connected">@*android:color/car_green_500</color>
+ <color name="connection_color_disconnected">@*android:color/car_red_500</color>
+ <color name="connection_color_not_detected">#b7ffffff</color>
+
<color name="car_red_300">@*android:color/car_red_300</color>
</resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index d2c9db7..150ce7b 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -16,36 +16,33 @@
-->
<resources>
- <dimen name="car_button_height">@*android:dimen/car_button_height</dimen>
- <dimen name="icon_size">@*android:dimen/car_primary_icon_size</dimen>
- <dimen name="icon_margin_end">44dp</dimen>
- <dimen name="icon_margin_start">@dimen/car_ui_keyline_1</dimen>
- <dimen name="text_margin_start">@dimen/car_ui_keyline_3</dimen>
- <dimen name="text_margin_end">@dimen/car_ui_keyline_1</dimen>
- <dimen name="no_icon_text_margin_start">@dimen/car_ui_keyline_1</dimen>
- <dimen name="list_item_margin_top">@dimen/car_ui_preference_content_margin_top</dimen>
- <dimen name="list_item_margin_bottom">@dimen/car_ui_preference_content_margin_bottom</dimen>
- <dimen name="list_item_margin_end">@dimen/car_ui_preference_icon_margin_end</dimen>
- <dimen name="list_item_height">116dp</dimen>
- <dimen name="list_item_padding">36dp</dimen>
-
+ <dimen name="icon_size">@dimen/car_ui_primary_icon_size</dimen>
<dimen name="title_margin_top">116dp</dimen>
<dimen name="message_margin_top">@dimen/car_ui_padding_3</dimen>
<dimen name="button_margin_top">48dp</dimen>
+ <dimen name="list_item_height">@dimen/car_ui_list_item_height</dimen>
+ <dimen name="list_item_icon_size">@dimen/car_ui_list_item_icon_size</dimen>
+ <dimen name="list_item_icon_container_size">@dimen/car_ui_list_item_icon_container_width</dimen>
+ <dimen name="list_item_text_margin_start">@dimen/car_ui_list_item_text_start_margin</dimen>
+ <dimen name="list_item_text_margin_end">@dimen/car_ui_list_item_text_start_margin</dimen>
+ <dimen name="list_item_text_no_icon_margin_start">@dimen/car_ui_list_item_text_no_icon_start_margin</dimen>
+ <dimen name="list_item_content_margin_top">@dimen/car_ui_preference_content_margin_top</dimen>
+ <dimen name="list_item_content_margin_bottom">@dimen/car_ui_preference_content_margin_bottom</dimen>
+ <dimen name="list_item_divider_width">@dimen/car_ui_divider_width</dimen>
+
<dimen name="instruction_margin_top">48dp</dimen>
<dimen name="instruction_detail_margin_top">@dimen/car_ui_padding_3</dimen>
<dimen name="instruction_steps_margin_top">72dp</dimen>
- <dimen name="pairing_code_top_margin">@dimen/car_ui_padding_5</dimen>
-
<dimen name="device_detail_margin_top">48dp</dimen>
<dimen name="device_detail_margin">@dimen/car_ui_padding_3</dimen>
- <dimen name="device_detail_text_margin">44dp</dimen>
<dimen name="device_detail_button_margin_top">72dp</dimen>
<dimen name="device_detail_divider_margin">@dimen/car_ui_padding_2</dimen>
- <dimen name="car_ui_display1_size">44sp</dimen>
-
+ <dimen name="pairing_code_margin_top">@dimen/car_ui_padding_5</dimen>
<item name="pairing_code_text_spacing" format="float" type="dimen">0.6</item>
+
+ <dimen name="connection_indicator_size">12dp</dimen>
+ <dimen name="connection_indicator_margin_end">12dp</dimen>
</resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 51d6951..4b22e7a 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -99,15 +99,11 @@
<string name="unknown">Unknown</string>
<!-- NotificationMsg Feature strings-->
- <!-- Notification Channel name for the Companion Device Text Message notifications service [CHAR LIMIT=80]-->
- <string name="notification_msg_channel_name">Companion Device Text Message Notifications Channel</string>
- <!-- Notification Channel description for the Companion Device Text Message notifications service [CHAR LIMIT=50]-->
- <string name="notification_msg_channel_description">Phone Text Message Receiver Service</string>
<!--Notification Channel name for the silent "message notification service running" notification [CHAR LIMIT=100]-->
- <string name="app_running_msg_channel_name">Phone Text Message service running</string>
+ <string name="app_running_msg_channel_name">Phone text message service running</string>
<!--Notification Channel description for the silent "message notification service running" notification [CHAR LIMIT=50]-->
- <string name="app_running_msg_notification_title">Phone Text Messaging service is active</string>
- <string name="app_running_msg_notification_content">Receiving Messages via Companion Device</string>
+ <string name="app_running_msg_notification_title">Phone text messaging service is active</string>
+ <string name="app_running_msg_notification_content">Receiving text messages via companion device</string>
<!-- Title for trusted device feature [CHAR LIMIT=40]-->
<string name="trusted_device_feature_title">Unlock profile with phone</string>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 45ac7ea..02835fc 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -19,43 +19,27 @@
<item name="android:tint">@color/car_ui_color_accent</item>
<item name="android:scaleType">fitCenter</item>
</style>
- <style name="AssociationDetailIcon">
- <item name="android:tint">?android:attr/textColorPrimary</item>
- <item name="android:scaleType">fitCenter</item>
- </style>
<style name="AssociationTitle" parent="TextAppearance.Body1"/>
+
<style name="AssociationInstruction" parent="TextAppearance.Body3"/>
+
<style name="AssociationMessage" parent="TextAppearance.Body3">
<item name="android:textColor">?android:attr/textColorTertiary</item>
</style>
+
<style name="AssociationPairingCode" parent="TextAppearance.Display1">
<item name="android:letterSpacing">@dimen/pairing_code_text_spacing</item>
<item name="android:textColor">@color/car_ui_color_accent</item>
</style>
+
<style name="TrustedDeviceTitle" parent="TextAppearance.Body1"/>
- <style name="TrustedDeviceMessage" parent="TextAppearance.Body3">
- <item name="android:textColor">?android:attr/textColorTertiary</item>
- </style>
- <style name="ConnectionDisabled">
- <item name="android:textColor">@*android:color/car_red_300</item>
- </style>
- <style name="ConnectionEnabled">
+ <style name="TrustedDeviceMessage" parent="TextAppearance.Body3">
<item name="android:textColor">?android:attr/textColorTertiary</item>
</style>
<style name="DeviceDetailDivider">
<item name="android:background">?android:attr/textColorPrimary</item>
</style>
-
- <style name="InstructionStepItem" parent="DeviceListItem"/>
- <style name="DeviceDetailButton" parent="DeviceListItem">
- <item name="android:background">?android:attr/selectableItemBackground</item>
- </style>
- <style name="DeviceListItem">
- <item name="android:minHeight">@dimen/list_item_height</item>
- <item name="android:paddingTop">@dimen/list_item_padding</item>
- <item name="android:paddingBottom">@dimen/list_item_padding</item>
- </style>
-</resources> \ No newline at end of file
+</resources>
diff --git a/res/values/themes.xml b/res/values/themes.xml
index ef4ea68..d7c3ebf 100644
--- a/res/values/themes.xml
+++ b/res/values/themes.xml
@@ -16,5 +16,5 @@
-->
<resources>
- <style name="CompanionDeviceSupportTheme" parent="Theme.CarUi"/>
+ <style name="CompanionDeviceSupportTheme" parent="Theme.CarUi.WithToolbar"/>
</resources>
diff --git a/src/com/android/car/companiondevicesupport/activity/AssociatedDeviceDetailFragment.java b/src/com/android/car/companiondevicesupport/activity/AssociatedDeviceDetailFragment.java
index 0438636..ab3224c 100644
--- a/src/com/android/car/companiondevicesupport/activity/AssociatedDeviceDetailFragment.java
+++ b/src/com/android/car/companiondevicesupport/activity/AssociatedDeviceDetailFragment.java
@@ -18,6 +18,7 @@ package com.android.car.companiondevicesupport.activity;
import static com.android.car.connecteddevice.util.SafeLog.loge;
+import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
@@ -30,7 +31,6 @@ import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProviders;
import com.android.car.companiondevicesupport.R;
-import com.android.car.companiondevicesupport.api.external.AssociatedDevice;
import com.android.car.companiondevicesupport.feature.trust.TrustedDeviceConstants;
@@ -38,7 +38,8 @@ import com.android.car.companiondevicesupport.feature.trust.TrustedDeviceConstan
public class AssociatedDeviceDetailFragment extends Fragment {
private final static String TAG = "AssociatedDeviceDetailFragment";
private TextView mDeviceName;
- private TextView mConnectionStatus;
+ private TextView mConnectionStatusText;
+ private ImageView mConnectionStatusIndicator;
private TextView mConnectionText;
private ImageView mConnectionIcon;
private AssociatedDeviceViewModel mModel;
@@ -54,7 +55,8 @@ public class AssociatedDeviceDetailFragment extends Fragment {
mDeviceName = view.findViewById(R.id.device_name);
mConnectionIcon = view.findViewById(R.id.connection_icon);
mConnectionText = view.findViewById(R.id.connection_text);
- mConnectionStatus = view.findViewById(R.id.connection_status);
+ mConnectionStatusText = view.findViewById(R.id.connection_status_text);
+ mConnectionStatusIndicator = view.findViewById(R.id.connection_status_indicator);
mModel = ViewModelProviders.of(getActivity()).get(AssociatedDeviceViewModel.class);
mModel.getDeviceDetails().observe(this, this::setDeviceDetails);
@@ -74,30 +76,33 @@ public class AssociatedDeviceDetailFragment extends Fragment {
return;
}
mDeviceName.setText(deviceDetails.getDeviceName());
+
if (!deviceDetails.isConnectionEnabled()) {
- setConnectionDisabledStyle();
- return;
- }
- setConnectionEnabledStyle();
- if (deviceDetails.isConnected()) {
- mConnectionStatus.setText(getString(R.string.connected));
+ setConnectionStatus(
+ ContextCompat.getColor(getContext(), R.color.connection_color_disconnected),
+ getString(R.string.disconnected),
+ ContextCompat.getDrawable(getContext(), R.drawable.ic_phonelink_ring),
+ getString(R.string.enable_device_connection_text));
+ } else if (deviceDetails.isConnected()) {
+ setConnectionStatus(
+ ContextCompat.getColor(getContext(), R.color.connection_color_connected),
+ getString(R.string.connected),
+ ContextCompat.getDrawable(getContext(), R.drawable.ic_phonelink_erase),
+ getString(R.string.disable_device_connection_text));
} else {
- mConnectionStatus.setText(getString(R.string.notDetected));
+ setConnectionStatus(
+ ContextCompat.getColor(getContext(), R.color.connection_color_not_detected),
+ getString(R.string.notDetected),
+ ContextCompat.getDrawable(getContext(), R.drawable.ic_phonelink_erase),
+ getString(R.string.disable_device_connection_text));
}
}
- private void setConnectionEnabledStyle() {
- mConnectionText.setText(getString(R.string.disable_device_connection_text));
- mConnectionIcon.setImageDrawable(ContextCompat.getDrawable(getContext(),
- R.drawable.ic_phonelink_erase));
- mConnectionStatus.setTextAppearance(R.style.ConnectionEnabled);
- }
-
- private void setConnectionDisabledStyle() {
- mConnectionStatus.setText(getString(R.string.disconnected));
- mConnectionStatus.setTextAppearance(R.style.ConnectionDisabled);
- mConnectionText.setText(getString(R.string.enable_device_connection_text));
- mConnectionIcon.setImageDrawable(ContextCompat.getDrawable(getContext(),
- R.drawable.ic_phonelink_ring));
+ private void setConnectionStatus(int connectionStatusColor, String connectionStatusText,
+ Drawable connectionIcon, String connectionText) {
+ mConnectionStatusText.setText(connectionStatusText);
+ mConnectionStatusIndicator.setColorFilter(connectionStatusColor);
+ mConnectionText.setText(connectionText);
+ mConnectionIcon.setImageDrawable(connectionIcon);
}
}
diff --git a/src/com/android/car/companiondevicesupport/activity/AssociationActivity.java b/src/com/android/car/companiondevicesupport/activity/AssociationActivity.java
index bb522c4..aae0a9c 100644
--- a/src/com/android/car/companiondevicesupport/activity/AssociationActivity.java
+++ b/src/com/android/car/companiondevicesupport/activity/AssociationActivity.java
@@ -18,6 +18,8 @@ package com.android.car.companiondevicesupport.activity;
import static com.android.car.connecteddevice.util.SafeLog.logd;
import static com.android.car.connecteddevice.util.SafeLog.loge;
+import static com.android.car.ui.core.CarUi.requireToolbar;
+import static com.android.car.ui.toolbar.Toolbar.State.SUBPAGE;
import android.annotation.NonNull;
import android.app.AlertDialog;
@@ -38,7 +40,7 @@ import androidx.lifecycle.ViewModelProviders;
import com.android.car.companiondevicesupport.R;
import com.android.car.companiondevicesupport.api.external.AssociatedDevice;
import com.android.car.ui.toolbar.MenuItem;
-import com.android.car.ui.toolbar.Toolbar;
+import com.android.car.ui.toolbar.ToolbarController;
import java.util.Arrays;
@@ -62,19 +64,20 @@ public class AssociationActivity extends FragmentActivity {
private static final String REMOVE_DEVICE_DIALOG_TAG = "RemoveDeviceDialog";
private static final String TURN_ON_BLUETOOTH_DIALOG_TAG = "TurnOnBluetoothDialog";
- private Toolbar mToolbar;
+ private ToolbarController mToolbar;
private AssociatedDeviceViewModel mModel;
@Override
public void onCreate(Bundle saveInstanceState) {
super.onCreate(saveInstanceState);
setContentView(R.layout.base_activity);
- mToolbar = findViewById(R.id.toolbar);
+ mToolbar = requireToolbar(this);
+ mToolbar.setState(SUBPAGE);
observeViewModel();
if (saveInstanceState != null) {
resumePreviousState();
}
- mToolbar.showProgressBar();
+ mToolbar.getProgressBar().setVisible(true);
}
@Override
@@ -82,7 +85,7 @@ public class AssociationActivity extends FragmentActivity {
super.onBackPressed();
mModel.stopAssociation();
dismissConfirmButtons();
- mToolbar.hideProgressBar();
+ mToolbar.getProgressBar().setVisible(false);
}
private void observeViewModel() {
@@ -167,26 +170,26 @@ public class AssociationActivity extends FragmentActivity {
private void showTurnOnBluetoothFragment() {
TurnOnBluetoothFragment fragment = new TurnOnBluetoothFragment();
- mToolbar.showProgressBar();
+ mToolbar.getProgressBar().setVisible(true);
launchFragment(fragment, TURN_ON_BLUETOOTH_FRAGMENT_TAG);
}
private void showAddAssociatedDeviceFragment(String deviceName) {
AddAssociatedDeviceFragment fragment = AddAssociatedDeviceFragment.newInstance(deviceName);
launchFragment(fragment, ADD_DEVICE_FRAGMENT_TAG);
- mToolbar.showProgressBar();
+ mToolbar.getProgressBar().setVisible(true);
}
private void showConfirmPairingCodeFragment(String pairingCode) {
ConfirmPairingCodeFragment fragment = ConfirmPairingCodeFragment.newInstance(pairingCode);
launchFragment(fragment, PAIRING_CODE_FRAGMENT_TAG);
showConfirmButtons();
- mToolbar.hideProgressBar();
+ mToolbar.getProgressBar().setVisible(false);
}
private void showAssociationErrorFragment() {
dismissConfirmButtons();
- mToolbar.showProgressBar();
+ mToolbar.getProgressBar().setVisible(true);
AssociationErrorFragment fragment = new AssociationErrorFragment();
launchFragment(fragment, ASSOCIATION_ERROR_FRAGMENT_TAG);
}
@@ -194,7 +197,7 @@ public class AssociationActivity extends FragmentActivity {
private void showAssociatedDeviceDetailFragment() {
AssociatedDeviceDetailFragment fragment = new AssociatedDeviceDetailFragment();
launchFragment(fragment, DEVICE_DETAIL_FRAGMENT_TAG);
- mToolbar.hideProgressBar();
+ mToolbar.getProgressBar().setVisible(false);
showTurnOnBluetoothDialog();
}
@@ -254,7 +257,7 @@ public class AssociationActivity extends FragmentActivity {
private void retryAssociation() {
dismissConfirmButtons();
- mToolbar.showProgressBar();
+ mToolbar.getProgressBar().setVisible(true);
Fragment fragment = getSupportFragmentManager()
.findFragmentByTag(PAIRING_CODE_FRAGMENT_TAG);
if (fragment != null) {
diff --git a/src/com/android/car/companiondevicesupport/feature/RemoteFeature.java b/src/com/android/car/companiondevicesupport/feature/RemoteFeature.java
index 6fa3256..d4551e5 100644
--- a/src/com/android/car/companiondevicesupport/feature/RemoteFeature.java
+++ b/src/com/android/car/companiondevicesupport/feature/RemoteFeature.java
@@ -18,6 +18,7 @@ package com.android.car.companiondevicesupport.feature;
import static com.android.car.connecteddevice.util.SafeLog.logd;
import static com.android.car.connecteddevice.util.SafeLog.loge;
+import static com.android.car.connecteddevice.util.SafeLog.logw;
import android.annotation.CallSuper;
import android.annotation.NonNull;
@@ -26,7 +27,9 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
+import android.os.Handler;
import android.os.IBinder;
+import android.os.Looper;
import android.os.ParcelUuid;
import android.os.RemoteException;
import android.os.UserHandle;
@@ -39,6 +42,7 @@ import com.android.car.companiondevicesupport.api.external.IDeviceAssociationCal
import com.android.car.companiondevicesupport.api.external.IDeviceCallback;
import com.android.car.companiondevicesupport.service.CompanionDeviceSupportService;
+import java.time.Duration;
import java.util.List;
/**
@@ -50,12 +54,23 @@ public abstract class RemoteFeature {
private static final String TAG = "RemoteFeature";
+ private static final String SERVICE_PACKAGE_NAME = "com.android.car.companiondevicesupport";
+
+ private static final String FULLY_QUALIFIED_SERVICE_NAME = SERVICE_PACKAGE_NAME
+ + ".service.CompanionDeviceSupportService";
+
+ private static final Duration BIND_RETRY_DURATION = Duration.ofSeconds(1);
+
+ private static final int MAX_BIND_ATTEMPTS = 3;
+
private final Context mContext;
private final ParcelUuid mFeatureId;
private IConnectedDeviceManager mConnectedDeviceManager;
+ private int mBindAttempts;
+
public RemoteFeature(@NonNull Context context, @NonNull ParcelUuid featureId) {
mContext = context;
mFeatureId = featureId;
@@ -64,10 +79,8 @@ public abstract class RemoteFeature {
/** Start setup process and begin binding to {@link CompanionDeviceSupportService}. */
@CallSuper
public void start() {
- Intent intent = new Intent(mContext, CompanionDeviceSupportService.class);
- intent.setAction(CompanionDeviceSupportService.ACTION_BIND_CONNECTED_DEVICE_MANAGER);
- mContext.bindServiceAsUser(intent, mServiceConnection, Context.BIND_AUTO_CREATE,
- UserHandle.SYSTEM);
+ mBindAttempts = 0;
+ bindToService();
}
/** Called when the hosting service is being destroyed. Cleans up internal feature logic. */
@@ -211,6 +224,25 @@ public abstract class RemoteFeature {
/** Called when an {@link AssociatedDevice} is updated for the given user. */
protected void onAssociatedDeviceUpdated(@NonNull AssociatedDevice device) { }
+ private void bindToService() {
+ Intent intent = new Intent();
+ intent.setComponent(new ComponentName(SERVICE_PACKAGE_NAME, FULLY_QUALIFIED_SERVICE_NAME));
+ intent.setAction(CompanionDeviceSupportService.ACTION_BIND_CONNECTED_DEVICE_MANAGER);
+ boolean success = mContext.bindServiceAsUser(intent, mServiceConnection,
+ Context.BIND_AUTO_CREATE, UserHandle.SYSTEM);
+ if (!success) {
+ mBindAttempts++;
+ if (mBindAttempts > MAX_BIND_ATTEMPTS) {
+ loge(TAG, "Failed to bind to CompanionDeviceSupportService after " + mBindAttempts
+ + " attempts. Aborting.");
+ return;
+ }
+ logw(TAG, "Unable to bind to CompanionDeviceSupportService. Trying again.");
+ new Handler(Looper.getMainLooper()).postDelayed(this::bindToService,
+ BIND_RETRY_DURATION.toMillis());
+ }
+ }
+
private final ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
diff --git a/src/com/android/car/companiondevicesupport/feature/calendarsync/CalendarCleaner.java b/src/com/android/car/companiondevicesupport/feature/calendarsync/CalendarCleaner.java
index 9bedfab..9b2885c 100644
--- a/src/com/android/car/companiondevicesupport/feature/calendarsync/CalendarCleaner.java
+++ b/src/com/android/car/companiondevicesupport/feature/calendarsync/CalendarCleaner.java
@@ -60,6 +60,12 @@ class CalendarCleaner {
CalendarContract.ACCOUNT_TYPE_LOCAL
},
null);
+ if (cursor == null) {
+ // This means the content provider crashed and the Activity Manager will kill this
+ // process shortly afterwards.
+ return;
+ }
+
ArrayList<ContentProviderOperation> deleteOps = new ArrayList<>();
while (cursor.moveToNext()) {
buildCalendarDeletionOps(cursor.getString(0), deleteOps);
@@ -89,6 +95,11 @@ class CalendarCleaner {
EVENTS_CALENDAR_ID_SELECTION,
calendarIdArgs,
null);
+ if (cursor == null) {
+ // This means the content provider crashed and the Activity Manager will kill this
+ // process shortly afterwards.
+ return;
+ }
while (cursor.moveToNext()) {
operations.add(
diff --git a/src/com/android/car/companiondevicesupport/feature/calendarsync/CalendarImporter.java b/src/com/android/car/companiondevicesupport/feature/calendarsync/CalendarImporter.java
index c05fc4d..5416a15 100644
--- a/src/com/android/car/companiondevicesupport/feature/calendarsync/CalendarImporter.java
+++ b/src/com/android/car/companiondevicesupport/feature/calendarsync/CalendarImporter.java
@@ -75,11 +75,8 @@ class CalendarImporter {
*/
void importCalendars(@NonNull Calendars calendars) {
for (Calendar calendar : calendars.getCalendarList()) {
- logd(
- TAG,
- String.format(
- "Import Calendar[title=%s, uuid=%s] with %d events",
- calendar.getTitle(), calendar.getUuid(), calendar.getEventCount()));
+ logd(TAG, String.format("Import Calendar[title=%s, uuid=%s] with %d events",
+ calendar.getTitle(), calendar.getUuid(), calendar.getEventCount()));
if (calendar.getEventCount() == 0) {
logd(TAG, "Ignore calendar- has no events");
continue;
@@ -102,21 +99,23 @@ class CalendarImporter {
/**
* Provides the calendar identifier used by the system.
- *
- * <p>This identifier is system-specific and is used to know to which calendar an events
- * belongs.
+ * <p>
+ * This identifier is system-specific and is used to know to which calendar an events belongs.
*
* @param uuid The UUID of the calendar to find.
* @return The identifier of the calendar or {@link #INVALID_CALENDAR_ID} if nothing was found.
*/
int findCalendar(@NonNull final String uuid) {
- Cursor cursor =
- mContentResolver.query(
- CalendarContract.Calendars.CONTENT_URI,
- new String[]{CalendarContract.Calendars._ID},
- CalendarContract.Calendars._SYNC_ID + " = ?",
- new String[]{uuid},
- null);
+ Cursor cursor = mContentResolver.query(
+ CalendarContract.Calendars.CONTENT_URI,
+ new String[]{
+ CalendarContract.Calendars._ID
+ },
+ CalendarContract.Calendars._SYNC_ID + " = ?",
+ new String[]{
+ uuid
+ },
+ null);
if (cursor.getCount() == 0) {
return INVALID_CALENDAR_ID;
@@ -150,8 +149,7 @@ class CalendarImporter {
Uri uri = insertContent(CalendarContract.Calendars.CONTENT_URI, values);
Matcher matcher = CALENDAR_ID_PATTERN.matcher(uri.toString());
- return matcher.matches()
- ? Integer.valueOf(matcher.group(CALENDAR_ID_GROUP))
+ return matcher.matches() ? Integer.valueOf(matcher.group(CALENDAR_ID_GROUP))
: findCalendar(calendar.getUuid());
}
@@ -167,11 +165,10 @@ class CalendarImporter {
SECONDS.toMillis(event.getStartDate().getSeconds()));
values.put(CalendarContract.Events.DTEND,
SECONDS.toMillis(event.getEndDate().getSeconds()));
- values.put(CalendarContract.Events.ALL_DAY, event.getIsAllDay() ? 1 : 0);
values.put(CalendarContract.Events.EVENT_LOCATION, event.getLocation());
values.put(CalendarContract.Events.ORGANIZER, event.getOrganizer());
+ values.put(CalendarContract.Events.ALL_DAY, event.getIsAllDay() ? 1 : 0);
- // TODO(b/150335852) Store CreationDate and LastModifiedDate
if (event.hasColor()) {
values.put(CalendarContract.Events.EVENT_COLOR, event.getColor().getArgb());
}
@@ -210,16 +207,14 @@ class CalendarImporter {
values.put(CalendarContract.Attendees.ATTENDEE_EMAIL, attendee.getEmail());
values.put(CalendarContract.Attendees.ATTENDEE_TYPE,
convertAttendeeType(attendee.getType()));
- values.put(
- CalendarContract.Attendees.ATTENDEE_STATUS,
+ values.put(CalendarContract.Attendees.ATTENDEE_STATUS,
convertAttendeeStatus(attendee.getStatus()));
values.put(CalendarContract.Attendees.EVENT_ID, eventId);
- operations.add(
- ContentProviderOperation.newInsert(
- appendQueryParameters(CalendarContract.Attendees.CONTENT_URI))
- .withValues(values)
- .build());
+ operations.add(ContentProviderOperation.newInsert(
+ appendQueryParameters(CalendarContract.Attendees.CONTENT_URI))
+ .withValues(values)
+ .build());
}
try {
@@ -236,8 +231,8 @@ class CalendarImporter {
private Uri appendQueryParameters(@NonNull Uri contentUri) {
Uri.Builder builder = contentUri.buildUpon();
builder.appendQueryParameter(CalendarContract.Calendars.ACCOUNT_NAME, DEFAULT_ACCOUNT_NAME);
- builder.appendQueryParameter(
- CalendarContract.Calendars.ACCOUNT_TYPE, CalendarContract.ACCOUNT_TYPE_LOCAL);
+ builder.appendQueryParameter(CalendarContract.Calendars.ACCOUNT_TYPE,
+ CalendarContract.ACCOUNT_TYPE_LOCAL);
builder.appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true");
return builder.build();
}
diff --git a/src/com/android/car/companiondevicesupport/feature/notificationmsg/NotificationMsgDelegate.java b/src/com/android/car/companiondevicesupport/feature/notificationmsg/NotificationMsgDelegate.java
index 6e82fb4..9c00b93 100644
--- a/src/com/android/car/companiondevicesupport/feature/notificationmsg/NotificationMsgDelegate.java
+++ b/src/com/android/car/companiondevicesupport/feature/notificationmsg/NotificationMsgDelegate.java
@@ -22,25 +22,30 @@ import static com.android.car.connecteddevice.util.SafeLog.logw;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
+import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.AudioAttributes;
import android.provider.Settings;
+import androidx.annotation.Nullable;
+
import com.android.car.companiondevicesupport.api.external.CompanionDevice;
import com.android.car.messenger.NotificationMsgProto.NotificationMsg.Action;
-import com.android.car.messenger.NotificationMsgProto.NotificationMsg.MapEntry;
+import com.android.car.messenger.NotificationMsgProto.NotificationMsg.AvatarIconSync;
import com.android.car.messenger.NotificationMsgProto.NotificationMsg.CarToPhoneMessage;
+import com.android.car.messenger.NotificationMsgProto.NotificationMsg.ClearAppDataRequest;
import com.android.car.messenger.NotificationMsgProto.NotificationMsg.ConversationNotification;
+import com.android.car.messenger.NotificationMsgProto.NotificationMsg.MapEntry;
import com.android.car.messenger.NotificationMsgProto.NotificationMsg.MessagingStyleMessage;
import com.android.car.messenger.NotificationMsgProto.NotificationMsg.PhoneToCarMessage;
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.MessageKey;
import com.android.car.messenger.common.ProjectionStateListener;
import com.android.car.messenger.common.SenderKey;
import com.android.car.messenger.common.Utils;
+import com.android.internal.annotations.VisibleForTesting;
import java.util.HashMap;
import java.util.List;
@@ -53,28 +58,36 @@ import java.util.Map;
public class NotificationMsgDelegate extends BaseNotificationDelegate {
private static final String TAG = "NotificationMsgDelegate";
- /** The different {@link PhoneToCarMessage} Message Types. **/
- private static final String NEW_CONVERSATION_MESSAGE_TYPE = "NEW_CONVERSATION";
- private static final String NEW_MESSAGE_MESSAGE_TYPE = "NEW_MESSAGE";
- private static final String ACTION_STATUS_UPDATE_MESSAGE_TYPE = "ACTION_STATUS_UPDATE";
- private static final String OTHER_MESSAGE_TYPE = "OTHER";
/** Key for the Reply string in a {@link MapEntry}. **/
- private static final String REPLY_KEY = "REPLY";
+ protected static final String REPLY_KEY = "REPLY";
+ /**
+ * Value for {@link ClearAppDataRequest#getMessagingAppPackageName()}, representing
+ * when all messaging applications' data should be removed.
+ */
+ protected static final String REMOVE_ALL_APP_DATA = "ALL";
private static final AudioAttributes AUDIO_ATTRIBUTES = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_NOTIFICATION)
.build();
private Map<String, NotificationChannelWrapper> mAppNameToChannel = new HashMap<>();
+
+ /**
+ * The Bluetooth Device address of the connected device. NOTE: this is NOT the same as
+ * {@link CompanionDevice#getDeviceId()}.
+ */
private String mConnectedDeviceBluetoothAddress;
+ /**
+ * Maps a Bitmap of a sender's Large Icon to the sender's unique key for 1-1 conversations.
+ **/
+ protected final Map<SenderKey, Bitmap> mOneOnOneConversationAvatarMap = new HashMap<>();
/** Tracks whether a projection application is active in the foreground. **/
private ProjectionStateListener mProjectionStateListener;
- public NotificationMsgDelegate(Context context, String className) {
- super(context, className, /* useLetterTile */ false);
+ public NotificationMsgDelegate(Context context) {
+ super(context, /* useLetterTile */ false);
mProjectionStateListener = new ProjectionStateListener(context);
- mProjectionStateListener.start();
}
public void onMessageReceived(CompanionDevice device, PhoneToCarMessage message) {
@@ -83,19 +96,28 @@ public class NotificationMsgDelegate extends BaseNotificationDelegate {
switch (message.getMessageDataCase()) {
case CONVERSATION:
initializeNewConversation(device, message.getConversation(), notificationKey);
+ return;
case MESSAGE:
initializeNewMessage(device.getDeviceId(), message.getMessage(), notificationKey);
+ return;
case STATUS_UPDATE:
// TODO (b/144924164): implement Action Request tracking logic.
+ return;
case AVATAR_ICON_SYNC:
- // TODO(b/148412881): implement avatar icon sync.
+ storeIcon(new ConversationKey(device.getDeviceId(), notificationKey),
+ message.getAvatarIconSync());
+ return;
case PHONE_METADATA:
mConnectedDeviceBluetoothAddress =
message.getPhoneMetadata().getBluetoothDeviceAddress();
+ return;
case CLEAR_APP_DATA_REQUEST:
- // TODO(b/150326327): implement removal behavior.
+ clearAppData(device.getDeviceId(),
+ message.getClearAppDataRequest().getMessagingAppPackageName());
+ return;
case FEATURE_ENABLED_STATE_CHANGE:
// TODO(b/150326327): implement enabled state change behavior.
+ return;
case MESSAGEDATA_NOT_SET:
default:
logw(TAG, "PhoneToCarMessage: message data not set!");
@@ -103,8 +125,7 @@ public class NotificationMsgDelegate extends BaseNotificationDelegate {
}
protected CarToPhoneMessage dismiss(ConversationKey convoKey) {
- clearNotifications(key -> key.equals(convoKey));
- excludeFromNotification(convoKey);
+ super.dismissInternal(convoKey);
// TODO(b/144924164): add a request id to the action.
Action action = Action.newBuilder()
.setActionName(Action.ActionName.DISMISS)
@@ -129,18 +150,6 @@ public class NotificationMsgDelegate extends BaseNotificationDelegate {
.build();
}
- /**
- * Excludes messages from a notification so that the messages are not shown to the user once
- * the notification gets updated with newer messages.
- */
- private void excludeFromNotification(ConversationKey convoKey) {
- ConversationNotificationInfo info = mNotificationInfos.get(convoKey);
- for (MessageKey key : info.mMessageKeys) {
- Message message = mMessages.get(key);
- message.excludeFromNotification();
- }
- }
-
protected CarToPhoneMessage reply(ConversationKey convoKey, String message) {
// TODO(b/144924164): add a request id to the action.
MapEntry entry = MapEntry.newBuilder()
@@ -162,7 +171,8 @@ public class NotificationMsgDelegate extends BaseNotificationDelegate {
// Erase all the notifications and local data, so that no user data stays on the device
// after the feature is stopped.
cleanupMessagesAndNotifications(key -> true);
- mProjectionStateListener.stop();
+ mProjectionStateListener.destroy();
+ mOneOnOneConversationAvatarMap.clear();
mAppNameToChannel.clear();
mConnectedDeviceBluetoothAddress = null;
}
@@ -170,34 +180,45 @@ public class NotificationMsgDelegate extends BaseNotificationDelegate {
protected void onDeviceDisconnected(String deviceId) {
mConnectedDeviceBluetoothAddress = null;
cleanupMessagesAndNotifications(key -> key.matches(deviceId));
+ mOneOnOneConversationAvatarMap.entrySet().removeIf(
+ conversationKey -> conversationKey.getKey().matches(deviceId));
}
private void initializeNewConversation(CompanionDevice device,
ConversationNotification notification, String notificationKey) {
String deviceAddress = device.getDeviceId();
ConversationKey convoKey = new ConversationKey(deviceAddress, notificationKey);
- if (mNotificationInfos.containsKey(convoKey)) {
- logw(TAG, "Conversation already exists! " + notificationKey);
- }
if (!Utils.isValidConversationNotification(notification, /* isShallowCheck= */ false)) {
logd(TAG, "Failed to initialize new Conversation, object missing required fields");
return;
}
- ConversationNotificationInfo convoInfo = ConversationNotificationInfo.
- createConversationNotificationInfo(device.getDeviceName(), device.getDeviceId(),
- notification, notificationKey);
- mNotificationInfos.put(convoKey, convoInfo);
+ ConversationNotificationInfo convoInfo;
+ if (mNotificationInfos.containsKey(convoKey)) {
+ logw(TAG, "Conversation already exists! " + notificationKey);
+ convoInfo = mNotificationInfos.get(convoKey);
+ } else {
+ convoInfo = ConversationNotificationInfo.
+ createConversationNotificationInfo(device.getDeviceName(), device.getDeviceId(),
+ notification, notificationKey);
+ mNotificationInfos.put(convoKey, convoInfo);
+ }
+
String appDisplayName = convoInfo.getAppDisplayName();
List<MessagingStyleMessage> messages =
notification.getMessagingStyle().getMessagingStyleMsgList();
+ MessagingStyleMessage latestMessage = messages.get(0);
for (MessagingStyleMessage messagingStyleMessage : messages) {
createNewMessage(deviceAddress, messagingStyleMessage, convoKey);
+ if (messagingStyleMessage.getTimestamp() > latestMessage.getTimestamp()) {
+ latestMessage = messagingStyleMessage;
+ }
}
- postNotification(convoKey, convoInfo, getChannelId(appDisplayName));
+ postNotification(convoKey, convoInfo, getChannelId(appDisplayName),
+ getAvatarIcon(convoKey, latestMessage));
}
private void initializeNewMessage(String deviceAddress,
@@ -216,7 +237,41 @@ public class NotificationMsgDelegate extends BaseNotificationDelegate {
createNewMessage(deviceAddress, messagingStyleMessage, convoKey);
ConversationNotificationInfo convoInfo = mNotificationInfos.get(convoKey);
- postNotification(convoKey, convoInfo, getChannelId(convoInfo.getAppDisplayName()));
+ postNotification(convoKey, convoInfo, getChannelId(convoInfo.getAppDisplayName()),
+ getAvatarIcon(convoKey, messagingStyleMessage));
+ }
+
+ @Nullable
+ private Bitmap getAvatarIcon(ConversationKey convoKey, MessagingStyleMessage message) {
+ ConversationNotificationInfo notificationInfo = mNotificationInfos.get(convoKey);
+ if (!notificationInfo.isGroupConvo()) {
+ return mOneOnOneConversationAvatarMap.get(
+ SenderKey.createSenderKey(convoKey, message.getSender()));
+ } else if (message.getSender().getAvatar() != null
+ || !message.getSender().getAvatar().isEmpty()) {
+ byte[] iconArray = message.getSender().getAvatar().toByteArray();
+ return BitmapFactory.decodeByteArray(iconArray, 0, iconArray.length);
+ }
+ return null;
+ }
+
+ private void storeIcon(ConversationKey convoKey, AvatarIconSync iconSync) {
+ if (!Utils.isValidAvatarIconSync(iconSync) || !mNotificationInfos.containsKey(convoKey)) {
+ logw(TAG, "storeIcon: invalid AvatarIconSync obj or no conversation found.");
+ return;
+ }
+ if (mNotificationInfos.get(convoKey).isGroupConvo()) {
+ return;
+ }
+ byte[] iconArray = iconSync.getPerson().getAvatar().toByteArray();
+ Bitmap bitmap = BitmapFactory.decodeByteArray(iconArray, /* offset= */ 0, iconArray.length);
+ if (bitmap != null) {
+ mOneOnOneConversationAvatarMap.put(
+ SenderKey.createSenderKey(convoKey, iconSync.getPerson()),
+ bitmap);
+ } else {
+ logw(TAG, "storeIcon: Bitmap could not be created from byteArray");
+ }
}
private String getChannelId(String appDisplayName) {
@@ -224,24 +279,34 @@ public class NotificationMsgDelegate extends BaseNotificationDelegate {
mAppNameToChannel.put(appDisplayName,
new NotificationChannelWrapper(appDisplayName));
}
- boolean isProjectionActive = mProjectionStateListener.isProjectionInActiveForeground(
- mConnectedDeviceBluetoothAddress);
- return mAppNameToChannel.get(appDisplayName).getChannelId(isProjectionActive);
+ return mAppNameToChannel.get(appDisplayName).getChannelId(
+ mProjectionStateListener.isProjectionInActiveForeground(
+ mConnectedDeviceBluetoothAddress));
}
private void createNewMessage(String deviceAddress, MessagingStyleMessage messagingStyleMessage,
ConversationKey convoKey) {
- String appDisplayName = mNotificationInfos.get(convoKey).getAppDisplayName();
+ String appPackageName = mNotificationInfos.get(convoKey).getAppPackageName();
Message message = Message.parseFromMessage(deviceAddress, messagingStyleMessage,
- appDisplayName);
+ SenderKey.createSenderKey(convoKey, messagingStyleMessage.getSender()));
addMessageToNotificationInfo(message, convoKey);
- SenderKey senderKey = message.getSenderKey();
- if (!mSenderLargeIcons.containsKey(senderKey)
- && messagingStyleMessage.getSender().getAvatar() != null) {
- byte[] iconArray = messagingStyleMessage.getSender().getAvatar().toByteArray();
- mSenderLargeIcons.put(senderKey,
- BitmapFactory.decodeByteArray(iconArray, 0, iconArray.length));
+ AvatarIconSync iconSync = AvatarIconSync.newBuilder()
+ .setPerson(messagingStyleMessage.getSender())
+ .setMessagingAppPackageName(appPackageName)
+ .build();
+ storeIcon(convoKey, iconSync);
+ }
+
+ private void clearAppData(String deviceId, String packageName) {
+ if (!packageName.equals(REMOVE_ALL_APP_DATA)) {
+ // Clearing data for specific package names is not supported since this use case
+ // is not needed right now.
+ logw(TAG, "clearAppData not supported for arg: " + packageName);
+ return;
}
+ cleanupMessagesAndNotifications(key -> key.matches(deviceId));
+ mOneOnOneConversationAvatarMap.entrySet().removeIf(
+ conversationKey -> conversationKey.getKey().matches(deviceId));
}
/** Creates notification channels per unique messaging application. **/
@@ -297,4 +362,14 @@ public class NotificationMsgDelegate extends BaseNotificationDelegate {
return ++NEXT_NOTIFICATION_CHANNEL_ID;
}
}
+
+ @VisibleForTesting
+ void setNotificationManager(NotificationManager manager) {
+ mNotificationManager = manager;
+ }
+
+ @VisibleForTesting
+ void setProjectionStateListener(ProjectionStateListener listener) {
+ mProjectionStateListener = listener;
+ }
}
diff --git a/src/com/android/car/companiondevicesupport/feature/notificationmsg/NotificationMsgFeature.java b/src/com/android/car/companiondevicesupport/feature/notificationmsg/NotificationMsgFeature.java
index 925aa0c..9e8d4a3 100644
--- a/src/com/android/car/companiondevicesupport/feature/notificationmsg/NotificationMsgFeature.java
+++ b/src/com/android/car/companiondevicesupport/feature/notificationmsg/NotificationMsgFeature.java
@@ -63,7 +63,7 @@ public class NotificationMsgFeature extends RemoteFeature {
public void stop() {
// Erase all the notifications and local data, so that no user data stays on the device
// after the feature is stopped.
- mNotificationMsgDelegate.cleanupMessagesAndNotifications(key -> true);
+ mNotificationMsgDelegate.onDestroy();
super.stop();
}
diff --git a/src/com/android/car/companiondevicesupport/feature/notificationmsg/NotificationMsgService.java b/src/com/android/car/companiondevicesupport/feature/notificationmsg/NotificationMsgService.java
index 8d7e694..c75d6af 100644
--- a/src/com/android/car/companiondevicesupport/feature/notificationmsg/NotificationMsgService.java
+++ b/src/com/android/car/companiondevicesupport/feature/notificationmsg/NotificationMsgService.java
@@ -75,7 +75,7 @@ public class NotificationMsgService extends Service {
super.onCreate();
mNotificationManager = getSystemService(NotificationManager.class);
- mNotificationMsgDelegate = new NotificationMsgDelegate(this, this.getClass().getName());
+ mNotificationMsgDelegate = new NotificationMsgDelegate(this);
mNotificationMsgFeature = new NotificationMsgFeature(this, mNotificationMsgDelegate);
mNotificationMsgFeature.start();
sendServiceRunningNotification();
@@ -85,7 +85,6 @@ public class NotificationMsgService extends Service {
public void onDestroy() {
super.onDestroy();
mNotificationMsgFeature.stop();
- mNotificationMsgDelegate.onDestroy();
}
@Override
diff --git a/src/com/android/car/companiondevicesupport/feature/trust/ui/TrustedDeviceActivity.java b/src/com/android/car/companiondevicesupport/feature/trust/ui/TrustedDeviceActivity.java
index fc9b227..e53e821 100644
--- a/src/com/android/car/companiondevicesupport/feature/trust/ui/TrustedDeviceActivity.java
+++ b/src/com/android/car/companiondevicesupport/feature/trust/ui/TrustedDeviceActivity.java
@@ -21,6 +21,8 @@ import static com.android.car.companiondevicesupport.activity.AssociationActivit
import static com.android.car.connecteddevice.util.SafeLog.logd;
import static com.android.car.connecteddevice.util.SafeLog.loge;
import static com.android.car.connecteddevice.util.SafeLog.logw;
+import static com.android.car.ui.core.CarUi.requireToolbar;
+import static com.android.car.ui.toolbar.Toolbar.State.SUBPAGE;
import android.annotation.Nullable;
import android.app.AlertDialog;
@@ -53,7 +55,7 @@ import com.android.car.companiondevicesupport.api.internal.trust.ITrustedDeviceM
import com.android.car.companiondevicesupport.api.internal.trust.TrustedDevice;
import com.android.car.companiondevicesupport.feature.trust.TrustedDeviceConstants;
import com.android.car.companiondevicesupport.feature.trust.TrustedDeviceManagerService;
-import com.android.car.ui.toolbar.Toolbar;
+import com.android.car.ui.toolbar.ToolbarController;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -104,7 +106,7 @@ public class TrustedDeviceActivity extends FragmentActivity {
private ITrustedDeviceManager mTrustedDeviceManager;
- private Toolbar mToolbar;
+ private ToolbarController mToolbar;
private TrustedDeviceViewModel mModel;
@@ -114,9 +116,10 @@ public class TrustedDeviceActivity extends FragmentActivity {
setContentView(R.layout.base_activity);
observeViewModel();
resumePreviousState(savedInstanceState);
- mToolbar = findViewById(R.id.toolbar);
+ mToolbar = requireToolbar(this);
+ mToolbar.setState(SUBPAGE);
mToolbar.setTitle(R.string.trusted_device_feature_title);
- mToolbar.showProgressBar();
+ mToolbar.getProgressBar().setVisible(true);
mIsScreenLockNewlyCreated.set(false);
mIsStartedForEnrollment.set(false);
mHasPendingCredential.set(false);
@@ -375,7 +378,7 @@ public class TrustedDeviceActivity extends FragmentActivity {
}
private void showTrustedDeviceDetailFragment(AssociatedDevice device) {
- mToolbar.hideProgressBar();
+ mToolbar.getProgressBar().setVisible(false);
TrustedDeviceDetailFragment fragment = TrustedDeviceDetailFragment.newInstance(device);
getSupportFragmentManager().beginTransaction()
.replace(R.id.fragment_container, fragment, DEVICE_DETAIL_FRAGMENT_TAG)
diff --git a/tests/unit/AndroidManifest.xml b/tests/unit/AndroidManifest.xml
index b121cf9..7447fe0 100644
--- a/tests/unit/AndroidManifest.xml
+++ b/tests/unit/AndroidManifest.xml
@@ -26,16 +26,8 @@
<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS"/>
<application android:testOnly="true"
- android:debuggable="true"
- xmlns:tools="http://schemas.android.com/tools">
+ android:debuggable="true">
<uses-library android:name="android.test.runner"/>
- <!-- Workaround for b/113294940 -->
- <provider
- android:name="androidx.lifecycle.ProcessLifecycleOwnerInitializer"
- tools:replace="android:authorities"
- android:authorities="${applicationId}.lifecycle"
- android:exported="false"
- android:multiprocess="true" />
</application>
<instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
diff --git a/tests/unit/src/com/android/car/companiondevicesupport/feature/calendarsync/CalendarCleanerTest.java b/tests/unit/src/com/android/car/companiondevicesupport/feature/calendarsync/CalendarCleanerTest.java
index 536f098..8f65b81 100644
--- a/tests/unit/src/com/android/car/companiondevicesupport/feature/calendarsync/CalendarCleanerTest.java
+++ b/tests/unit/src/com/android/car/companiondevicesupport/feature/calendarsync/CalendarCleanerTest.java
@@ -20,6 +20,7 @@ import static android.provider.CalendarContract.AUTHORITY;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
@@ -165,6 +166,28 @@ public class CalendarCleanerTest {
verifyNoMoreInteractions(mContentProvider);
}
+ @Test
+ public void eraseCalendar_failingQuery() {
+ when(mContentResolver.query(eq(Events.CONTENT_URI), any(), any(), any(),
+ eq(null))).thenReturn(null);
+ try {
+ mCalendarCleaner.eraseCalendar(CALENDAR_IDENTIFIER);
+ } catch (NullPointerException e) {
+ fail();
+ }
+ }
+
+ @Test
+ public void eraseCalendars_failingQuery() {
+ when(mContentResolver.query(eq(Events.CONTENT_URI), any(), any(), any(),
+ eq(null))).thenReturn(null);
+ try {
+ mCalendarCleaner.eraseCalendars();
+ } catch (NullPointerException e) {
+ fail();
+ }
+ }
+
// --- Helpers ---
private ArgumentMatcher<String[]> createStringArrayMatcher(String expectedArg) {
@@ -181,7 +204,7 @@ public class CalendarCleanerTest {
private void verifyDelete(Uri uri, String selection, String selectionArg) {
verify(mContentProvider).delete(
eq(uri),
- eq(String.format("%s = ?", selection)),
- argThat(createStringArrayMatcher(selectionArg)));
+ argThat(selectionBundleMatcher(
+ String.format("%s = ?", selection), new String[]{selectionArg})));
}
}
diff --git a/tests/unit/src/com/android/car/companiondevicesupport/feature/calendarsync/CalendarImporterTest.java b/tests/unit/src/com/android/car/companiondevicesupport/feature/calendarsync/CalendarImporterTest.java
index a2c2cad..97dfeb0 100644
--- a/tests/unit/src/com/android/car/companiondevicesupport/feature/calendarsync/CalendarImporterTest.java
+++ b/tests/unit/src/com/android/car/companiondevicesupport/feature/calendarsync/CalendarImporterTest.java
@@ -113,15 +113,16 @@ public class CalendarImporterTest {
when(mContentProvider.insert(
argThat(startsWithUriMatcher(CalendarContract.Attendees.CONTENT_URI)),
+ any(),
any()))
.thenReturn(CalendarContract.Attendees.CONTENT_URI);
- }
- @Test
- public void findCalendar() {
when(mCursor.getCount()).thenReturn(1);
when(mCursor.getString(eq(0))).thenReturn(CALENDAR_ID);
+ }
+ @Test
+ public void findCalendar() {
assertEquals(CALENDAR_ID,
String.valueOf(mCalendarImporter.findCalendar(CALENDAR_UNIQUE_ID)));
@@ -141,9 +142,6 @@ public class CalendarImporterTest {
@Test
public void importCalendarsWithExistingCalendar() throws Exception {
- when(mCursor.getCount()).thenReturn(1);
- when(mCursor.getString(eq(0))).thenReturn(CALENDAR_ID);
-
ArgumentCaptor<ArrayList<ContentProviderOperation>> batchOpsCaptor =
ArgumentCaptor.forClass(ArrayList.class);
@@ -176,6 +174,27 @@ public class CalendarImporterTest {
}
@Test
+ public void importCalendarAndVerifyAllDayEvent() throws Exception {
+ Event.Builder allDayEventBuilder = newEvent("UID_1", "Event A", "", "here", 1, 2,
+ "Europe/Berlin", null);
+ allDayEventBuilder.setIsAllDay(true);
+
+ mCalendarsProto = Calendars.newBuilder()
+ .addCalendar(Calendar.newBuilder()
+ .setUuid(CALENDAR_UNIQUE_ID)
+ .setTitle("calendar one")
+ .addEvent(allDayEventBuilder))
+ .build();
+
+ mCalendarImporter.importCalendars(mCalendarsProto);
+
+ // Verify event insertion.
+ verify(mContentResolver).insert(
+ argThat(startsWithUriMatcher(CalendarContract.Events.CONTENT_URI)),
+ argThat(argument -> argument.getAsInteger(Events.ALL_DAY) == 1));
+ }
+
+ @Test
public void convertAttendeeStatus() {
assertEquals(Attendees.ATTENDEE_STATUS_NONE,
CalendarImporter.convertAttendeeStatus(Attendee.Status.NONE_STATUS));
@@ -230,7 +249,8 @@ public class CalendarImporterTest {
argument.getAsLong(Events.DTEND).equals(
SECONDS.toMillis(event.getEndDate().getSeconds())) &&
argument.getAsString(Events.EVENT_LOCATION).equals(event.getLocation()) &&
- argument.getAsString(Events.ORGANIZER).equals(event.getOrganizer());
+ argument.getAsString(Events.ORGANIZER).equals(event.getOrganizer()) &&
+ argument.getAsInteger(Events.ALL_DAY) == (event.getIsAllDay() ? 1 : 0);
};
}
@@ -277,6 +297,7 @@ public class CalendarImporterTest {
private void verifyAttendeeInsert(Attendee attendee) {
verify(mContentProvider).insert(
argThat(startsWithUriMatcher(CalendarContract.Attendees.CONTENT_URI)),
- argThat(attendeeMatcher(attendee)));
+ argThat(attendeeMatcher(attendee)),
+ any());
}
}
diff --git a/tests/unit/src/com/android/car/companiondevicesupport/feature/notificationmsg/NotificationMsgDelegateTest.java b/tests/unit/src/com/android/car/companiondevicesupport/feature/notificationmsg/NotificationMsgDelegateTest.java
new file mode 100644
index 0000000..e41df2c
--- /dev/null
+++ b/tests/unit/src/com/android/car/companiondevicesupport/feature/notificationmsg/NotificationMsgDelegateTest.java
@@ -0,0 +1,785 @@
+/*
+ * 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.companiondevicesupport.feature.notificationmsg;
+
+
+import static android.app.NotificationManager.IMPORTANCE_HIGH;
+import static android.app.NotificationManager.IMPORTANCE_LOW;
+
+import static androidx.core.app.NotificationCompat.CATEGORY_MESSAGE;
+
+import static com.android.car.messenger.NotificationMsgProto.NotificationMsg.Action.ActionName.DISMISS;
+import static com.android.car.messenger.NotificationMsgProto.NotificationMsg.Action.ActionName.MARK_AS_READ;
+import static com.android.car.messenger.NotificationMsgProto.NotificationMsg.Action.ActionName.REPLY;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.drawable.Icon;
+
+import androidx.core.app.NotificationCompat;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.car.companiondevicesupport.api.external.CompanionDevice;
+import com.android.car.messenger.NotificationMsgProto.NotificationMsg;
+import com.android.car.messenger.NotificationMsgProto.NotificationMsg.Action;
+import com.android.car.messenger.NotificationMsgProto.NotificationMsg.AvatarIconSync;
+import com.android.car.messenger.NotificationMsgProto.NotificationMsg.CarToPhoneMessage;
+import com.android.car.messenger.NotificationMsgProto.NotificationMsg.ClearAppDataRequest;
+import com.android.car.messenger.NotificationMsgProto.NotificationMsg.ConversationNotification;
+import com.android.car.messenger.NotificationMsgProto.NotificationMsg.MessagingStyle;
+import com.android.car.messenger.NotificationMsgProto.NotificationMsg.MessagingStyleMessage;
+import com.android.car.messenger.NotificationMsgProto.NotificationMsg.Person;
+import com.android.car.messenger.NotificationMsgProto.NotificationMsg.PhoneMetadata;
+import com.android.car.messenger.NotificationMsgProto.NotificationMsg.PhoneToCarMessage;
+import com.android.car.messenger.common.ConversationKey;
+import com.android.car.messenger.common.ProjectionStateListener;
+import com.android.car.messenger.common.SenderKey;
+import com.android.car.protobuf.ByteString;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.ByteArrayOutputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+@RunWith(AndroidJUnit4.class)
+public class NotificationMsgDelegateTest {
+ private static final String NOTIFICATION_KEY_1 = "notification_key_1";
+ private static final String NOTIFICATION_KEY_2 = "notification_key_2";
+
+ private static final String COMPANION_DEVICE_ID = "sampleId";
+ private static final String COMPANION_DEVICE_NAME = "sampleName";
+ private static final String DEVICE_ADDRESS = UUID.randomUUID().toString();
+ private static final String BT_DEVICE_ADDRESS = UUID.randomUUID().toString();
+
+ private static final String MESSAGING_APP_NAME = "Messaging App";
+ private static final String MESSAGING_PACKAGE_NAME = "com.android.messaging.app";
+ private static final String CONVERSATION_TITLE = "Conversation";
+ private static final String USER_DISPLAY_NAME = "User";
+ private static final String SENDER_1 = "Sender";
+ private static final String MESSAGE_TEXT_1 = "Message 1";
+ private static final String MESSAGE_TEXT_2 = "Message 2";
+
+ /** ConversationKey for {@link NotificationMsgDelegateTest#VALID_CONVERSATION_MSG}. **/
+ private static final ConversationKey CONVERSATION_KEY_1
+ = new ConversationKey(COMPANION_DEVICE_ID, NOTIFICATION_KEY_1);
+
+ private static final MessagingStyleMessage MESSAGE_2 = MessagingStyleMessage.newBuilder()
+ .setTextMessage(MESSAGE_TEXT_2)
+ .setSender(Person.newBuilder()
+ .setName(SENDER_1))
+ .setTimestamp((long) 1577909718950f)
+ .build();
+
+ private static final MessagingStyle VALID_STYLE = MessagingStyle.newBuilder()
+ .setConvoTitle(CONVERSATION_TITLE)
+ .setUserDisplayName(USER_DISPLAY_NAME)
+ .setIsGroupConvo(false)
+ .addMessagingStyleMsg(MessagingStyleMessage.newBuilder()
+ .setTextMessage(MESSAGE_TEXT_1)
+ .setSender(Person.newBuilder()
+ .setName(SENDER_1))
+ .setTimestamp((long) 1577909718050f)
+ .build())
+ .build();
+
+ private static final ConversationNotification VALID_CONVERSATION =
+ ConversationNotification.newBuilder()
+ .setMessagingAppDisplayName(MESSAGING_APP_NAME)
+ .setMessagingAppPackageName(MESSAGING_PACKAGE_NAME)
+ .setTimeMs((long) 1577909716000f)
+ .setMessagingStyle(VALID_STYLE)
+ .build();
+
+ private static final PhoneToCarMessage VALID_CONVERSATION_MSG = PhoneToCarMessage.newBuilder()
+ .setNotificationKey(NOTIFICATION_KEY_1)
+ .setConversation(VALID_CONVERSATION)
+ .build();
+
+ private Bitmap mIconBitmap;
+ private byte[] mIconByteArray;
+
+ @Mock
+ CompanionDevice mCompanionDevice;
+ @Mock
+ NotificationManager mMockNotificationManager;
+ @Mock
+ ProjectionStateListener mMockProjectionStateListener;
+
+ @Captor
+ ArgumentCaptor<Notification> mNotificationCaptor;
+ @Captor
+ ArgumentCaptor<Integer> mNotificationIdCaptor;
+
+ Context mContext = ApplicationProvider.getApplicationContext();
+ NotificationMsgDelegate mNotificationMsgDelegate;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ when(mCompanionDevice.getDeviceId()).thenReturn(COMPANION_DEVICE_ID);
+ when(mCompanionDevice.getDeviceName()).thenReturn(COMPANION_DEVICE_NAME);
+
+ mNotificationMsgDelegate = new NotificationMsgDelegate(mContext);
+ mNotificationMsgDelegate.setNotificationManager(mMockNotificationManager);
+ mNotificationMsgDelegate.setProjectionStateListener(mMockProjectionStateListener);
+
+ mIconBitmap = Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888);
+
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ mIconBitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
+ mIconByteArray = stream.toByteArray();
+ stream.reset();
+
+ }
+
+ @After
+ public void tearDown() {
+ mIconBitmap.recycle();
+ }
+
+ @Test
+ public void newConversationShouldPostNewNotification() {
+ // Test that a new conversation notification is posted with the correct fields.
+ mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
+
+ verify(mMockNotificationManager).notify(anyInt(), mNotificationCaptor.capture());
+
+ Notification postedNotification = mNotificationCaptor.getValue();
+ verifyNotification(VALID_CONVERSATION, postedNotification);
+ }
+
+ @Test
+ public void multipleNewConversationShouldPostMultipleNewNotifications() {
+ mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
+
+ verify(mMockNotificationManager).notify(mNotificationIdCaptor.capture(),
+ any(Notification.class));
+ int firstNotificationId = mNotificationIdCaptor.getValue();
+
+ mNotificationMsgDelegate.onMessageReceived(mCompanionDevice,
+ createSecondConversation());
+ verify(mMockNotificationManager, times(2)).notify(mNotificationIdCaptor.capture(),
+ any(Notification.class));
+
+ // Verify the notification id is different than the first.
+ assertThat((long) mNotificationIdCaptor.getValue()).isNotEqualTo(firstNotificationId);
+ }
+
+ @Test
+ public void invalidConversationShouldDoNothing() {
+ // Test that a conversation without all the required fields is dropped.
+ PhoneToCarMessage newConvo = PhoneToCarMessage.newBuilder()
+ .setNotificationKey(NOTIFICATION_KEY_1)
+ .setConversation(VALID_CONVERSATION.toBuilder().clearMessagingStyle())
+ .build();
+ mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, newConvo);
+
+ verify(mMockNotificationManager, never()).notify(anyInt(), any(Notification.class));
+ }
+
+ @Test
+ public void newMessageShouldUpdateConversationNotification() {
+ // Check whether a new message updates the notification of the conversation it belongs to.
+ mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
+ verify(mMockNotificationManager).notify(mNotificationIdCaptor.capture(),
+ any(Notification.class));
+ int notificationId = mNotificationIdCaptor.getValue();
+ int messageCount = VALID_CONVERSATION_MSG.getConversation().getMessagingStyle()
+ .getMessagingStyleMsgCount();
+
+ // Post a new message in this conversation.
+ updateConversationWithMessage2();
+
+ // Verify same notification id is posted twice.
+ verify(mMockNotificationManager, times(2)).notify(eq(notificationId),
+ mNotificationCaptor.capture());
+
+ // Verify the notification contains one more message.
+ NotificationCompat.MessagingStyle messagingStyle = getMessagingStyle(
+ mNotificationCaptor.getValue());
+ assertThat(messagingStyle.getMessages().size()).isEqualTo(messageCount + 1);
+
+ // Verify notification's latest message matches the new message.
+ verifyMessage(MESSAGE_2, messagingStyle.getMessages().get(messageCount));
+ }
+
+ @Test
+ public void existingConversationShouldUpdateNotification() {
+ // Test that a conversation that already exists, but gets a new conversation message
+ // is updated with the new conversation metadata.
+ mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
+ verify(mMockNotificationManager).notify(mNotificationIdCaptor.capture(),
+ any(Notification.class));
+ int notificationId = mNotificationIdCaptor.getValue();
+
+ ConversationNotification updatedConversation = addSecondMessageToConversation().toBuilder()
+ .setMessagingAppDisplayName("New Messaging App")
+ .build();
+ mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, PhoneToCarMessage.newBuilder()
+ .setNotificationKey(NOTIFICATION_KEY_1)
+ .setConversation(updatedConversation)
+ .build());
+
+ verify(mMockNotificationManager, times(2)).notify(eq(notificationId),
+ mNotificationCaptor.capture());
+ Notification postedNotification = mNotificationCaptor.getValue();
+
+ // Verify Conversation level metadata does NOT change
+ verifyConversationLevelMetadata(VALID_CONVERSATION, postedNotification);
+ // Verify the MessagingStyle metadata does update with the new message.
+ verifyMessagingStyle(updatedConversation.getMessagingStyle(), postedNotification);
+ }
+
+ @Test
+ public void messageForUnknownConversationShouldDoNothing() {
+ // A message for an unknown conversation should be dropped.
+ updateConversationWithMessage2();
+
+ verify(mMockNotificationManager, never()).notify(anyInt(), any(Notification.class));
+ }
+
+ @Test
+ public void invalidMessageShouldDoNothing() {
+ // Message without all the required fields is dropped.
+ mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
+
+ // Create a MessagingStyleMessage without a required field (Sender information).
+ MessagingStyleMessage invalidMsgStyleMessage = MessagingStyleMessage.newBuilder()
+ .setTextMessage("Message 2")
+ .setTimestamp((long) 1577909718950f)
+ .build();
+ PhoneToCarMessage invalidMessage = PhoneToCarMessage.newBuilder()
+ .setNotificationKey(NOTIFICATION_KEY_1)
+ .setMessage(invalidMsgStyleMessage)
+ .build();
+ mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, invalidMessage);
+
+ // Verify only one notification is posted, and never updated.
+ verify(mMockNotificationManager).notify(anyInt(), any(Notification.class));
+ }
+
+ @Test
+ public void invalidAvatarIconSyncShouldDoNothing() {
+ mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
+
+ // Create AvatarIconSync message without required field (Icon), ensure it's treated as an
+ // invalid message.
+ PhoneToCarMessage invalidMessage = PhoneToCarMessage.newBuilder()
+ .setNotificationKey(NOTIFICATION_KEY_1)
+ .setAvatarIconSync(AvatarIconSync.newBuilder()
+ .setPerson(Person.newBuilder()
+ .setName(SENDER_1))
+ .build())
+ .build();
+ mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, invalidMessage);
+ assertThat(mNotificationMsgDelegate.mOneOnOneConversationAvatarMap.isEmpty()).isTrue();
+ }
+
+ @Test
+ public void avatarIconSyncForGroupConversationShouldDoNothing() {
+ // We only sync avatars for 1-1 conversations.
+ sendGroupConversationMessage();
+
+ sendValidAvatarIconSyncMessage();
+
+ assertThat(mNotificationMsgDelegate.mOneOnOneConversationAvatarMap.isEmpty()).isTrue();
+ }
+
+ @Test
+ public void avatarIconSyncForUnknownConversationShouldDoNothing() {
+ // Drop avatar if it's for a conversation that is unknown.
+ sendValidAvatarIconSyncMessage();
+
+ assertThat(mNotificationMsgDelegate.mOneOnOneConversationAvatarMap.isEmpty()).isTrue();
+ }
+
+ @Test
+ public void avatarIconSyncSetsAvatarInNotification() {
+ // Check that a conversation that didn't have an avatar, but gets this message posts
+ // a notification with the avatar.
+ mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
+ verify(mMockNotificationManager).notify(anyInt(), mNotificationCaptor.capture());
+ Icon notSetIcon = mNotificationCaptor.getValue().getLargeIcon();
+
+ sendValidAvatarIconSyncMessage();
+
+ // Post an update so we update the notification and can see the new icon.
+ updateConversationWithMessage2();
+
+ verify(mMockNotificationManager, times(2)).notify(anyInt(), mNotificationCaptor.capture());
+ Icon newIcon = mNotificationCaptor.getValue().getLargeIcon();
+ assertThat(newIcon).isNotEqualTo(notSetIcon);
+ }
+
+
+ @Test
+ public void avatarIconSyncStoresBitmapCorrectly() {
+ // Post a conversation notification first, so we don't drop the avatar message.
+ mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
+
+ sendValidAvatarIconSyncMessage();
+
+ AvatarIconSync iconSync = createValidAvatarIconSync();
+ SenderKey senderKey = SenderKey.createSenderKey(CONVERSATION_KEY_1, iconSync.getPerson());
+ byte[] iconArray = iconSync.getPerson().getAvatar().toByteArray();
+ Bitmap bitmap = BitmapFactory.decodeByteArray(iconArray, /* offset= */ 0, iconArray.length);
+
+ assertThat(mNotificationMsgDelegate.mOneOnOneConversationAvatarMap).hasSize(1);
+ Bitmap actualBitmap = mNotificationMsgDelegate.mOneOnOneConversationAvatarMap.get(
+ senderKey);
+ assertThat(actualBitmap).isNotNull();
+ assertThat(actualBitmap.sameAs(bitmap)).isTrue();
+ }
+
+ @Test
+ public void phoneMetadataUsedToCheckProjectionStatus_projectionActive() {
+ // Assert projectionListener gets called with phone metadata address.
+ when(mMockProjectionStateListener.isProjectionInActiveForeground(
+ BT_DEVICE_ADDRESS)).thenReturn(true);
+ sendValidPhoneMetadataMessage();
+
+ // Send a new conversation to trigger Projection State check.
+ mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
+
+ verify(mMockProjectionStateListener).isProjectionInActiveForeground(BT_DEVICE_ADDRESS);
+ verify(mMockNotificationManager).notify(anyInt(), mNotificationCaptor.capture());
+ checkChannelImportanceLevel(
+ mNotificationCaptor.getValue().getChannelId(), /* isLowImportance= */ true);
+ }
+
+ @Test
+ public void phoneMetadataUsedCorrectlyToCheckProjectionStatus_projectionInactive() {
+ // Assert projectionListener gets called with phone metadata address.
+ when(mMockProjectionStateListener.isProjectionInActiveForeground(
+ BT_DEVICE_ADDRESS)).thenReturn(false);
+ sendValidPhoneMetadataMessage();
+
+ // Send a new conversation to trigger Projection State check.
+ mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
+
+ verify(mMockProjectionStateListener).isProjectionInActiveForeground(BT_DEVICE_ADDRESS);
+ verify(mMockNotificationManager).notify(anyInt(), mNotificationCaptor.capture());
+ checkChannelImportanceLevel(
+ mNotificationCaptor.getValue().getChannelId(), /* isLowImportance= */ false);
+ }
+
+ @Test
+ public void delegateChecksProjectionStatus_projectionActive() {
+ // Assert projectionListener gets called with phone metadata address.
+ when(mMockProjectionStateListener.isProjectionInActiveForeground(null)).thenReturn(true);
+
+ // Send a new conversation to trigger Projection State check.
+ mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
+
+ verify(mMockProjectionStateListener).isProjectionInActiveForeground(null);
+ verify(mMockNotificationManager).notify(anyInt(), mNotificationCaptor.capture());
+ checkChannelImportanceLevel(
+ mNotificationCaptor.getValue().getChannelId(), /* isLowImportance= */ true);
+ }
+
+ @Test
+ public void delegateChecksProjectionStatus_projectionInactive() {
+ // Assert projectionListener gets called with phone metadata address.
+ when(mMockProjectionStateListener.isProjectionInActiveForeground(null)).thenReturn(false);
+
+ // Send a new conversation to trigger Projection State check.
+ mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
+
+ verify(mMockProjectionStateListener).isProjectionInActiveForeground(null);
+ verify(mMockNotificationManager).notify(anyInt(), mNotificationCaptor.capture());
+ checkChannelImportanceLevel(
+ mNotificationCaptor.getValue().getChannelId(), /* isLowImportance= */ false);
+ }
+
+ @Test
+ public void clearAllAppDataShouldClearInternalDataAndNotifications() {
+ mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
+ verify(mMockNotificationManager).notify(mNotificationIdCaptor.capture(),
+ any(Notification.class));
+ int notificationId = mNotificationIdCaptor.getValue();
+
+ sendClearAppDataRequest(NotificationMsgDelegate.REMOVE_ALL_APP_DATA);
+
+ verify(mMockNotificationManager).cancel(eq(notificationId));
+ }
+
+ @Test
+ public void clearSpecificAppDataShouldDoNothing() {
+ mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
+ verify(mMockNotificationManager).notify(anyInt(), any(Notification.class));
+
+ sendClearAppDataRequest(MESSAGING_PACKAGE_NAME);
+
+ verify(mMockNotificationManager, never()).cancel(anyInt());
+ }
+
+ @Test
+ public void conversationsFromSameApplicationPostedOnSameChannel() {
+ mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
+
+ verify(mMockNotificationManager).notify(anyInt(), mNotificationCaptor.capture());
+ String firstChannelId = mNotificationCaptor.getValue().getChannelId();
+
+ mNotificationMsgDelegate.onMessageReceived(mCompanionDevice,
+ VALID_CONVERSATION_MSG.toBuilder()
+ .setNotificationKey(NOTIFICATION_KEY_2)
+ .setConversation(VALID_CONVERSATION.toBuilder()
+ .setMessagingStyle(
+ VALID_STYLE.toBuilder().addMessagingStyleMsg(MESSAGE_2))
+ .build())
+ .build());
+ verify(mMockNotificationManager, times(2)).notify(anyInt(), mNotificationCaptor.capture());
+
+ assertThat(mNotificationCaptor.getValue().getChannelId()).isEqualTo(firstChannelId);
+ }
+
+ @Test
+ public void messageDataNotSetShouldDoNothing() {
+ // For a PhoneToCarMessage w/ no MessageData
+ mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, PhoneToCarMessage.newBuilder()
+ .setNotificationKey(NOTIFICATION_KEY_1)
+ .build());
+
+ verifyZeroInteractions(mMockNotificationManager);
+ }
+
+ @Test
+ public void dismissShouldCreateCarToPhoneMessage() {
+ mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
+
+ CarToPhoneMessage dismissMessage = mNotificationMsgDelegate.dismiss(CONVERSATION_KEY_1);
+
+ verifyCarToPhoneActionMessage(dismissMessage, NOTIFICATION_KEY_1, DISMISS);
+ }
+
+ @Test
+ public void dismissShouldDismissNotification() {
+ mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
+ verify(mMockNotificationManager).notify(mNotificationIdCaptor.capture(),
+ any(Notification.class));
+ int notificationId = mNotificationIdCaptor.getValue();
+
+ mNotificationMsgDelegate.dismiss(CONVERSATION_KEY_1);
+
+ verify(mMockNotificationManager).cancel(eq(notificationId));
+ }
+
+ @Test
+ public void markAsReadShouldCreateCarToPhoneMessage() {
+ // Mark message as read, verify message sent to phone.
+ mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
+
+ CarToPhoneMessage markAsRead = mNotificationMsgDelegate.markAsRead(CONVERSATION_KEY_1);
+
+ verifyCarToPhoneActionMessage(markAsRead, NOTIFICATION_KEY_1, MARK_AS_READ);
+ }
+
+ @Test
+ public void markAsReadShouldExcludeMessageFromNotification() {
+ // Mark message as read, verify when new message comes in, read
+ // messages are not in notification.
+ mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
+ verify(mMockNotificationManager).notify(anyInt(), any(Notification.class));
+
+ mNotificationMsgDelegate.markAsRead(CONVERSATION_KEY_1);
+ // Post an update to this conversation to ensure the now read message is not in
+ // notification.
+ updateConversationWithMessage2();
+ verify(mMockNotificationManager, times(2)).notify(anyInt(),
+ mNotificationCaptor.capture());
+
+ // Verify the notification contains only the latest message.
+ NotificationCompat.MessagingStyle messagingStyle =
+ NotificationCompat.MessagingStyle.extractMessagingStyleFromNotification(
+ mNotificationCaptor.getValue());
+ assertThat(messagingStyle.getMessages().size()).isEqualTo(1);
+ verifyMessage(MESSAGE_2, messagingStyle.getMessages().get(0));
+
+ }
+
+ @Test
+ public void replyShouldCreateCarToPhoneMessage() {
+ mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
+
+ CarToPhoneMessage reply = mNotificationMsgDelegate.reply(CONVERSATION_KEY_1,
+ MESSAGE_TEXT_2);
+ Action replyAction = reply.getActionRequest();
+ NotificationMsg.MapEntry replyEntry = replyAction.getMapEntry(0);
+
+ verifyCarToPhoneActionMessage(reply, NOTIFICATION_KEY_1, REPLY);
+ assertThat(replyAction.getMapEntryCount()).isEqualTo(1);
+ assertThat(replyEntry.getKey()).isEqualTo(NotificationMsgDelegate.REPLY_KEY);
+ assertThat(replyEntry.getValue()).isEqualTo(MESSAGE_TEXT_2);
+ }
+
+ @Test
+ public void onDestroyShouldClearInternalDataAndNotifications() {
+ mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
+ verify(mMockNotificationManager).notify(mNotificationIdCaptor.capture(),
+ any(Notification.class));
+ int notificationId = mNotificationIdCaptor.getValue();
+ sendValidAvatarIconSyncMessage();
+
+ mNotificationMsgDelegate.onDestroy();
+
+ assertThat(mNotificationMsgDelegate.mOneOnOneConversationAvatarMap.isEmpty()).isTrue();
+ verify(mMockNotificationManager).cancel(eq(notificationId));
+ }
+
+ @Test
+ public void deviceDisconnectedShouldClearDeviceNotificationsAndMetadata() {
+ // Test that after a device disconnects, all the avatars, notifications for the device
+ // is removed.
+ mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
+ verify(mMockNotificationManager).notify(mNotificationIdCaptor.capture(),
+ any(Notification.class));
+ int notificationId = mNotificationIdCaptor.getValue();
+ sendValidAvatarIconSyncMessage();
+
+ mNotificationMsgDelegate.onDeviceDisconnected(COMPANION_DEVICE_ID);
+
+ assertThat(mNotificationMsgDelegate.mOneOnOneConversationAvatarMap.isEmpty()).isTrue();
+ verify(mMockNotificationManager).cancel(eq(notificationId));
+ }
+
+ @Test
+ public void deviceDisconnectedShouldResetProjectionDeviceAddress() {
+ // Test that after a device disconnects, then reconnects, the projection device address
+ // is reset.
+ when(mMockProjectionStateListener.isProjectionInActiveForeground(
+ BT_DEVICE_ADDRESS)).thenReturn(true);
+ sendValidPhoneMetadataMessage();
+
+ mNotificationMsgDelegate.onDeviceDisconnected(COMPANION_DEVICE_ID);
+
+ // Now post a new notification for this device and ensure it is not posted silently.
+ mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, VALID_CONVERSATION_MSG);
+
+ verify(mMockProjectionStateListener).isProjectionInActiveForeground(null);
+ verify(mMockNotificationManager).notify(anyInt(), mNotificationCaptor.capture());
+ checkChannelImportanceLevel(
+ mNotificationCaptor.getValue().getChannelId(), /* isLowImportance= */ false);
+ }
+
+ private void verifyNotification(ConversationNotification expected, Notification notification) {
+ verifyConversationLevelMetadata(expected, notification);
+ verifyMessagingStyle(expected.getMessagingStyle(), notification);
+ }
+
+ /**
+ * Verifies the conversation level metadata and other aspects of a notification that do not
+ * change when a new message is added to it (such as the actions, intents).
+ */
+ private void verifyConversationLevelMetadata(ConversationNotification expected,
+ Notification notification) {
+ assertThat(notification.category).isEqualTo(CATEGORY_MESSAGE);
+
+ assertThat(notification.getSmallIcon()).isNotNull();
+ if (!expected.getAppIcon().isEmpty()) {
+ byte[] iconBytes = expected.getAppIcon().toByteArray();
+ Icon appIcon = Icon.createWithData(iconBytes, 0, iconBytes.length);
+ assertThat(notification.getSmallIcon()).isEqualTo(appIcon);
+ }
+
+ assertThat(notification.deleteIntent).isNotNull();
+
+ if (expected.getMessagingAppPackageName() != null) {
+ CharSequence appName = notification.extras.getCharSequence(
+ Notification.EXTRA_SUBSTITUTE_APP_NAME);
+ assertThat(appName).isEqualTo(expected.getMessagingAppDisplayName());
+ }
+
+ assertThat(notification.actions.length).isEqualTo(2);
+ for (NotificationCompat.Action action : getAllActions(notification)) {
+ if (action.getSemanticAction() == NotificationCompat.Action.SEMANTIC_ACTION_REPLY) {
+ assertThat(action.getRemoteInputs().length).isEqualTo(1);
+ }
+ assertThat(action.getShowsUserInterface()).isFalse();
+ }
+ }
+
+ private void verifyMessagingStyle(MessagingStyle expected, Notification notification) {
+ final NotificationCompat.MessagingStyle messagingStyle = getMessagingStyle(notification);
+
+ assertThat(messagingStyle.getUser().getName()).isEqualTo(expected.getUserDisplayName());
+ assertThat(messagingStyle.isGroupConversation()).isEqualTo(expected.getIsGroupConvo());
+ assertThat(messagingStyle.getMessages().size()).isEqualTo(expected.getMessagingStyleMsgCount());
+
+ for (int i = 0; i < expected.getMessagingStyleMsgCount(); i++) {
+ MessagingStyleMessage expectedMsg = expected.getMessagingStyleMsg(i);
+ NotificationCompat.MessagingStyle.Message actualMsg = messagingStyle.getMessages().get(
+ i);
+ verifyMessage(expectedMsg, actualMsg);
+
+ }
+ }
+
+ private void verifyMessage(MessagingStyleMessage expectedMsg,
+ NotificationCompat.MessagingStyle.Message actualMsg) {
+ assertThat(actualMsg.getTimestamp()).isEqualTo(expectedMsg.getTimestamp());
+ assertThat(actualMsg.getText()).isEqualTo(expectedMsg.getTextMessage());
+
+ Person expectedSender = expectedMsg.getSender();
+ androidx.core.app.Person actualSender = actualMsg.getPerson();
+ assertThat(actualSender.getName()).isEqualTo(expectedSender.getName());
+ if (!expectedSender.getAvatar().isEmpty()) {
+ assertThat(actualSender.getIcon()).isNotNull();
+ } else {
+ assertThat(actualSender.getIcon()).isNull();
+ }
+ }
+
+ private void verifyCarToPhoneActionMessage(CarToPhoneMessage message, String notificationKey,
+ Action.ActionName actionName) {
+ assertThat(message.getNotificationKey()).isEqualTo(notificationKey);
+ assertThat(message.getActionRequest()).isNotNull();
+ assertThat(message.getActionRequest().getNotificationKey()).isEqualTo(notificationKey);
+ assertThat(message.getActionRequest().getActionName()).isEqualTo(actionName);
+ }
+
+ private void checkChannelImportanceLevel(String channelId, boolean isLowImportance) {
+ ArgumentCaptor<NotificationChannel> channelCaptor = ArgumentCaptor.forClass(
+ NotificationChannel.class);
+ verify(mMockNotificationManager, atLeastOnce()).createNotificationChannel(
+ channelCaptor.capture());
+
+ int desiredImportance = isLowImportance ? IMPORTANCE_LOW : IMPORTANCE_HIGH;
+ List<String> desiredImportanceChannelIds = new ArrayList<>();
+ // Each messaging app has 2 channels, one high and one low importance.
+ for (NotificationChannel notificationChannel : channelCaptor.getAllValues()) {
+ if (notificationChannel.getImportance() == desiredImportance) {
+ desiredImportanceChannelIds.add(notificationChannel.getId());
+ }
+ }
+ assertThat(desiredImportanceChannelIds.contains(channelId)).isTrue();
+ }
+
+ private NotificationCompat.MessagingStyle getMessagingStyle(Notification notification) {
+ return NotificationCompat.MessagingStyle.extractMessagingStyleFromNotification(
+ notification);
+ }
+
+ private List<NotificationCompat.Action> getAllActions(Notification notification) {
+ List<NotificationCompat.Action> actions = new ArrayList<>();
+ actions.addAll(NotificationCompat.getInvisibleActions(notification));
+ for (int i = 0; i < NotificationCompat.getActionCount(notification); i++) {
+ actions.add(NotificationCompat.getAction(notification, i));
+ }
+ return actions;
+ }
+
+ private PhoneToCarMessage createSecondConversation() {
+ return VALID_CONVERSATION_MSG.toBuilder()
+ .setNotificationKey(NOTIFICATION_KEY_2)
+ .setConversation(addSecondMessageToConversation())
+ .build();
+ }
+
+ private ConversationNotification addSecondMessageToConversation() {
+ return VALID_CONVERSATION.toBuilder()
+ .setMessagingStyle(
+ VALID_STYLE.toBuilder().addMessagingStyleMsg(MESSAGE_2)).build();
+ }
+
+ private AvatarIconSync createValidAvatarIconSync() {
+ return AvatarIconSync.newBuilder()
+ .setMessagingAppPackageName(MESSAGING_PACKAGE_NAME)
+ .setMessagingAppDisplayName(MESSAGING_APP_NAME)
+ .setPerson(Person.newBuilder()
+ .setName(SENDER_1)
+ .setAvatar(ByteString.copyFrom(mIconByteArray))
+ .build())
+ .build();
+ }
+
+ /**
+ * Small helper method that updates {@link NotificationMsgDelegateTest#VALID_CONVERSATION} with
+ * a new message.
+ */
+ private void updateConversationWithMessage2() {
+ PhoneToCarMessage updateConvo = PhoneToCarMessage.newBuilder()
+ .setNotificationKey(NOTIFICATION_KEY_1)
+ .setMessage(MESSAGE_2)
+ .build();
+ mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, updateConvo);
+ }
+
+ private void sendValidAvatarIconSyncMessage() {
+ PhoneToCarMessage validMessage = PhoneToCarMessage.newBuilder()
+ .setNotificationKey(NOTIFICATION_KEY_1)
+ .setAvatarIconSync(createValidAvatarIconSync())
+ .build();
+ mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, validMessage);
+ }
+
+ private void sendValidPhoneMetadataMessage() {
+ PhoneToCarMessage metadataMessage = PhoneToCarMessage.newBuilder()
+ .setPhoneMetadata(PhoneMetadata.newBuilder()
+ .setBluetoothDeviceAddress(BT_DEVICE_ADDRESS)
+ .build())
+ .build();
+ mNotificationMsgDelegate.onMessageReceived(mCompanionDevice, metadataMessage);
+ }
+
+ private void sendGroupConversationMessage() {
+ mNotificationMsgDelegate.onMessageReceived(mCompanionDevice,
+ VALID_CONVERSATION_MSG.toBuilder()
+ .setConversation(VALID_CONVERSATION.toBuilder()
+ .setMessagingStyle(VALID_STYLE.toBuilder()
+ .setIsGroupConvo(true)
+ .build())
+ .build())
+ .build());
+ }
+
+
+ private void sendClearAppDataRequest(String messagingAppPackageName) {
+ mNotificationMsgDelegate.onMessageReceived(mCompanionDevice,
+ PhoneToCarMessage.newBuilder()
+ .setClearAppDataRequest(ClearAppDataRequest.newBuilder()
+ .setMessagingAppPackageName(messagingAppPackageName)
+ .build())
+ .build());
+ }
+}
diff --git a/tests/unit/src/com/android/car/companiondevicesupport/feature/notificationmsg/NotificationMsgFeatureTest.java b/tests/unit/src/com/android/car/companiondevicesupport/feature/notificationmsg/NotificationMsgFeatureTest.java
new file mode 100644
index 0000000..e195610
--- /dev/null
+++ b/tests/unit/src/com/android/car/companiondevicesupport/feature/notificationmsg/NotificationMsgFeatureTest.java
@@ -0,0 +1,159 @@
+/*
+ * 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.companiondevicesupport.feature.notificationmsg;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.car.companiondevicesupport.R;
+import com.android.car.companiondevicesupport.api.external.CompanionDevice;
+import com.android.car.messenger.NotificationMsgProto.NotificationMsg;
+import com.android.car.messenger.common.CompositeKey;
+import com.android.car.messenger.common.ConversationKey;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.UUID;
+import java.util.function.Predicate;
+
+@RunWith(AndroidJUnit4.class)
+public class NotificationMsgFeatureTest {
+
+ private static final String DEVICE_ID = UUID.randomUUID().toString();
+ private static final NotificationMsg.PhoneToCarMessage PHONE_TO_CAR_MESSAGE =
+ NotificationMsg.PhoneToCarMessage.newBuilder()
+ .build();
+
+ @Mock
+ private Context mContext;
+ @Mock
+ private IBinder mIBinder;
+ @Mock
+ private NotificationMsgDelegate mNotificationMsgDelegate;
+ @Mock
+ private CompanionDevice mCompanionDevice;
+
+
+ private NotificationMsgFeature mNotificationMsgFeature;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ when(mContext.getString(eq(R.string.notification_msg_feature_id))).thenReturn(
+ UUID.randomUUID().toString());
+ when(mContext.bindServiceAsUser(any(), any(), anyInt(), any())).thenReturn(true);
+ when(mCompanionDevice.getDeviceId()).thenReturn(DEVICE_ID);
+
+ mNotificationMsgFeature = new NotificationMsgFeature(mContext, mNotificationMsgDelegate);
+ }
+
+ @Test
+ public void startShouldClearInternalMemory() {
+ mNotificationMsgFeature.start();
+
+ ArgumentCaptor<Predicate<CompositeKey>> predicateArgumentCaptor = ArgumentCaptor.forClass(
+ Predicate.class);
+ verify(mNotificationMsgDelegate).cleanupMessagesAndNotifications(
+ predicateArgumentCaptor.capture());
+
+ // There's no way to test if two predicates have the same logic, so test the logic itself.
+ // The expected predicate should return true for any CompositeKey. Since CompositeKey is
+ // an abstract class, test with a class that extends it.
+ ConversationKey device_key = new ConversationKey(DEVICE_ID, "subKey");
+ ConversationKey device_key_1 = new ConversationKey("NOT_DEVICE_ID", "subKey");
+ assertThat(predicateArgumentCaptor.getValue().test(device_key)).isTrue();
+ assertThat(predicateArgumentCaptor.getValue().test(device_key_1)).isTrue();
+ assertThat(predicateArgumentCaptor.getValue().test(null)).isTrue();
+ }
+
+ @Test
+ public void stopShouldDestroyDelegate() {
+ mNotificationMsgFeature.start();
+
+ ArgumentCaptor<ServiceConnection> serviceConnectionCaptor =
+ ArgumentCaptor.forClass(ServiceConnection.class);
+ verify(mContext).bindServiceAsUser(any(), serviceConnectionCaptor.capture(), anyInt(),
+ any());
+ serviceConnectionCaptor.getValue().onServiceConnected(null, mIBinder);
+ mNotificationMsgFeature.stop();
+ verify(mNotificationMsgDelegate).onDestroy();
+ }
+
+ @Test
+ public void onMessageReceivedShouldPassMessageToDelegate() {
+ startWithSecureDevice();
+
+ mNotificationMsgFeature.onMessageReceived(mCompanionDevice,
+ PHONE_TO_CAR_MESSAGE.toByteArray());
+ verify(mNotificationMsgDelegate).onMessageReceived(mCompanionDevice, PHONE_TO_CAR_MESSAGE);
+ }
+
+ @Test
+ public void onMessageReceivedShouldCheckDeviceConnection() {
+ when(mCompanionDevice.hasSecureChannel()).thenReturn(false);
+ when(mCompanionDevice.isActiveUser()).thenReturn(true);
+ mNotificationMsgFeature.start();
+
+ mNotificationMsgFeature.onMessageReceived(mCompanionDevice,
+ PHONE_TO_CAR_MESSAGE.toByteArray());
+ verify(mNotificationMsgDelegate, never()).onMessageReceived(mCompanionDevice,
+ PHONE_TO_CAR_MESSAGE);
+ }
+
+ @Test
+ public void unknownDeviceDisconnectedShouldDoNothing() {
+ when(mCompanionDevice.hasSecureChannel()).thenReturn(true);
+ when(mCompanionDevice.isActiveUser()).thenReturn(true);
+ mNotificationMsgFeature.start();
+
+ mNotificationMsgFeature.onDeviceDisconnected(mCompanionDevice);
+ verify(mNotificationMsgDelegate, never()).onDeviceDisconnected(DEVICE_ID);
+ }
+
+ @Test
+ public void secureDeviceDisconnectedShouldAlertDelegate() {
+ startWithSecureDevice();
+
+ mNotificationMsgFeature.onDeviceDisconnected(mCompanionDevice);
+ verify(mNotificationMsgDelegate).onDeviceDisconnected(DEVICE_ID);
+ }
+
+ private void startWithSecureDevice() {
+ when(mCompanionDevice.hasSecureChannel()).thenReturn(true);
+ when(mCompanionDevice.isActiveUser()).thenReturn(true);
+ mNotificationMsgFeature.start();
+ mNotificationMsgFeature.onSecureChannelEstablished(mCompanionDevice);
+ }
+}
diff --git a/tests/unit/src/com/android/car/companiondevicesupport/feature/notificationmsg/OWNERS b/tests/unit/src/com/android/car/companiondevicesupport/feature/notificationmsg/OWNERS
new file mode 100644
index 0000000..0c113c4
--- /dev/null
+++ b/tests/unit/src/com/android/car/companiondevicesupport/feature/notificationmsg/OWNERS
@@ -0,0 +1,3 @@
+# People who can approve changes for submission.
+igorr@google.com
+jiayuzhou@google.com