Snap for 11954976 from 9692e940ec to 24Q3-release
Change-Id: I9082e0365a00a7d78b18d3e19d4aaa5de12be117
This commit is contained in:
@@ -14,3 +14,14 @@ flag {
|
||||
description: "Gates whether to require an auth challenge for changing USB preferences"
|
||||
bug: "317367746"
|
||||
}
|
||||
|
||||
|
||||
flag {
|
||||
name: "enable_bonded_bluetooth_device_searchable"
|
||||
namespace: "pixel_cross_device_control"
|
||||
description: "Set bonded bluetooth devices under connected devices page to be searchable by Settings search."
|
||||
bug: "319056077"
|
||||
metadata {
|
||||
purpose: PURPOSE_BUGFIX
|
||||
}
|
||||
}
|
||||
27
res/color/modes_set_schedule_text_color.xml
Normal file
27
res/color/modes_set_schedule_text_color.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<!--
|
||||
~ Copyright (C) 2024 The Android Open Source Project
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<selector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- when checked, background color will be accent color -->
|
||||
<item
|
||||
android:state_checked="true"
|
||||
android:color="?android:attr/textColorPrimaryInverse" />
|
||||
<!-- when unchecked, background color will be transparent -->
|
||||
<item
|
||||
android:state_checked="false"
|
||||
android:color="?android:attr/colorAccent" />
|
||||
</selector>
|
||||
25
res/drawable/ic_zen_mode_action_change_icon.xml
Normal file
25
res/drawable/ic_zen_mode_action_change_icon.xml
Normal file
@@ -0,0 +1,25 @@
|
||||
<!--
|
||||
Copyright (C) 2024 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?android:attr/colorControlNormal"
|
||||
android:viewportHeight="960"
|
||||
android:viewportWidth="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M620,440Q645,440 662.5,422.5Q680,405 680,380Q680,355 662.5,337.5Q645,320 620,320Q595,320 577.5,337.5Q560,355 560,380Q560,405 577.5,422.5Q595,440 620,440ZM340,440Q365,440 382.5,422.5Q400,405 400,380Q400,355 382.5,337.5Q365,320 340,320Q315,320 297.5,337.5Q280,355 280,380Q280,405 297.5,422.5Q315,440 340,440ZM480,700Q548,700 603.5,661.5Q659,623 684,560L276,560Q301,623 356.5,661.5Q412,700 480,700ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800Z" />
|
||||
</vector>
|
||||
47
res/drawable/modes_schedule_day_toggle.xml
Normal file
47
res/drawable/modes_schedule_day_toggle.xml
Normal file
@@ -0,0 +1,47 @@
|
||||
<!--
|
||||
~ Copyright (C) 2024 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.
|
||||
-->
|
||||
|
||||
<layer-list
|
||||
xmlns:android="http://schemas.android.com/apk/res/android" >
|
||||
<item
|
||||
android:top="2dp"
|
||||
android:bottom="2dp"
|
||||
android:left="2dp"
|
||||
android:right="2dp">
|
||||
<selector>
|
||||
<!-- selected state = solid filled in circle -->
|
||||
<item android:state_checked="true">
|
||||
<shape android:shape="oval"
|
||||
android:tint="?android:attr/colorAccent">
|
||||
<size android:height="34dp"
|
||||
android:width="34dp" />
|
||||
<solid android:color="@android:color/white" />
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
<!-- unselected state = just the outline of a circle -->
|
||||
<item android:state_checked="false">
|
||||
<shape android:shape="oval">
|
||||
<size android:height="34dp"
|
||||
android:width="34dp" />
|
||||
<stroke android:width="2dp"
|
||||
android:color="?android:attr/colorAccent" />
|
||||
<solid android:color="@android:color/transparent" />
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
||||
</item>
|
||||
</layer-list>
|
||||
38
res/layout/modes_icon_list.xml
Normal file
38
res/layout/modes_icon_list.xml
Normal file
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2024 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/icon_list"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipToPadding="true"
|
||||
android:nestedScrollingEnabled="false"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
31
res/layout/modes_icon_list_item.xml
Normal file
31
res/layout/modes_icon_list_item.xml
Normal file
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2024 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.
|
||||
-->
|
||||
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/zen_mode_icon_list_item_size"
|
||||
android:clickable="true">
|
||||
<!-- width is match_parent to distribute remaining horizontal space -->
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/icon_image_view"
|
||||
android:layout_width="@dimen/zen_mode_icon_list_circle_diameter"
|
||||
android:layout_height="@dimen/zen_mode_icon_list_circle_diameter"
|
||||
android:importantForAccessibility="no"
|
||||
android:layout_gravity="center" />
|
||||
</FrameLayout>
|
||||
228
res/layout/modes_set_schedule_layout.xml
Normal file
228
res/layout/modes_set_schedule_layout.xml
Normal file
@@ -0,0 +1,228 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (C) 2024 The Android Open Source Project
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/modes_set_schedule_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:gravity="fill_horizontal"
|
||||
android:orientation="vertical"
|
||||
android:paddingLeft="24dp"
|
||||
android:paddingRight="24dp"
|
||||
android:paddingTop="24dp"
|
||||
android:paddingBottom="24dp">
|
||||
|
||||
<!-- Start time & end time row -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="fill_horizontal"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<!-- Start time: title (non-clickable preference), time setter -->
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/start_time_label"
|
||||
android:clickable="false"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Medium"
|
||||
android:text="@string/zen_mode_start_time" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/start_time"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Title"
|
||||
android:textColor="?android:attr/colorAccent"
|
||||
android:textSize="40sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- End time: title (non-clickable preference), time setter -->
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/end_time_label"
|
||||
android:clickable="false"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Medium"
|
||||
android:text="@string/zen_mode_end_time" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/end_time"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Title"
|
||||
android:textColor="?android:attr/colorAccent"
|
||||
android:textSize="40sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Schedule duration display row -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<!-- left side line divider -->
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1.5dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:background="?android:attr/dividerHorizontal" />
|
||||
|
||||
<!-- length of schedule -->
|
||||
<TextView
|
||||
android:id="@+id/schedule_duration"
|
||||
android:clickable="false"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="8dp"
|
||||
android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Small" />
|
||||
|
||||
<!-- right side line divider -->
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1.5dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:background="?android:attr/dividerHorizontal" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Buttons for selecting days of the week -->
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/days_of_week_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="10dp"
|
||||
android:maxHeight="60dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ToggleButton
|
||||
android:id="@+id/day0"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="@drawable/modes_schedule_day_toggle"
|
||||
android:textColor="@color/modes_set_schedule_text_color"
|
||||
android:textSize="18sp"
|
||||
app:layout_constraintDimensionRatio="1:1"
|
||||
app:layout_constraintHorizontal_chainStyle="spread_inside"
|
||||
app:layout_constraintEnd_toStartOf="@+id/day1"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ToggleButton
|
||||
android:id="@+id/day1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="@drawable/modes_schedule_day_toggle"
|
||||
android:textColor="@color/modes_set_schedule_text_color"
|
||||
android:textSize="18sp"
|
||||
app:layout_constraintDimensionRatio="1:1"
|
||||
app:layout_constraintHorizontal_chainStyle="spread_inside"
|
||||
app:layout_constraintStart_toEndOf="@+id/day0"
|
||||
app:layout_constraintEnd_toStartOf="@+id/day2"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ToggleButton
|
||||
android:id="@+id/day2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="@drawable/modes_schedule_day_toggle"
|
||||
android:textColor="@color/modes_set_schedule_text_color"
|
||||
android:textSize="18sp"
|
||||
app:layout_constraintDimensionRatio="1:1"
|
||||
app:layout_constraintHorizontal_chainStyle="spread_inside"
|
||||
app:layout_constraintStart_toEndOf="@+id/day1"
|
||||
app:layout_constraintEnd_toStartOf="@+id/day3"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ToggleButton
|
||||
android:id="@+id/day3"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="@drawable/modes_schedule_day_toggle"
|
||||
android:textColor="@color/modes_set_schedule_text_color"
|
||||
android:textSize="18sp"
|
||||
app:layout_constraintDimensionRatio="1:1"
|
||||
app:layout_constraintHorizontal_chainStyle="spread_inside"
|
||||
app:layout_constraintStart_toEndOf="@+id/day2"
|
||||
app:layout_constraintEnd_toStartOf="@+id/day4"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ToggleButton
|
||||
android:id="@+id/day4"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="@drawable/modes_schedule_day_toggle"
|
||||
android:textColor="@color/modes_set_schedule_text_color"
|
||||
android:textSize="18sp"
|
||||
app:layout_constraintDimensionRatio="1:1"
|
||||
app:layout_constraintHorizontal_chainStyle="spread_inside"
|
||||
app:layout_constraintStart_toEndOf="@+id/day3"
|
||||
app:layout_constraintEnd_toStartOf="@+id/day5"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ToggleButton
|
||||
android:id="@+id/day5"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="@drawable/modes_schedule_day_toggle"
|
||||
android:textColor="@color/modes_set_schedule_text_color"
|
||||
android:textSize="18sp"
|
||||
app:layout_constraintDimensionRatio="1:1"
|
||||
app:layout_constraintHorizontal_chainStyle="spread_inside"
|
||||
app:layout_constraintStart_toEndOf="@+id/day4"
|
||||
app:layout_constraintEnd_toStartOf="@+id/day6"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ToggleButton
|
||||
android:id="@+id/day6"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="@drawable/modes_schedule_day_toggle"
|
||||
android:textColor="@color/modes_set_schedule_text_color"
|
||||
android:textSize="18sp"
|
||||
app:layout_constraintDimensionRatio="1:1"
|
||||
app:layout_constraintHorizontal_chainStyle="spread_inside"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/day5"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -497,4 +497,9 @@
|
||||
|
||||
<dimen name="audio_streams_qrcode_size">264dp</dimen>
|
||||
<dimen name="audio_streams_qrcode_preview_radius">30dp</dimen>
|
||||
|
||||
<!-- Zen Modes -->
|
||||
<dimen name="zen_mode_icon_list_item_size">96dp</dimen>
|
||||
<dimen name="zen_mode_icon_list_circle_diameter">56dp</dimen>
|
||||
<dimen name="zen_mode_icon_list_icon_size">32dp</dimen>
|
||||
</resources>
|
||||
|
||||
@@ -7963,6 +7963,15 @@
|
||||
<!-- Do not disturb: Title on the page where users choose a calendar to determine the schedule for an automatically-triggered DND rule. [CHAR LIMIT=30] -->
|
||||
<string name="zen_mode_set_calendar_category_title">Schedule</string>
|
||||
|
||||
<!-- Do not disturb: Title prompting a user to set a time-based schedule to use for an automatic rule [CHAR LIMIT=30] -->
|
||||
<string name="zen_mode_set_schedule_title">Set a schedule</string>
|
||||
|
||||
<!-- Do not disturb: Link text prompting a user to click through to setting a time-based schedule [CHAR LIMIT=40] -->
|
||||
<string name="zen_mode_set_schedule_link">Schedule</string>
|
||||
|
||||
<!-- Duration in hours and minutes for the length of a Do Not Disturb schedule. For example "1 hr, 22 min" -->
|
||||
<string name="zen_mode_schedule_duration"><xliff:g example="10" id="hours">%1$d</xliff:g> hr, <xliff:g example="20" id="minutes">%2$d</xliff:g> min</string>
|
||||
|
||||
<!-- Do not disturb: Title do not disturb settings representing automatic (scheduled) do not disturb rules. [CHAR LIMIT=30] -->
|
||||
<string name="zen_mode_schedule_category_title">Schedule</string>
|
||||
|
||||
@@ -9303,6 +9312,15 @@
|
||||
<!-- [CHAR LIMIT=NONE] Zen mode summary spoken when changing mode by voice: Turn on all notifications. -->
|
||||
<string name="zen_mode_summary_always">Change to always interrupt</string>
|
||||
|
||||
<!-- [CHAR LIMIT=20] Caption of the action button to change the name of a mode. -->
|
||||
<string name="zen_mode_action_change_name">Rename</string>
|
||||
|
||||
<!-- [CHAR LIMIT=20] Caption of the action button to change the icon of a mode. -->
|
||||
<string name="zen_mode_action_change_icon">Change icon</string>
|
||||
|
||||
<!-- [CHAR LIMIT=40] Zen mode settings: Title for the "choose mode icon" screen -->
|
||||
<string name="zen_mode_icon_picker_title">Change icon</string>
|
||||
|
||||
<!-- Content description for help icon button [CHAR LIMIT=20] -->
|
||||
<string name="warning_button_text">Warning</string>
|
||||
|
||||
|
||||
37
res/xml/modes_icon_picker.xml
Normal file
37
res/xml/modes_icon_picker.xml
Normal file
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2024 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.
|
||||
-->
|
||||
|
||||
<PreferenceScreen
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:settings="http://schemas.android.com/apk/res-auto"
|
||||
android:key="zen_mode_icon_picker_page"
|
||||
settings:searchable="false"
|
||||
android:title="@string/zen_mode_icon_picker_title">
|
||||
|
||||
<com.android.settingslib.widget.LayoutPreference
|
||||
android:key="current_icon"
|
||||
android:layout="@layout/settings_entity_header" />
|
||||
|
||||
<com.android.settings.applications.SpacePreference
|
||||
android:layout_height="16dp" />
|
||||
|
||||
<com.android.settingslib.widget.LayoutPreference
|
||||
android:key="icon_list"
|
||||
android:selectable="false"
|
||||
android:layout="@layout/modes_icon_list"/>
|
||||
|
||||
</PreferenceScreen>
|
||||
@@ -28,6 +28,10 @@
|
||||
android:selectable="false"
|
||||
android:layout="@layout/modes_activation_button"/>
|
||||
|
||||
<com.android.settingslib.widget.ActionButtonsPreference
|
||||
android:key="actions"
|
||||
android:selectable="true" />
|
||||
|
||||
<PreferenceCategory
|
||||
android:title="@string/mode_interruption_filter_title"
|
||||
android:key="modes_filters">
|
||||
|
||||
38
res/xml/modes_set_schedule.xml
Normal file
38
res/xml/modes_set_schedule.xml
Normal file
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (C) 2024 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.
|
||||
-->
|
||||
|
||||
<PreferenceScreen
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:settings="http://schemas.android.com/apk/res-auto"
|
||||
android:key="zen_mode_set_schedule"
|
||||
settings:searchable="false"
|
||||
android:title="@string/zen_mode_set_schedule_title">
|
||||
|
||||
<!-- Time picker for schedule -->
|
||||
<com.android.settingslib.widget.LayoutPreference
|
||||
android:key="schedule"
|
||||
android:selectable="false"
|
||||
android:layout="@layout/modes_set_schedule_layout"/>
|
||||
|
||||
<!-- Exit mode with alarm -->
|
||||
<SwitchPreferenceCompat
|
||||
android:key="exit_at_alarm"
|
||||
android:title="@string/zen_mode_schedule_alarm_title"
|
||||
android:summary="@string/zen_mode_schedule_alarm_summary"
|
||||
android:order="99" />
|
||||
|
||||
</PreferenceScreen>
|
||||
@@ -283,7 +283,7 @@ public class SettingsActivity extends SettingsBaseActivity
|
||||
createUiFromIntent(savedState, intent);
|
||||
}
|
||||
|
||||
protected void createUiFromIntent(Bundle savedState, Intent intent) {
|
||||
protected void createUiFromIntent(@Nullable Bundle savedState, Intent intent) {
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
final FeatureFactory factory = FeatureFactory.getFeatureFactory();
|
||||
|
||||
@@ -19,6 +19,7 @@ import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.hardware.input.InputManager;
|
||||
import android.util.FeatureFlagUtils;
|
||||
import android.util.Log;
|
||||
import android.view.InputDevice;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
@@ -26,19 +27,29 @@ import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceGroup;
|
||||
import androidx.preference.PreferenceScreen;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.bluetooth.BluetoothDeviceUpdater;
|
||||
import com.android.settings.bluetooth.ConnectedBluetoothDeviceUpdater;
|
||||
import com.android.settings.bluetooth.Utils;
|
||||
import com.android.settings.connecteddevice.dock.DockUpdater;
|
||||
import com.android.settings.connecteddevice.stylus.StylusDeviceUpdater;
|
||||
import com.android.settings.connecteddevice.usb.ConnectedUsbDeviceUpdater;
|
||||
import com.android.settings.core.BasePreferenceController;
|
||||
import com.android.settings.core.PreferenceControllerMixin;
|
||||
import com.android.settings.dashboard.DashboardFragment;
|
||||
import com.android.settings.flags.Flags;
|
||||
import com.android.settings.overlay.DockUpdaterFeatureProvider;
|
||||
import com.android.settings.overlay.FeatureFactory;
|
||||
import com.android.settingslib.bluetooth.BluetoothDeviceFilter;
|
||||
import com.android.settingslib.bluetooth.BluetoothUtils;
|
||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
|
||||
import com.android.settingslib.bluetooth.LocalBluetoothManager;
|
||||
import com.android.settingslib.core.lifecycle.LifecycleObserver;
|
||||
import com.android.settingslib.core.lifecycle.events.OnStart;
|
||||
import com.android.settingslib.core.lifecycle.events.OnStop;
|
||||
import com.android.settingslib.search.SearchIndexableRaw;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Controller to maintain the {@link androidx.preference.PreferenceGroup} for all
|
||||
@@ -49,6 +60,7 @@ public class ConnectedDeviceGroupController extends BasePreferenceController
|
||||
DevicePreferenceCallback {
|
||||
|
||||
private static final String KEY = "connected_device_list";
|
||||
private static final String TAG = "ConnectedDeviceGroupController";
|
||||
|
||||
@VisibleForTesting
|
||||
PreferenceGroup mPreferenceGroup;
|
||||
@@ -58,11 +70,13 @@ public class ConnectedDeviceGroupController extends BasePreferenceController
|
||||
private StylusDeviceUpdater mStylusDeviceUpdater;
|
||||
private final PackageManager mPackageManager;
|
||||
private final InputManager mInputManager;
|
||||
private final LocalBluetoothManager mLocalBluetoothManager;
|
||||
|
||||
public ConnectedDeviceGroupController(Context context) {
|
||||
super(context, KEY);
|
||||
mPackageManager = context.getPackageManager();
|
||||
mInputManager = context.getSystemService(InputManager.class);
|
||||
mLocalBluetoothManager = Utils.getLocalBluetoothManager(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -221,4 +235,31 @@ public class ConnectedDeviceGroupController extends BasePreferenceController
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDynamicRawDataToIndex(List<SearchIndexableRaw> rawData) {
|
||||
if (!Flags.enableBondedBluetoothDeviceSearchable()) {
|
||||
return;
|
||||
}
|
||||
if (mLocalBluetoothManager == null) {
|
||||
Log.d(TAG, "Bluetooth is not supported");
|
||||
return;
|
||||
}
|
||||
for (CachedBluetoothDevice cachedDevice :
|
||||
mLocalBluetoothManager.getCachedDeviceManager().getCachedDevicesCopy()) {
|
||||
if (!BluetoothDeviceFilter.BONDED_DEVICE_FILTER.matches(cachedDevice.getDevice())) {
|
||||
continue;
|
||||
}
|
||||
if (BluetoothUtils.isExclusivelyManagedBluetoothDevice(mContext,
|
||||
cachedDevice.getDevice())) {
|
||||
continue;
|
||||
}
|
||||
SearchIndexableRaw data = new SearchIndexableRaw(mContext);
|
||||
// Include the identity address as well to ensure the key is unique.
|
||||
data.key = cachedDevice.getName() + cachedDevice.getIdentityAddress();
|
||||
data.title = cachedDevice.getName();
|
||||
data.summaryOn = mContext.getString(R.string.connected_devices_dashboard_title);
|
||||
rawData.add(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,9 +57,10 @@ public class AudioSharingCompatibilityPreferenceController extends TogglePrefere
|
||||
@Nullable private TwoStatePreference mPreference;
|
||||
private final Executor mExecutor;
|
||||
private final MetricsFeatureProvider mMetricsFeatureProvider;
|
||||
private AtomicBoolean mCallbacksRegistered = new AtomicBoolean(false);
|
||||
private final AtomicBoolean mCallbacksRegistered = new AtomicBoolean(false);
|
||||
|
||||
private final BluetoothLeBroadcast.Callback mBroadcastCallback =
|
||||
@VisibleForTesting
|
||||
protected final BluetoothLeBroadcast.Callback mBroadcastCallback =
|
||||
new BluetoothLeBroadcast.Callback() {
|
||||
@Override
|
||||
public void onBroadcastStarted(int reason, int broadcastId) {
|
||||
|
||||
@@ -20,6 +20,8 @@ import android.app.settings.SettingsEnums;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.SettingsActivity;
|
||||
import com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsCategoryController;
|
||||
@@ -31,7 +33,6 @@ public class AudioSharingDashboardFragment extends DashboardFragment
|
||||
private static final String TAG = "AudioSharingDashboardFrag";
|
||||
|
||||
SettingsMainSwitchBar mMainSwitchBar;
|
||||
private AudioSharingSwitchBarController mSwitchBarController;
|
||||
private AudioSharingDeviceVolumeGroupController mAudioSharingDeviceVolumeGroupController;
|
||||
private AudioSharingCallAudioPreferenceController mAudioSharingCallAudioPreferenceController;
|
||||
private AudioSharingPlaySoundPreferenceController mAudioSharingPlaySoundPreferenceController;
|
||||
@@ -83,9 +84,10 @@ public class AudioSharingDashboardFragment extends DashboardFragment
|
||||
final SettingsActivity activity = (SettingsActivity) getActivity();
|
||||
mMainSwitchBar = activity.getSwitchBar();
|
||||
mMainSwitchBar.setTitle(getText(R.string.audio_sharing_switch_title));
|
||||
mSwitchBarController = new AudioSharingSwitchBarController(activity, mMainSwitchBar, this);
|
||||
mSwitchBarController.init(this);
|
||||
getSettingsLifecycle().addObserver(mSwitchBarController);
|
||||
AudioSharingSwitchBarController switchBarController =
|
||||
new AudioSharingSwitchBarController(activity, mMainSwitchBar, this);
|
||||
switchBarController.init(this);
|
||||
getSettingsLifecycle().addObserver(switchBarController);
|
||||
mMainSwitchBar.show();
|
||||
}
|
||||
|
||||
@@ -99,6 +101,19 @@ public class AudioSharingDashboardFragment extends DashboardFragment
|
||||
onProfilesConnectedForAttachedPreferences();
|
||||
}
|
||||
|
||||
/** Test only: set mock controllers for the {@link AudioSharingDashboardFragment} */
|
||||
@VisibleForTesting
|
||||
protected void setControllers(
|
||||
AudioSharingDeviceVolumeGroupController volumeGroupController,
|
||||
AudioSharingCallAudioPreferenceController callAudioController,
|
||||
AudioSharingPlaySoundPreferenceController playSoundController,
|
||||
AudioStreamsCategoryController streamsCategoryController) {
|
||||
mAudioSharingDeviceVolumeGroupController = volumeGroupController;
|
||||
mAudioSharingCallAudioPreferenceController = callAudioController;
|
||||
mAudioSharingPlaySoundPreferenceController = playSoundController;
|
||||
mAudioStreamsCategoryController = streamsCategoryController;
|
||||
}
|
||||
|
||||
private void updateVisibilityForAttachedPreferences() {
|
||||
mAudioSharingDeviceVolumeGroupController.updateVisibility();
|
||||
mAudioSharingCallAudioPreferenceController.updateVisibility();
|
||||
|
||||
@@ -20,9 +20,11 @@ import android.app.Dialog;
|
||||
import android.app.settings.SettingsEnums;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
@@ -48,13 +50,17 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment {
|
||||
* @param item The device item clicked.
|
||||
*/
|
||||
void onItemClick(AudioSharingDeviceItem item);
|
||||
|
||||
/** Called when users click the cancel button in the dialog. */
|
||||
void onCancelClick();
|
||||
}
|
||||
|
||||
@Nullable private static DialogEventListener sListener;
|
||||
private static Pair<Integer, Object>[] sEventData = new Pair[0];
|
||||
|
||||
@Override
|
||||
public int getMetricsCategory() {
|
||||
return SettingsEnums.DIALOG_START_AUDIO_SHARING;
|
||||
return SettingsEnums.DIALOG_AUDIO_SHARING_ADD_DEVICE;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,14 +69,17 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment {
|
||||
* @param host The Fragment this dialog will be hosted.
|
||||
* @param deviceItems The connected device items eligible for audio sharing.
|
||||
* @param listener The callback to handle the user action on this dialog.
|
||||
* @param eventData The eventData to log with for dialog onClick events.
|
||||
*/
|
||||
public static void show(
|
||||
@NonNull Fragment host,
|
||||
@NonNull List<AudioSharingDeviceItem> deviceItems,
|
||||
@NonNull DialogEventListener listener) {
|
||||
@NonNull DialogEventListener listener,
|
||||
@NonNull Pair<Integer, Object>[] eventData) {
|
||||
if (!AudioSharingUtils.isFeatureEnabled()) return;
|
||||
final FragmentManager manager = host.getChildFragmentManager();
|
||||
sListener = listener;
|
||||
sEventData = eventData;
|
||||
AlertDialog dialog = AudioSharingDialogHelper.getDialogIfShowing(manager, TAG);
|
||||
if (dialog != null) {
|
||||
Log.d(TAG, "Dialog is showing, return.");
|
||||
@@ -84,7 +93,19 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment {
|
||||
dialogFrag.show(manager, TAG);
|
||||
}
|
||||
|
||||
/** Return the tag of {@link AudioSharingDialogFragment} dialog. */
|
||||
public static @NonNull String tag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
/** Test only: get the event data passed to the dialog. */
|
||||
@VisibleForTesting
|
||||
protected @NonNull Pair<Integer, Object>[] getEventData() {
|
||||
return sEventData;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
Bundle arguments = requireArguments();
|
||||
List<AudioSharingDeviceItem> deviceItems =
|
||||
@@ -93,12 +114,17 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment {
|
||||
AudioSharingDialogFactory.newBuilder(getActivity())
|
||||
.setTitleIcon(com.android.settingslib.R.drawable.ic_bt_le_audio_sharing)
|
||||
.setIsCustomBodyEnabled(true);
|
||||
if (deviceItems == null) {
|
||||
Log.d(TAG, "Create dialog error: null deviceItems");
|
||||
return builder.build();
|
||||
}
|
||||
if (deviceItems.isEmpty()) {
|
||||
builder.setTitle(R.string.audio_sharing_share_dialog_title)
|
||||
.setCustomImage(R.drawable.audio_sharing_guidance)
|
||||
.setCustomMessage(R.string.audio_sharing_dialog_connect_device_content)
|
||||
.setNegativeButton(
|
||||
R.string.audio_sharing_close_button_label, (dig, which) -> dismiss());
|
||||
R.string.audio_sharing_close_button_label,
|
||||
(dig, which) -> onCancelClick());
|
||||
} else if (deviceItems.size() == 1) {
|
||||
AudioSharingDeviceItem deviceItem = Iterables.getOnlyElement(deviceItems);
|
||||
builder.setTitle(
|
||||
@@ -111,11 +137,16 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment {
|
||||
v -> {
|
||||
if (sListener != null) {
|
||||
sListener.onItemClick(deviceItem);
|
||||
mMetricsFeatureProvider.action(
|
||||
getContext(),
|
||||
SettingsEnums
|
||||
.ACTION_AUDIO_SHARING_DIALOG_POSITIVE_BTN_CLICKED,
|
||||
sEventData);
|
||||
}
|
||||
dismiss();
|
||||
})
|
||||
.setCustomNegativeButton(
|
||||
R.string.audio_sharing_no_thanks_button_label, v -> dismiss());
|
||||
R.string.audio_sharing_no_thanks_button_label, v -> onCancelClick());
|
||||
} else {
|
||||
builder.setTitle(R.string.audio_sharing_share_with_more_dialog_title)
|
||||
.setCustomMessage(R.string.audio_sharing_dialog_share_more_content)
|
||||
@@ -130,8 +161,20 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment {
|
||||
dismiss();
|
||||
},
|
||||
AudioSharingDeviceAdapter.ActionType.SHARE))
|
||||
.setCustomNegativeButton(com.android.settings.R.string.cancel, v -> dismiss());
|
||||
.setCustomNegativeButton(
|
||||
com.android.settings.R.string.cancel, v -> onCancelClick());
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private void onCancelClick() {
|
||||
if (sListener != null) {
|
||||
sListener.onCancelClick();
|
||||
mMetricsFeatureProvider.action(
|
||||
getContext(),
|
||||
SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_NEGATIVE_BTN_CLICKED,
|
||||
sEventData);
|
||||
}
|
||||
dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import android.bluetooth.BluetoothLeBroadcastMetadata;
|
||||
import android.bluetooth.BluetoothLeBroadcastReceiveState;
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -33,15 +34,21 @@ import androidx.fragment.app.Fragment;
|
||||
import com.android.settings.bluetooth.Utils;
|
||||
import com.android.settings.core.SubSettingLauncher;
|
||||
import com.android.settings.dashboard.DashboardFragment;
|
||||
import com.android.settings.overlay.FeatureFactory;
|
||||
import com.android.settingslib.bluetooth.BluetoothUtils;
|
||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
|
||||
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
|
||||
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
|
||||
import com.android.settingslib.bluetooth.LocalBluetoothManager;
|
||||
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
|
||||
import com.android.settingslib.utils.ThreadUtils;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
public class AudioSharingDialogHandler {
|
||||
@@ -51,6 +58,7 @@ public class AudioSharingDialogHandler {
|
||||
@Nullable private final LocalBluetoothManager mLocalBtManager;
|
||||
@Nullable private final LocalBluetoothLeBroadcast mBroadcast;
|
||||
@Nullable private final LocalBluetoothLeBroadcastAssistant mAssistant;
|
||||
private final MetricsFeatureProvider mMetricsFeatureProvider;
|
||||
private List<BluetoothDevice> mTargetSinks = new ArrayList<>();
|
||||
|
||||
private final BluetoothLeBroadcast.Callback mBroadcastCallback =
|
||||
@@ -119,9 +127,7 @@ public class AudioSharingDialogHandler {
|
||||
new SubSettingLauncher(mContext)
|
||||
.setDestination(AudioSharingDashboardFragment.class.getName())
|
||||
.setSourceMetricsCategory(
|
||||
(mHostFragment != null
|
||||
&& mHostFragment
|
||||
instanceof DashboardFragment)
|
||||
(mHostFragment instanceof DashboardFragment)
|
||||
? ((DashboardFragment) mHostFragment)
|
||||
.getMetricsCategory()
|
||||
: SettingsEnums.PAGE_UNKNOWN)
|
||||
@@ -146,6 +152,7 @@ public class AudioSharingDialogHandler {
|
||||
mLocalBtManager != null
|
||||
? mLocalBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile()
|
||||
: null;
|
||||
mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider();
|
||||
}
|
||||
|
||||
/** Register callbacks for dialog handler */
|
||||
@@ -191,6 +198,18 @@ public class AudioSharingDialogHandler {
|
||||
List<AudioSharingDeviceItem> deviceItemsInSharingSession =
|
||||
AudioSharingUtils.buildOrderedConnectedLeadAudioSharingDeviceItem(
|
||||
mLocalBtManager, groupedDevices, /* filterByInSharing= */ true);
|
||||
AudioSharingStopDialogFragment.DialogEventListener listener =
|
||||
() -> {
|
||||
cachedDevice.setActive();
|
||||
AudioSharingUtils.stopBroadcasting(mLocalBtManager);
|
||||
};
|
||||
Pair<Integer, Object>[] eventData =
|
||||
AudioSharingUtils.buildAudioSharingDialogEventData(
|
||||
SettingsEnums.SETTINGS_CONNECTED_DEVICE_CATEGORY,
|
||||
SettingsEnums.DIALOG_STOP_AUDIO_SHARING,
|
||||
userTriggered,
|
||||
deviceItemsInSharingSession.size(),
|
||||
/* candidateDeviceCount= */ 0);
|
||||
postOnMainThread(
|
||||
() -> {
|
||||
closeOpeningDialogsOtherThan(AudioSharingStopDialogFragment.tag());
|
||||
@@ -198,10 +217,8 @@ public class AudioSharingDialogHandler {
|
||||
mHostFragment,
|
||||
deviceItemsInSharingSession,
|
||||
cachedDevice,
|
||||
() -> {
|
||||
cachedDevice.setActive();
|
||||
AudioSharingUtils.stopBroadcasting(mLocalBtManager);
|
||||
});
|
||||
listener,
|
||||
eventData);
|
||||
});
|
||||
} else {
|
||||
if (userTriggered) {
|
||||
@@ -252,6 +269,20 @@ public class AudioSharingDialogHandler {
|
||||
// Show audio sharing switch dialog when the third eligible (LE audio) remote device
|
||||
// connected during a sharing session.
|
||||
if (deviceItemsInSharingSession.size() >= 2) {
|
||||
AudioSharingDisconnectDialogFragment.DialogEventListener listener =
|
||||
(AudioSharingDeviceItem item) -> {
|
||||
// Remove all sources from the device user clicked
|
||||
removeSourceForGroup(item.getGroupId(), groupedDevices);
|
||||
// Add current broadcast to the latest connected device
|
||||
addSourceForGroup(groupId, groupedDevices);
|
||||
};
|
||||
Pair<Integer, Object>[] eventData =
|
||||
AudioSharingUtils.buildAudioSharingDialogEventData(
|
||||
SettingsEnums.SETTINGS_CONNECTED_DEVICE_CATEGORY,
|
||||
SettingsEnums.DIALOG_AUDIO_SHARING_SWITCH_DEVICE,
|
||||
userTriggered,
|
||||
deviceItemsInSharingSession.size(),
|
||||
/* candidateDeviceCount= */ 1);
|
||||
postOnMainThread(
|
||||
() -> {
|
||||
closeOpeningDialogsOtherThan(
|
||||
@@ -260,16 +291,29 @@ public class AudioSharingDialogHandler {
|
||||
mHostFragment,
|
||||
deviceItemsInSharingSession,
|
||||
cachedDevice,
|
||||
(AudioSharingDeviceItem item) -> {
|
||||
// Remove all sources from the device user clicked
|
||||
removeSourceForGroup(item.getGroupId(), groupedDevices);
|
||||
// Add current broadcast to the latest connected device
|
||||
addSourceForGroup(groupId, groupedDevices);
|
||||
});
|
||||
listener,
|
||||
eventData);
|
||||
});
|
||||
} else {
|
||||
// Show audio sharing join dialog when the first or second eligible (LE audio)
|
||||
// remote device connected during a sharing session.
|
||||
AudioSharingJoinDialogFragment.DialogEventListener listener =
|
||||
new AudioSharingJoinDialogFragment.DialogEventListener() {
|
||||
@Override
|
||||
public void onShareClick() {
|
||||
addSourceForGroup(groupId, groupedDevices);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancelClick() {}
|
||||
};
|
||||
Pair<Integer, Object>[] eventData =
|
||||
AudioSharingUtils.buildAudioSharingDialogEventData(
|
||||
SettingsEnums.SETTINGS_CONNECTED_DEVICE_CATEGORY,
|
||||
SettingsEnums.DIALOG_AUDIO_SHARING_ADD_DEVICE,
|
||||
userTriggered,
|
||||
deviceItemsInSharingSession.size(),
|
||||
/* candidateDeviceCount= */ 1);
|
||||
postOnMainThread(
|
||||
() -> {
|
||||
closeOpeningDialogsOtherThan(AudioSharingJoinDialogFragment.tag());
|
||||
@@ -277,15 +321,8 @@ public class AudioSharingDialogHandler {
|
||||
mHostFragment,
|
||||
deviceItemsInSharingSession,
|
||||
cachedDevice,
|
||||
new AudioSharingJoinDialogFragment.DialogEventListener() {
|
||||
@Override
|
||||
public void onShareClick() {
|
||||
addSourceForGroup(groupId, groupedDevices);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancelClick() {}
|
||||
});
|
||||
listener,
|
||||
eventData);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
@@ -302,39 +339,43 @@ public class AudioSharingDialogHandler {
|
||||
// Show audio sharing join dialog when the second eligible (LE audio) remote
|
||||
// device connect and no sharing session.
|
||||
if (deviceItems.size() == 1) {
|
||||
AudioSharingJoinDialogFragment.DialogEventListener listener =
|
||||
new AudioSharingJoinDialogFragment.DialogEventListener() {
|
||||
@Override
|
||||
public void onShareClick() {
|
||||
mTargetSinks = new ArrayList<>();
|
||||
for (List<CachedBluetoothDevice> devices :
|
||||
groupedDevices.values()) {
|
||||
for (CachedBluetoothDevice device : devices) {
|
||||
mTargetSinks.add(device.getDevice());
|
||||
}
|
||||
}
|
||||
Log.d(TAG, "Start broadcast with sinks = " + mTargetSinks.size());
|
||||
if (mBroadcast != null) {
|
||||
mBroadcast.startPrivateBroadcast();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancelClick() {
|
||||
if (userTriggered) {
|
||||
cachedDevice.setActive();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Pair<Integer, Object>[] eventData =
|
||||
AudioSharingUtils.buildAudioSharingDialogEventData(
|
||||
SettingsEnums.SETTINGS_CONNECTED_DEVICE_CATEGORY,
|
||||
SettingsEnums.DIALOG_START_AUDIO_SHARING,
|
||||
userTriggered,
|
||||
/* deviceCountInSharing= */ 0,
|
||||
/* candidateDeviceCount= */ 2);
|
||||
postOnMainThread(
|
||||
() -> {
|
||||
closeOpeningDialogsOtherThan(AudioSharingJoinDialogFragment.tag());
|
||||
AudioSharingJoinDialogFragment.show(
|
||||
mHostFragment,
|
||||
deviceItems,
|
||||
cachedDevice,
|
||||
new AudioSharingJoinDialogFragment.DialogEventListener() {
|
||||
@Override
|
||||
public void onShareClick() {
|
||||
mTargetSinks = new ArrayList<>();
|
||||
for (List<CachedBluetoothDevice> devices :
|
||||
groupedDevices.values()) {
|
||||
for (CachedBluetoothDevice device : devices) {
|
||||
mTargetSinks.add(device.getDevice());
|
||||
}
|
||||
}
|
||||
Log.d(
|
||||
TAG,
|
||||
"Start broadcast with sinks: "
|
||||
+ mTargetSinks.size());
|
||||
if (mBroadcast != null) {
|
||||
mBroadcast.startPrivateBroadcast();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancelClick() {
|
||||
if (userTriggered) {
|
||||
cachedDevice.setActive();
|
||||
}
|
||||
}
|
||||
});
|
||||
mHostFragment, deviceItems, cachedDevice, listener, eventData);
|
||||
});
|
||||
} else if (userTriggered) {
|
||||
cachedDevice.setActive();
|
||||
@@ -346,9 +387,12 @@ public class AudioSharingDialogHandler {
|
||||
if (mHostFragment == null) return;
|
||||
List<Fragment> fragments = mHostFragment.getChildFragmentManager().getFragments();
|
||||
for (Fragment fragment : fragments) {
|
||||
if (fragment instanceof DialogFragment && !fragment.getTag().equals(tag)) {
|
||||
if (fragment instanceof DialogFragment
|
||||
&& fragment.getTag() != null
|
||||
&& !fragment.getTag().equals(tag)) {
|
||||
Log.d(TAG, "Remove staled opening dialog " + fragment.getTag());
|
||||
((DialogFragment) fragment).dismiss();
|
||||
logDialogDismissEvent(fragment);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -365,6 +409,7 @@ public class AudioSharingDialogHandler {
|
||||
&& AudioSharingUtils.getGroupId(device) == groupId) {
|
||||
Log.d(TAG, "Remove staled opening dialog for group " + groupId);
|
||||
((DialogFragment) fragment).dismiss();
|
||||
logDialogDismissEvent(fragment);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -382,6 +427,7 @@ public class AudioSharingDialogHandler {
|
||||
"Remove staled opening dialog for device "
|
||||
+ cachedDevice.getDevice().getAnonymizedAddress());
|
||||
((DialogFragment) fragment).dismiss();
|
||||
logDialogDismissEvent(fragment);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -409,9 +455,9 @@ public class AudioSharingDialogHandler {
|
||||
Log.d(TAG, "Fail to remove source for group " + groupId);
|
||||
return;
|
||||
}
|
||||
groupedDevices.get(groupId).stream()
|
||||
groupedDevices.getOrDefault(groupId, ImmutableList.of()).stream()
|
||||
.map(CachedBluetoothDevice::getDevice)
|
||||
.filter(device -> device != null)
|
||||
.filter(Objects::nonNull)
|
||||
.forEach(
|
||||
device -> {
|
||||
for (BluetoothLeBroadcastReceiveState source :
|
||||
@@ -431,9 +477,9 @@ public class AudioSharingDialogHandler {
|
||||
Log.d(TAG, "Fail to add source due to invalid group id, group = " + groupId);
|
||||
return;
|
||||
}
|
||||
groupedDevices.get(groupId).stream()
|
||||
groupedDevices.getOrDefault(groupId, ImmutableList.of()).stream()
|
||||
.map(CachedBluetoothDevice::getDevice)
|
||||
.filter(device -> device != null)
|
||||
.filter(Objects::nonNull)
|
||||
.forEach(
|
||||
device ->
|
||||
mAssistant.addSource(
|
||||
@@ -449,4 +495,29 @@ public class AudioSharingDialogHandler {
|
||||
private boolean isBroadcasting() {
|
||||
return mBroadcast != null && mBroadcast.isEnabled(null);
|
||||
}
|
||||
|
||||
private void logDialogDismissEvent(Fragment fragment) {
|
||||
var unused =
|
||||
ThreadUtils.postOnBackgroundThread(
|
||||
() -> {
|
||||
int pageId = SettingsEnums.PAGE_UNKNOWN;
|
||||
if (fragment instanceof AudioSharingJoinDialogFragment) {
|
||||
pageId =
|
||||
((AudioSharingJoinDialogFragment) fragment)
|
||||
.getMetricsCategory();
|
||||
} else if (fragment instanceof AudioSharingStopDialogFragment) {
|
||||
pageId =
|
||||
((AudioSharingStopDialogFragment) fragment)
|
||||
.getMetricsCategory();
|
||||
} else if (fragment instanceof AudioSharingDisconnectDialogFragment) {
|
||||
pageId =
|
||||
((AudioSharingDisconnectDialogFragment) fragment)
|
||||
.getMetricsCategory();
|
||||
}
|
||||
mMetricsFeatureProvider.action(
|
||||
mContext,
|
||||
SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_AUTO_DISMISS,
|
||||
pageId);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,16 +20,20 @@ import android.app.Dialog;
|
||||
import android.app.settings.SettingsEnums;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
|
||||
import com.android.settings.overlay.FeatureFactory;
|
||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
|
||||
import com.android.settingslib.utils.ThreadUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
@@ -55,6 +59,7 @@ public class AudioSharingDisconnectDialogFragment extends InstrumentedDialogFrag
|
||||
|
||||
@Nullable private static DialogEventListener sListener;
|
||||
@Nullable private static CachedBluetoothDevice sNewDevice;
|
||||
private static Pair<Integer, Object>[] sEventData = new Pair[0];
|
||||
|
||||
@Override
|
||||
public int getMetricsCategory() {
|
||||
@@ -70,12 +75,14 @@ public class AudioSharingDisconnectDialogFragment extends InstrumentedDialogFrag
|
||||
* @param deviceItems The existing connected device items in audio sharing session.
|
||||
* @param newDevice The latest connected device triggered this dialog.
|
||||
* @param listener The callback to handle the user action on this dialog.
|
||||
* @param eventData The eventData to log with for dialog onClick events.
|
||||
*/
|
||||
public static void show(
|
||||
@NonNull Fragment host,
|
||||
@NonNull List<AudioSharingDeviceItem> deviceItems,
|
||||
@NonNull CachedBluetoothDevice newDevice,
|
||||
@NonNull DialogEventListener listener) {
|
||||
@NonNull DialogEventListener listener,
|
||||
@NonNull Pair<Integer, Object>[] eventData) {
|
||||
if (!AudioSharingUtils.isFeatureEnabled()) return;
|
||||
FragmentManager manager = host.getChildFragmentManager();
|
||||
AlertDialog dialog = AudioSharingDialogHelper.getDialogIfShowing(manager, TAG);
|
||||
@@ -91,6 +98,7 @@ public class AudioSharingDisconnectDialogFragment extends InstrumentedDialogFrag
|
||||
newGroupId));
|
||||
sListener = listener;
|
||||
sNewDevice = newDevice;
|
||||
sEventData = eventData;
|
||||
return;
|
||||
} else {
|
||||
Log.d(
|
||||
@@ -101,10 +109,22 @@ public class AudioSharingDisconnectDialogFragment extends InstrumentedDialogFrag
|
||||
+ "dismiss current dialog.",
|
||||
newGroupId));
|
||||
dialog.dismiss();
|
||||
var unused =
|
||||
ThreadUtils.postOnBackgroundThread(
|
||||
() ->
|
||||
FeatureFactory.getFeatureFactory()
|
||||
.getMetricsFeatureProvider()
|
||||
.action(
|
||||
dialog.getContext(),
|
||||
SettingsEnums
|
||||
.ACTION_AUDIO_SHARING_DIALOG_AUTO_DISMISS,
|
||||
SettingsEnums
|
||||
.DIALOG_AUDIO_SHARING_SWITCH_DEVICE));
|
||||
}
|
||||
}
|
||||
sListener = listener;
|
||||
sNewDevice = newDevice;
|
||||
sEventData = eventData;
|
||||
Log.d(TAG, "Show up the dialog.");
|
||||
final Bundle bundle = new Bundle();
|
||||
bundle.putParcelableList(BUNDLE_KEY_DEVICE_TO_DISCONNECT_ITEMS, deviceItems);
|
||||
@@ -125,28 +145,54 @@ public class AudioSharingDisconnectDialogFragment extends InstrumentedDialogFrag
|
||||
return sNewDevice;
|
||||
}
|
||||
|
||||
/** Test only: get the event data passed to the dialog. */
|
||||
@VisibleForTesting
|
||||
protected @NonNull Pair<Integer, Object>[] getEventData() {
|
||||
return sEventData;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
|
||||
Bundle arguments = requireArguments();
|
||||
List<AudioSharingDeviceItem> deviceItems =
|
||||
arguments.getParcelable(BUNDLE_KEY_DEVICE_TO_DISCONNECT_ITEMS, List.class);
|
||||
return AudioSharingDialogFactory.newBuilder(getActivity())
|
||||
.setTitle(R.string.audio_sharing_disconnect_dialog_title)
|
||||
.setTitleIcon(com.android.settingslib.R.drawable.ic_bt_le_audio_sharing)
|
||||
.setIsCustomBodyEnabled(true)
|
||||
.setCustomMessage(R.string.audio_sharing_dialog_disconnect_content)
|
||||
.setCustomDeviceActions(
|
||||
new AudioSharingDeviceAdapter(
|
||||
getContext(),
|
||||
deviceItems,
|
||||
(AudioSharingDeviceItem item) -> {
|
||||
if (sListener != null) {
|
||||
sListener.onItemClick(item);
|
||||
}
|
||||
AudioSharingDialogFactory.DialogBuilder builder =
|
||||
AudioSharingDialogFactory.newBuilder(getActivity())
|
||||
.setTitle(R.string.audio_sharing_disconnect_dialog_title)
|
||||
.setTitleIcon(com.android.settingslib.R.drawable.ic_bt_le_audio_sharing)
|
||||
.setIsCustomBodyEnabled(true)
|
||||
.setCustomMessage(R.string.audio_sharing_dialog_disconnect_content)
|
||||
.setCustomNegativeButton(
|
||||
com.android.settings.R.string.cancel,
|
||||
v -> {
|
||||
mMetricsFeatureProvider.action(
|
||||
getContext(),
|
||||
SettingsEnums
|
||||
.ACTION_AUDIO_SHARING_DIALOG_NEGATIVE_BTN_CLICKED,
|
||||
sEventData);
|
||||
dismiss();
|
||||
},
|
||||
AudioSharingDeviceAdapter.ActionType.REMOVE))
|
||||
.setCustomNegativeButton(com.android.settings.R.string.cancel, v -> dismiss())
|
||||
.build();
|
||||
});
|
||||
if (deviceItems == null) {
|
||||
Log.d(TAG, "Create dialog error: null deviceItems");
|
||||
return builder.build();
|
||||
}
|
||||
builder.setCustomDeviceActions(
|
||||
new AudioSharingDeviceAdapter(
|
||||
getContext(),
|
||||
deviceItems,
|
||||
(AudioSharingDeviceItem item) -> {
|
||||
if (sListener != null) {
|
||||
sListener.onItemClick(item);
|
||||
mMetricsFeatureProvider.action(
|
||||
getContext(),
|
||||
SettingsEnums
|
||||
.ACTION_AUDIO_SHARING_DIALOG_POSITIVE_BTN_CLICKED,
|
||||
sEventData);
|
||||
}
|
||||
dismiss();
|
||||
},
|
||||
AudioSharingDeviceAdapter.ActionType.REMOVE));
|
||||
return builder.build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,9 +20,11 @@ import android.app.Dialog;
|
||||
import android.app.settings.SettingsEnums;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
@@ -52,6 +54,7 @@ public class AudioSharingJoinDialogFragment extends InstrumentedDialogFragment {
|
||||
|
||||
@Nullable private static DialogEventListener sListener;
|
||||
@Nullable private static CachedBluetoothDevice sNewDevice;
|
||||
private static Pair<Integer, Object>[] sEventData = new Pair[0];
|
||||
|
||||
@Override
|
||||
public int getMetricsCategory() {
|
||||
@@ -69,16 +72,19 @@ public class AudioSharingJoinDialogFragment extends InstrumentedDialogFragment {
|
||||
* @param deviceItems The existing connected device items eligible for audio sharing.
|
||||
* @param newDevice The latest connected device triggered this dialog.
|
||||
* @param listener The callback to handle the user action on this dialog.
|
||||
* @param eventData The eventData to log with for dialog onClick events.
|
||||
*/
|
||||
public static void show(
|
||||
@NonNull Fragment host,
|
||||
@NonNull List<AudioSharingDeviceItem> deviceItems,
|
||||
@NonNull CachedBluetoothDevice newDevice,
|
||||
@NonNull DialogEventListener listener) {
|
||||
@NonNull DialogEventListener listener,
|
||||
@NonNull Pair<Integer, Object>[] eventData) {
|
||||
if (!AudioSharingUtils.isFeatureEnabled()) return;
|
||||
final FragmentManager manager = host.getChildFragmentManager();
|
||||
sListener = listener;
|
||||
sNewDevice = newDevice;
|
||||
sEventData = eventData;
|
||||
AlertDialog dialog = AudioSharingDialogHelper.getDialogIfShowing(manager, TAG);
|
||||
if (dialog != null) {
|
||||
Log.d(TAG, "Dialog is showing, update the content.");
|
||||
@@ -104,7 +110,14 @@ public class AudioSharingJoinDialogFragment extends InstrumentedDialogFragment {
|
||||
return sNewDevice;
|
||||
}
|
||||
|
||||
/** Test only: get the event data passed to the dialog. */
|
||||
@VisibleForTesting
|
||||
protected @NonNull Pair<Integer, Object>[] getEventData() {
|
||||
return sEventData;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
|
||||
Bundle arguments = requireArguments();
|
||||
List<AudioSharingDeviceItem> deviceItems =
|
||||
@@ -121,6 +134,11 @@ public class AudioSharingJoinDialogFragment extends InstrumentedDialogFragment {
|
||||
v -> {
|
||||
if (sListener != null) {
|
||||
sListener.onShareClick();
|
||||
mMetricsFeatureProvider.action(
|
||||
getContext(),
|
||||
SettingsEnums
|
||||
.ACTION_AUDIO_SHARING_DIALOG_POSITIVE_BTN_CLICKED,
|
||||
sEventData);
|
||||
}
|
||||
dismiss();
|
||||
})
|
||||
@@ -129,11 +147,20 @@ public class AudioSharingJoinDialogFragment extends InstrumentedDialogFragment {
|
||||
v -> {
|
||||
if (sListener != null) {
|
||||
sListener.onCancelClick();
|
||||
mMetricsFeatureProvider.action(
|
||||
getContext(),
|
||||
SettingsEnums
|
||||
.ACTION_AUDIO_SHARING_DIALOG_NEGATIVE_BTN_CLICKED,
|
||||
sEventData);
|
||||
}
|
||||
dismiss();
|
||||
})
|
||||
.build();
|
||||
updateDialog(deviceItems, newDeviceName, dialog);
|
||||
if (deviceItems == null) {
|
||||
Log.d(TAG, "Fail to create dialog: null deviceItems");
|
||||
} else {
|
||||
updateDialog(deviceItems, newDeviceName, dialog);
|
||||
}
|
||||
dialog.show();
|
||||
AudioSharingDialogHelper.updateMessageStyle(dialog);
|
||||
return dialog;
|
||||
|
||||
@@ -23,6 +23,7 @@ import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.lifecycle.DefaultLifecycleObserver;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.preference.Preference;
|
||||
@@ -50,7 +51,8 @@ public class AudioSharingPreferenceController extends BasePreferenceController
|
||||
@Nullable private Preference mPreference;
|
||||
private final Executor mExecutor;
|
||||
|
||||
private final BluetoothLeBroadcast.Callback mBroadcastCallback =
|
||||
@VisibleForTesting
|
||||
protected final BluetoothLeBroadcast.Callback mBroadcastCallback =
|
||||
new BluetoothLeBroadcast.Callback() {
|
||||
@Override
|
||||
public void onBroadcastStarted(int reason, int broadcastId) {
|
||||
|
||||
@@ -20,16 +20,20 @@ import android.app.Dialog;
|
||||
import android.app.settings.SettingsEnums;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
|
||||
import com.android.settings.overlay.FeatureFactory;
|
||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
|
||||
import com.android.settingslib.utils.ThreadUtils;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
|
||||
@@ -52,6 +56,7 @@ public class AudioSharingStopDialogFragment extends InstrumentedDialogFragment {
|
||||
|
||||
@Nullable private static DialogEventListener sListener;
|
||||
@Nullable private static CachedBluetoothDevice sCachedDevice;
|
||||
private static Pair<Integer, Object>[] sEventData = new Pair[0];
|
||||
|
||||
@Override
|
||||
public int getMetricsCategory() {
|
||||
@@ -67,12 +72,14 @@ public class AudioSharingStopDialogFragment extends InstrumentedDialogFragment {
|
||||
* @param deviceItems The existing connected device items in audio sharing session.
|
||||
* @param newDevice The latest connected device triggered this dialog.
|
||||
* @param listener The callback to handle the user action on this dialog.
|
||||
* @param eventData The eventData to log with for dialog onClick events.
|
||||
*/
|
||||
public static void show(
|
||||
@NonNull Fragment host,
|
||||
@NonNull List<AudioSharingDeviceItem> deviceItems,
|
||||
@NonNull CachedBluetoothDevice newDevice,
|
||||
@NonNull DialogEventListener listener) {
|
||||
@NonNull DialogEventListener listener,
|
||||
@NonNull Pair<Integer, Object>[] eventData) {
|
||||
if (!AudioSharingUtils.isFeatureEnabled()) return;
|
||||
final FragmentManager manager = host.getChildFragmentManager();
|
||||
AlertDialog dialog = AudioSharingDialogHelper.getDialogIfShowing(manager, TAG);
|
||||
@@ -88,6 +95,7 @@ public class AudioSharingStopDialogFragment extends InstrumentedDialogFragment {
|
||||
newGroupId));
|
||||
sListener = listener;
|
||||
sCachedDevice = newDevice;
|
||||
sEventData = eventData;
|
||||
return;
|
||||
} else {
|
||||
Log.d(
|
||||
@@ -98,10 +106,21 @@ public class AudioSharingStopDialogFragment extends InstrumentedDialogFragment {
|
||||
+ "dismiss current dialog.",
|
||||
newGroupId));
|
||||
dialog.dismiss();
|
||||
var unused =
|
||||
ThreadUtils.postOnBackgroundThread(
|
||||
() ->
|
||||
FeatureFactory.getFeatureFactory()
|
||||
.getMetricsFeatureProvider()
|
||||
.action(
|
||||
dialog.getContext(),
|
||||
SettingsEnums
|
||||
.ACTION_AUDIO_SHARING_DIALOG_AUTO_DISMISS,
|
||||
SettingsEnums.DIALOG_STOP_AUDIO_SHARING));
|
||||
}
|
||||
}
|
||||
sListener = listener;
|
||||
sCachedDevice = newDevice;
|
||||
sEventData = eventData;
|
||||
Log.d(TAG, "Show up the dialog.");
|
||||
final Bundle bundle = new Bundle();
|
||||
bundle.putParcelableList(BUNDLE_KEY_DEVICE_TO_DISCONNECT_ITEMS, deviceItems);
|
||||
@@ -121,23 +140,34 @@ public class AudioSharingStopDialogFragment extends InstrumentedDialogFragment {
|
||||
return sCachedDevice;
|
||||
}
|
||||
|
||||
/** Test only: get the event data passed to the dialog. */
|
||||
@VisibleForTesting
|
||||
protected @NonNull Pair<Integer, Object>[] getEventData() {
|
||||
return sEventData;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
|
||||
Bundle arguments = requireArguments();
|
||||
List<AudioSharingDeviceItem> deviceItems =
|
||||
arguments.getParcelable(BUNDLE_KEY_DEVICE_TO_DISCONNECT_ITEMS, List.class);
|
||||
String newDeviceName = arguments.getString(BUNDLE_KEY_NEW_DEVICE_NAME);
|
||||
String customMessage =
|
||||
deviceItems.size() == 1
|
||||
? getString(
|
||||
R.string.audio_sharing_stop_dialog_content,
|
||||
Iterables.getOnlyElement(deviceItems).getName())
|
||||
: (deviceItems.size() == 2
|
||||
? getString(
|
||||
R.string.audio_sharing_stop_dialog_with_two_content,
|
||||
deviceItems.get(0).getName(),
|
||||
deviceItems.get(1).getName())
|
||||
: getString(R.string.audio_sharing_stop_dialog_with_more_content));
|
||||
String customMessage = "";
|
||||
if (deviceItems != null) {
|
||||
customMessage =
|
||||
deviceItems.size() == 1
|
||||
? getString(
|
||||
R.string.audio_sharing_stop_dialog_content,
|
||||
Iterables.getOnlyElement(deviceItems).getName())
|
||||
: (deviceItems.size() == 2
|
||||
? getString(
|
||||
R.string.audio_sharing_stop_dialog_with_two_content,
|
||||
deviceItems.get(0).getName(),
|
||||
deviceItems.get(1).getName())
|
||||
: getString(
|
||||
R.string.audio_sharing_stop_dialog_with_more_content));
|
||||
}
|
||||
AlertDialog dialog =
|
||||
AudioSharingDialogFactory.newBuilder(getActivity())
|
||||
.setTitle(
|
||||
@@ -150,10 +180,21 @@ public class AudioSharingStopDialogFragment extends InstrumentedDialogFragment {
|
||||
(dlg, which) -> {
|
||||
if (sListener != null) {
|
||||
sListener.onStopSharingClick();
|
||||
mMetricsFeatureProvider.action(
|
||||
getContext(),
|
||||
SettingsEnums
|
||||
.ACTION_AUDIO_SHARING_DIALOG_POSITIVE_BTN_CLICKED,
|
||||
sEventData);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(
|
||||
com.android.settings.R.string.cancel, (dlg, which) -> dismiss())
|
||||
com.android.settings.R.string.cancel,
|
||||
(dlg, which) ->
|
||||
mMetricsFeatureProvider.action(
|
||||
getContext(),
|
||||
SettingsEnums
|
||||
.ACTION_AUDIO_SHARING_DIALOG_NEGATIVE_BTN_CLICKED,
|
||||
sEventData))
|
||||
.build();
|
||||
dialog.show();
|
||||
AudioSharingDialogHelper.updateMessageStyle(dialog);
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package com.android.settings.connecteddevice.audiosharing;
|
||||
|
||||
import android.app.settings.SettingsEnums;
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.bluetooth.BluetoothLeBroadcast;
|
||||
@@ -29,24 +30,27 @@ import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.util.FeatureFlagUtils;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.CompoundButton.OnCheckedChangeListener;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.DefaultLifecycleObserver;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
|
||||
import com.android.settings.bluetooth.Utils;
|
||||
import com.android.settings.core.BasePreferenceController;
|
||||
import com.android.settings.dashboard.DashboardFragment;
|
||||
import com.android.settings.overlay.FeatureFactory;
|
||||
import com.android.settings.widget.SettingsMainSwitchBar;
|
||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
|
||||
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
|
||||
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
|
||||
import com.android.settingslib.bluetooth.LocalBluetoothManager;
|
||||
import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
|
||||
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
|
||||
import com.android.settingslib.utils.ThreadUtils;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
@@ -56,6 +60,7 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
@@ -91,14 +96,15 @@ public class AudioSharingSwitchBarController extends BasePreferenceController
|
||||
@Nullable private final LocalBluetoothProfileManager mProfileManager;
|
||||
@Nullable private final LocalBluetoothLeBroadcast mBroadcast;
|
||||
@Nullable private final LocalBluetoothLeBroadcastAssistant mAssistant;
|
||||
@Nullable private DashboardFragment mFragment;
|
||||
@Nullable private Fragment mFragment;
|
||||
private final Executor mExecutor;
|
||||
private final MetricsFeatureProvider mMetricsFeatureProvider;
|
||||
private final OnAudioSharingStateChangedListener mListener;
|
||||
private Map<Integer, List<CachedBluetoothDevice>> mGroupedConnectedDevices = new HashMap<>();
|
||||
private List<BluetoothDevice> mTargetActiveSinks = new ArrayList<>();
|
||||
private List<AudioSharingDeviceItem> mDeviceItemsForSharing = new ArrayList<>();
|
||||
@VisibleForTesting IntentFilter mIntentFilter;
|
||||
private AtomicBoolean mCallbacksRegistered = new AtomicBoolean(false);
|
||||
private final AtomicBoolean mCallbacksRegistered = new AtomicBoolean(false);
|
||||
|
||||
@VisibleForTesting
|
||||
BroadcastReceiver mReceiver =
|
||||
@@ -110,7 +116,8 @@ public class AudioSharingSwitchBarController extends BasePreferenceController
|
||||
}
|
||||
};
|
||||
|
||||
private final BluetoothLeBroadcast.Callback mBroadcastCallback =
|
||||
@VisibleForTesting
|
||||
protected final BluetoothLeBroadcast.Callback mBroadcastCallback =
|
||||
new BluetoothLeBroadcast.Callback() {
|
||||
@Override
|
||||
public void onBroadcastStarted(int reason, int broadcastId) {
|
||||
@@ -182,7 +189,7 @@ public class AudioSharingSwitchBarController extends BasePreferenceController
|
||||
public void onPlaybackStopped(int reason, int broadcastId) {}
|
||||
};
|
||||
|
||||
private BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback =
|
||||
private final BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback =
|
||||
new BluetoothLeBroadcastAssistant.Callback() {
|
||||
@Override
|
||||
public void onSearchStarted(int reason) {}
|
||||
@@ -251,9 +258,9 @@ public class AudioSharingSwitchBarController extends BasePreferenceController
|
||||
|
||||
@Override
|
||||
public void onReceiveStateChanged(
|
||||
BluetoothDevice sink,
|
||||
@NonNull BluetoothDevice sink,
|
||||
int sourceId,
|
||||
BluetoothLeBroadcastReceiveState state) {}
|
||||
@NonNull BluetoothLeBroadcastReceiveState state) {}
|
||||
};
|
||||
|
||||
AudioSharingSwitchBarController(
|
||||
@@ -273,6 +280,7 @@ public class AudioSharingSwitchBarController extends BasePreferenceController
|
||||
? null
|
||||
: mProfileManager.getLeAudioBroadcastAssistantProfile();
|
||||
mExecutor = Executors.newSingleThreadExecutor();
|
||||
mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -378,7 +386,7 @@ public class AudioSharingSwitchBarController extends BasePreferenceController
|
||||
*
|
||||
* @param fragment The fragment to host the {@link AudioSharingSwitchBarController} dialog.
|
||||
*/
|
||||
public void init(DashboardFragment fragment) {
|
||||
public void init(@NonNull Fragment fragment) {
|
||||
this.mFragment = fragment;
|
||||
}
|
||||
|
||||
@@ -494,34 +502,58 @@ public class AudioSharingSwitchBarController extends BasePreferenceController
|
||||
}
|
||||
|
||||
private void handleOnBroadcastReady() {
|
||||
AudioSharingUtils.addSourceToTargetSinks(mTargetActiveSinks, mBtManager);
|
||||
mTargetActiveSinks.clear();
|
||||
Pair<Integer, Object>[] eventData =
|
||||
AudioSharingUtils.buildAudioSharingDialogEventData(
|
||||
SettingsEnums.AUDIO_SHARING_SETTINGS,
|
||||
SettingsEnums.DIALOG_AUDIO_SHARING_ADD_DEVICE,
|
||||
/* userTriggered= */ false,
|
||||
/* deviceCountInSharing= */ mTargetActiveSinks.isEmpty() ? 0 : 1,
|
||||
/* candidateDeviceCount= */ mDeviceItemsForSharing.size());
|
||||
if (!mTargetActiveSinks.isEmpty()) {
|
||||
Log.d(TAG, "handleOnBroadcastReady: automatically add source to active sinks.");
|
||||
AudioSharingUtils.addSourceToTargetSinks(mTargetActiveSinks, mBtManager);
|
||||
mMetricsFeatureProvider.action(mContext, SettingsEnums.ACTION_AUTO_JOIN_AUDIO_SHARING);
|
||||
mTargetActiveSinks.clear();
|
||||
}
|
||||
if (mFragment == null) {
|
||||
Log.w(TAG, "Dialog fail to show due to null fragment.");
|
||||
Log.d(TAG, "handleOnBroadcastReady: dialog fail to show due to null fragment.");
|
||||
mGroupedConnectedDevices.clear();
|
||||
mDeviceItemsForSharing.clear();
|
||||
return;
|
||||
}
|
||||
showDialog(eventData);
|
||||
}
|
||||
|
||||
private void showDialog(Pair<Integer, Object>[] eventData) {
|
||||
AudioSharingDialogFragment.DialogEventListener listener =
|
||||
new AudioSharingDialogFragment.DialogEventListener() {
|
||||
@Override
|
||||
public void onItemClick(@NonNull AudioSharingDeviceItem item) {
|
||||
AudioSharingUtils.addSourceToTargetSinks(
|
||||
mGroupedConnectedDevices
|
||||
.getOrDefault(item.getGroupId(), ImmutableList.of())
|
||||
.stream()
|
||||
.map(CachedBluetoothDevice::getDevice)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList()),
|
||||
mBtManager);
|
||||
mGroupedConnectedDevices.clear();
|
||||
mDeviceItemsForSharing.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancelClick() {
|
||||
mGroupedConnectedDevices.clear();
|
||||
mDeviceItemsForSharing.clear();
|
||||
}
|
||||
};
|
||||
AudioSharingUtils.postOnMainThread(
|
||||
mContext,
|
||||
() -> {
|
||||
// Check nullability to pass NullAway check
|
||||
if (mFragment != null) {
|
||||
AudioSharingDialogFragment.show(
|
||||
mFragment,
|
||||
mDeviceItemsForSharing,
|
||||
item -> {
|
||||
AudioSharingUtils.addSourceToTargetSinks(
|
||||
mGroupedConnectedDevices
|
||||
.getOrDefault(
|
||||
item.getGroupId(), ImmutableList.of())
|
||||
.stream()
|
||||
.map(CachedBluetoothDevice::getDevice)
|
||||
.collect(Collectors.toList()),
|
||||
mBtManager);
|
||||
mGroupedConnectedDevices.clear();
|
||||
mDeviceItemsForSharing.clear();
|
||||
});
|
||||
mFragment, mDeviceItemsForSharing, listener, eventData);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -16,6 +16,12 @@
|
||||
|
||||
package com.android.settings.connecteddevice.audiosharing;
|
||||
|
||||
import static com.android.settings.connecteddevice.audiosharing.AudioSharingUtils.MetricKey.METRIC_KEY_CANDIDATE_DEVICE_COUNT;
|
||||
import static com.android.settings.connecteddevice.audiosharing.AudioSharingUtils.MetricKey.METRIC_KEY_DEVICE_COUNT_IN_SHARING;
|
||||
import static com.android.settings.connecteddevice.audiosharing.AudioSharingUtils.MetricKey.METRIC_KEY_PAGE_ID;
|
||||
import static com.android.settings.connecteddevice.audiosharing.AudioSharingUtils.MetricKey.METRIC_KEY_SOURCE_PAGE_ID;
|
||||
import static com.android.settings.connecteddevice.audiosharing.AudioSharingUtils.MetricKey.METRIC_KEY_USER_TRIGGERED;
|
||||
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothCsipSetCoordinator;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
@@ -25,6 +31,7 @@ import android.bluetooth.BluetoothStatusCodes;
|
||||
import android.content.Context;
|
||||
import android.provider.Settings;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -54,6 +61,14 @@ public class AudioSharingUtils {
|
||||
private static final String TAG = "AudioSharingUtils";
|
||||
private static final boolean DEBUG = BluetoothUtils.D;
|
||||
|
||||
public enum MetricKey {
|
||||
METRIC_KEY_SOURCE_PAGE_ID,
|
||||
METRIC_KEY_PAGE_ID,
|
||||
METRIC_KEY_USER_TRIGGERED,
|
||||
METRIC_KEY_DEVICE_COUNT_IN_SHARING,
|
||||
METRIC_KEY_CANDIDATE_DEVICE_COUNT
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch {@link CachedBluetoothDevice}s connected to the broadcast assistant. The devices are
|
||||
* grouped by CSIP group id.
|
||||
@@ -121,7 +136,7 @@ public class AudioSharingUtils {
|
||||
boolean filterByInSharing) {
|
||||
List<CachedBluetoothDevice> orderedDevices = new ArrayList<>();
|
||||
for (List<CachedBluetoothDevice> devices : groupedConnectedDevices.values()) {
|
||||
@Nullable CachedBluetoothDevice leadDevice = getLeadDevice(devices);
|
||||
CachedBluetoothDevice leadDevice = getLeadDevice(devices);
|
||||
if (leadDevice == null) {
|
||||
Log.d(TAG, "Skip due to no lead device");
|
||||
continue;
|
||||
@@ -206,7 +221,7 @@ public class AudioSharingUtils {
|
||||
return buildOrderedConnectedLeadDevices(
|
||||
localBtManager, groupedConnectedDevices, filterByInSharing)
|
||||
.stream()
|
||||
.map(device -> buildAudioSharingDeviceItem(device))
|
||||
.map(AudioSharingUtils::buildAudioSharingDeviceItem)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@@ -315,8 +330,9 @@ public class AudioSharingUtils {
|
||||
manager.getProfileManager().getLeAudioBroadcastProfile();
|
||||
if (broadcast == null) {
|
||||
Log.d(TAG, "Skip stop broadcasting due to broadcast profile is null");
|
||||
} else {
|
||||
broadcast.stopBroadcast(broadcast.getLatestBroadcastId());
|
||||
}
|
||||
broadcast.stopBroadcast(broadcast.getLatestBroadcastId());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -378,9 +394,32 @@ public class AudioSharingUtils {
|
||||
return false;
|
||||
}
|
||||
VolumeControlProfile vc = profileManager.getVolumeControlProfile();
|
||||
if (vc == null || !vc.isProfileReady()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
return vc != null && vc.isProfileReady();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build audio sharing dialog log event data
|
||||
*
|
||||
* @param sourcePageId The source page id on which the dialog is shown. *
|
||||
* @param pageId The page id of the dialog.
|
||||
* @param userTriggered Indicates whether the dialog is triggered by user click.
|
||||
* @param deviceCountInSharing The count of the devices joining the audio sharing.
|
||||
* @param candidateDeviceCount The count of the eligible devices to join the audio sharing.
|
||||
* @return The event data to be attached to the audio sharing action logs.
|
||||
*/
|
||||
@NonNull
|
||||
public static Pair<Integer, Object>[] buildAudioSharingDialogEventData(
|
||||
int sourcePageId,
|
||||
int pageId,
|
||||
boolean userTriggered,
|
||||
int deviceCountInSharing,
|
||||
int candidateDeviceCount) {
|
||||
return new Pair[] {
|
||||
Pair.create(METRIC_KEY_SOURCE_PAGE_ID.ordinal(), sourcePageId),
|
||||
Pair.create(METRIC_KEY_PAGE_ID.ordinal(), pageId),
|
||||
Pair.create(METRIC_KEY_USER_TRIGGERED.ordinal(), userTriggered ? 1 : 0),
|
||||
Pair.create(METRIC_KEY_DEVICE_COUNT_IN_SHARING.ordinal(), deviceCountInSharing),
|
||||
Pair.create(METRIC_KEY_CANDIDATE_DEVICE_COUNT.ordinal(), candidateDeviceCount)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,17 +16,91 @@
|
||||
|
||||
package com.android.settings.connecteddevice.audiosharing.audiostreams;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.android.settings.SettingsActivity;
|
||||
import com.android.settings.bluetooth.Utils;
|
||||
import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils;
|
||||
import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
|
||||
|
||||
public class AudioStreamConfirmDialogActivity extends SettingsActivity {
|
||||
public class AudioStreamConfirmDialogActivity extends SettingsActivity
|
||||
implements LocalBluetoothProfileManager.ServiceListener {
|
||||
private static final String TAG = "AudioStreamConfirmDialogActivity";
|
||||
@Nullable private LocalBluetoothProfileManager mProfileManager;
|
||||
@Nullable private Bundle mSavedState;
|
||||
@Nullable private Intent mIntent;
|
||||
|
||||
@Override
|
||||
protected boolean isToolbarEnabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedState) {
|
||||
var localBluetoothManager = Utils.getLocalBluetoothManager(this);
|
||||
mProfileManager =
|
||||
localBluetoothManager == null ? null : localBluetoothManager.getProfileManager();
|
||||
super.onCreate(savedState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void createUiFromIntent(@Nullable Bundle savedState, Intent intent) {
|
||||
if (AudioSharingUtils.isFeatureEnabled()
|
||||
&& !AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
|
||||
Log.d(TAG, "createUiFromIntent() : supported but not ready, skip createUiFromIntent");
|
||||
mSavedState = savedState;
|
||||
mIntent = intent;
|
||||
return;
|
||||
}
|
||||
|
||||
Log.d(
|
||||
TAG,
|
||||
"createUiFromIntent() : not supported or already connected, starting"
|
||||
+ " createUiFromIntent");
|
||||
super.createUiFromIntent(savedState, intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
if (AudioSharingUtils.isFeatureEnabled()
|
||||
&& !AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
|
||||
Log.d(TAG, "onStart() : supported but not ready, listen to service ready");
|
||||
if (mProfileManager != null) {
|
||||
mProfileManager.addServiceListener(this);
|
||||
}
|
||||
}
|
||||
super.onStart();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
if (mProfileManager != null) {
|
||||
mProfileManager.removeServiceListener(this);
|
||||
}
|
||||
super.onStop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceConnected() {
|
||||
if (AudioSharingUtils.isFeatureEnabled()
|
||||
&& AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
|
||||
if (mProfileManager != null) {
|
||||
mProfileManager.removeServiceListener(this);
|
||||
}
|
||||
if (mIntent != null) {
|
||||
Log.d(TAG, "onServiceConnected() : service ready, starting createUiFromIntent");
|
||||
super.createUiFromIntent(mSavedState, mIntent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected() {}
|
||||
|
||||
@Override
|
||||
protected boolean isValidFragment(String fragmentName) {
|
||||
return AudioStreamConfirmDialog.class.getName().equals(fragmentName);
|
||||
|
||||
@@ -66,10 +66,10 @@ import java.util.stream.Collectors;
|
||||
public final class DatabaseUtils {
|
||||
private static final String TAG = "DatabaseUtils";
|
||||
private static final String SHARED_PREFS_FILE = "battery_usage_shared_prefs";
|
||||
private static final boolean EXPLICIT_CLEAR_MEMORY_ENABLED = false;
|
||||
|
||||
/** Clear memory threshold for device booting phase. */
|
||||
private static final long CLEAR_MEMORY_THRESHOLD_MS = Duration.ofMinutes(5).toMillis();
|
||||
|
||||
private static final long CLEAR_MEMORY_DELAYED_MS = Duration.ofSeconds(2).toMillis();
|
||||
private static final long INVALID_TIMESTAMP = 0L;
|
||||
|
||||
@@ -975,7 +975,8 @@ public final class DatabaseUtils {
|
||||
}
|
||||
|
||||
private static void clearMemory() {
|
||||
if (SystemClock.uptimeMillis() > CLEAR_MEMORY_THRESHOLD_MS) {
|
||||
if (!EXPLICIT_CLEAR_MEMORY_ENABLED
|
||||
|| SystemClock.uptimeMillis() > CLEAR_MEMORY_THRESHOLD_MS) {
|
||||
return;
|
||||
}
|
||||
final Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||
|
||||
63
src/com/android/settings/network/ConnectivityRepository.kt
Normal file
63
src/com/android/settings/network/ConnectivityRepository.kt
Normal file
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.settings.network
|
||||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.ConnectivityManager.NetworkCallback
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.conflate
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
|
||||
class ConnectivityRepository(context: Context) {
|
||||
private val connectivityManager = context.getSystemService(ConnectivityManager::class.java)!!
|
||||
|
||||
fun networkCapabilitiesFlow(): Flow<NetworkCapabilities> = callbackFlow {
|
||||
val callback = object : NetworkCallback() {
|
||||
override fun onCapabilitiesChanged(
|
||||
network: Network,
|
||||
networkCapabilities: NetworkCapabilities,
|
||||
) {
|
||||
trySend(networkCapabilities)
|
||||
Log.d(TAG, "onCapabilitiesChanged: $networkCapabilities")
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
trySend(NetworkCapabilities())
|
||||
Log.d(TAG, "onLost")
|
||||
}
|
||||
}
|
||||
trySend(getNetworkCapabilities())
|
||||
connectivityManager.registerDefaultNetworkCallback(callback)
|
||||
|
||||
awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
|
||||
}.conflate().flowOn(Dispatchers.Default)
|
||||
|
||||
private fun getNetworkCapabilities(): NetworkCapabilities =
|
||||
connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
|
||||
?: NetworkCapabilities()
|
||||
|
||||
private companion object {
|
||||
private const val TAG = "ConnectivityRepository"
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,6 @@ import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import com.android.settings.R
|
||||
import com.android.settings.core.BasePreferenceController
|
||||
import com.android.settings.wifi.WifiSummaryRepository
|
||||
import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle
|
||||
|
||||
class InternetPreferenceControllerV2(context: Context, preferenceKey: String) :
|
||||
@@ -40,7 +39,7 @@ class InternetPreferenceControllerV2(context: Context, preferenceKey: String) :
|
||||
}
|
||||
|
||||
override fun onViewCreated(viewLifecycleOwner: LifecycleOwner) {
|
||||
WifiSummaryRepository(mContext).summaryFlow()
|
||||
InternetPreferenceRepository(mContext).summaryFlow()
|
||||
.collectLatestWithLifecycle(viewLifecycleOwner) {
|
||||
preference?.summary = it
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.settings.network
|
||||
|
||||
import android.content.Context
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.wifi.WifiManager
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import com.android.settings.R
|
||||
import com.android.settings.wifi.WifiSummaryRepository
|
||||
import com.android.settings.wifi.repository.WifiRepository
|
||||
import com.android.settingslib.spaprivileged.settingsprovider.settingsGlobalBooleanFlow
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.conflate
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class InternetPreferenceRepository(
|
||||
private val context: Context,
|
||||
private val connectivityRepository: ConnectivityRepository = ConnectivityRepository(context),
|
||||
private val wifiSummaryRepository: WifiSummaryRepository = WifiSummaryRepository(context),
|
||||
private val wifiRepository: WifiRepository = WifiRepository(context),
|
||||
private val airplaneModeOnFlow: Flow<Boolean> =
|
||||
context.settingsGlobalBooleanFlow(Settings.Global.AIRPLANE_MODE_ON),
|
||||
) {
|
||||
|
||||
fun summaryFlow(): Flow<String> = connectivityRepository.networkCapabilitiesFlow()
|
||||
.flatMapLatest { capabilities -> capabilities.summaryFlow() }
|
||||
.onEach { Log.d(TAG, "summaryFlow: $it") }
|
||||
.conflate()
|
||||
.flowOn(Dispatchers.Default)
|
||||
|
||||
private fun NetworkCapabilities.summaryFlow(): Flow<String> {
|
||||
if (hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
|
||||
hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||
) {
|
||||
for (transportType in transportTypes) {
|
||||
if (transportType == NetworkCapabilities.TRANSPORT_WIFI) {
|
||||
return wifiSummaryRepository.summaryFlow()
|
||||
}
|
||||
}
|
||||
}
|
||||
return defaultSummaryFlow()
|
||||
}
|
||||
|
||||
private fun defaultSummaryFlow(): Flow<String> = combine(
|
||||
airplaneModeOnFlow,
|
||||
wifiRepository.wifiStateFlow(),
|
||||
) { airplaneModeOn: Boolean, wifiState: Int ->
|
||||
context.getString(
|
||||
if (airplaneModeOn && wifiState != WifiManager.WIFI_STATE_ENABLED) {
|
||||
R.string.condition_airplane_title
|
||||
} else {
|
||||
R.string.networks_available
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val TAG = "InternetPreferenceRepo"
|
||||
}
|
||||
}
|
||||
@@ -99,6 +99,8 @@ public class ApnSettings extends RestrictedSettingsFragment
|
||||
private UserManager mUserManager;
|
||||
private int mSubId;
|
||||
private PreferredApnRepository mPreferredApnRepository;
|
||||
@Nullable
|
||||
private String mPreferredApnKey;
|
||||
private String mMvnoType;
|
||||
private String mMvnoMatchData;
|
||||
|
||||
@@ -175,6 +177,7 @@ public class ApnSettings extends RestrictedSettingsFragment
|
||||
});
|
||||
|
||||
mPreferredApnRepository.collectPreferredApn(viewLifecycleOwner, (preferredApn) -> {
|
||||
mPreferredApnKey = preferredApn;
|
||||
final PreferenceGroup apnPreferenceList = findPreference(APN_LIST);
|
||||
for (int i = 0; i < apnPreferenceList.getPreferenceCount(); i++) {
|
||||
ApnPreference apnPreference = (ApnPreference) apnPreferenceList.getPreference(i);
|
||||
@@ -259,6 +262,7 @@ public class ApnSettings extends RestrictedSettingsFragment
|
||||
((type == null) || type.contains(ApnSetting.TYPE_DEFAULT_STRING));
|
||||
pref.setDefaultSelectable(defaultSelectable);
|
||||
if (defaultSelectable) {
|
||||
pref.setIsChecked(key.equals(mPreferredApnKey));
|
||||
apnList.add(pref);
|
||||
} else {
|
||||
mmsApnList.add(pref);
|
||||
|
||||
69
src/com/android/settings/notification/modes/IconUtil.java
Normal file
69
src/com/android/settings/notification/modes/IconUtil.java
Normal file
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.settings.notification.modes;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.LayerDrawable;
|
||||
import android.graphics.drawable.ShapeDrawable;
|
||||
import android.graphics.drawable.shapes.OvalShape;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settingslib.Utils;
|
||||
|
||||
class IconUtil {
|
||||
|
||||
static Drawable applyTint(@NonNull Context context, @NonNull Drawable icon) {
|
||||
icon = icon.mutate();
|
||||
icon.setTintList(
|
||||
Utils.getColorAttr(context, android.R.attr.colorControlNormal));
|
||||
return icon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a variant of the supplied {@code icon} to be used in the icon picker. The inner icon
|
||||
* is 36x36dp and it's contained into a circle of diameter 54dp.
|
||||
*/
|
||||
static Drawable makeIconCircle(@NonNull Context context, @NonNull Drawable icon) {
|
||||
ShapeDrawable background = new ShapeDrawable(new OvalShape());
|
||||
background.getPaint().setColor(Utils.getColorAttrDefaultColor(context,
|
||||
com.android.internal.R.attr.materialColorSecondaryContainer));
|
||||
icon.setTint(Utils.getColorAttrDefaultColor(context,
|
||||
com.android.internal.R.attr.materialColorOnSecondaryContainer));
|
||||
|
||||
LayerDrawable layerDrawable = new LayerDrawable(new Drawable[] { background, icon });
|
||||
|
||||
int circleDiameter = context.getResources().getDimensionPixelSize(
|
||||
R.dimen.zen_mode_icon_list_circle_diameter);
|
||||
int iconSize = context.getResources().getDimensionPixelSize(
|
||||
R.dimen.zen_mode_icon_list_icon_size);
|
||||
int iconPadding = (circleDiameter - iconSize) / 2;
|
||||
layerDrawable.setBounds(0, 0, circleDiameter, circleDiameter);
|
||||
layerDrawable.setLayerInset(1, iconPadding, iconPadding, iconPadding, iconPadding);
|
||||
|
||||
return layerDrawable;
|
||||
}
|
||||
|
||||
static Drawable makeIconCircle(@NonNull Context context, @DrawableRes int iconResId) {
|
||||
return makeIconCircle(context, checkNotNull(context.getDrawable(iconResId)));
|
||||
}
|
||||
}
|
||||
@@ -204,6 +204,14 @@ class ZenMode {
|
||||
: new ZenDeviceEffects.Builder().build();
|
||||
}
|
||||
|
||||
public boolean canEditName() {
|
||||
return !isManualDnd();
|
||||
}
|
||||
|
||||
public boolean canEditIcon() {
|
||||
return !isManualDnd();
|
||||
}
|
||||
|
||||
public boolean canBeDeleted() {
|
||||
return !mIsManualDnd;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.settings.notification.modes;
|
||||
|
||||
import static com.android.settings.notification.modes.ZenModeFragmentBase.MODE_ID;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.core.SubSettingLauncher;
|
||||
import com.android.settingslib.widget.ActionButtonsPreference;
|
||||
|
||||
class ZenModeActionsPreferenceController extends AbstractZenModePreferenceController {
|
||||
|
||||
private ActionButtonsPreference mPreference;
|
||||
|
||||
ZenModeActionsPreferenceController(@NonNull Context context, @NonNull String key,
|
||||
@Nullable ZenModesBackend backend) {
|
||||
super(context, key, backend);
|
||||
}
|
||||
|
||||
@Override
|
||||
void updateState(Preference preference, @NonNull ZenMode zenMode) {
|
||||
ActionButtonsPreference buttonsPreference = (ActionButtonsPreference) preference;
|
||||
|
||||
// TODO: b/346278854 - Add rename action (with setButton1Enabled(zenMode.canEditName())
|
||||
buttonsPreference.setButton1Text(R.string.zen_mode_action_change_name);
|
||||
buttonsPreference.setButton1Icon(R.drawable.ic_mode_edit);
|
||||
buttonsPreference.setButton1Enabled(false);
|
||||
|
||||
buttonsPreference.setButton2Text(R.string.zen_mode_action_change_icon);
|
||||
buttonsPreference.setButton2Icon(R.drawable.ic_zen_mode_action_change_icon);
|
||||
buttonsPreference.setButton2Enabled(zenMode.canEditIcon());
|
||||
buttonsPreference.setButton2OnClickListener(v -> {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(MODE_ID, zenMode.getId());
|
||||
new SubSettingLauncher(mContext)
|
||||
.setDestination(ZenModeIconPickerFragment.class.getName())
|
||||
// TODO: b/332937635 - Update metrics category
|
||||
.setSourceMetricsCategory(0)
|
||||
.setArguments(bundle)
|
||||
.launch();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.settings.notification.modes;
|
||||
|
||||
import android.content.Context;
|
||||
import android.service.notification.ZenModeConfig;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.TwoStatePreference;
|
||||
|
||||
/**
|
||||
* Preference controller controlling whether a time schedule-based mode ends at the next alarm.
|
||||
*/
|
||||
class ZenModeExitAtAlarmPreferenceController extends
|
||||
AbstractZenModePreferenceController implements Preference.OnPreferenceChangeListener {
|
||||
private ZenModeConfig.ScheduleInfo mSchedule;
|
||||
|
||||
ZenModeExitAtAlarmPreferenceController(Context context,
|
||||
String key, ZenModesBackend backend) {
|
||||
super(context, key, backend);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateState(Preference preference, @NonNull ZenMode zenMode) {
|
||||
mSchedule = ZenModeConfig.tryParseScheduleConditionId(zenMode.getRule().getConditionId());
|
||||
((TwoStatePreference) preference).setChecked(mSchedule.exitAtAlarm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) {
|
||||
final boolean exitAtAlarm = (Boolean) newValue;
|
||||
if (mSchedule.exitAtAlarm != exitAtAlarm) {
|
||||
mSchedule.exitAtAlarm = exitAtAlarm;
|
||||
return saveMode(mode -> {
|
||||
mode.getRule().setConditionId(ZenModeConfig.toScheduleConditionId(mSchedule));
|
||||
return mode;
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@ public class ZenModeFragment extends ZenModeFragmentBase {
|
||||
List<AbstractPreferenceController> prefControllers = new ArrayList<>();
|
||||
prefControllers.add(new ZenModeHeaderController(context, "header", this, mBackend));
|
||||
prefControllers.add(new ZenModeButtonPreferenceController(context, "activate", mBackend));
|
||||
prefControllers.add(new ZenModeActionsPreferenceController(context, "actions", mBackend));
|
||||
prefControllers.add(new ZenModePeopleLinkPreferenceController(
|
||||
context, "zen_mode_people", mBackend));
|
||||
prefControllers.add(new ZenModeAppsLinkPreferenceController(
|
||||
|
||||
@@ -51,12 +51,12 @@ abstract class ZenModeFragmentBase extends ZenModesFragmentBase {
|
||||
if (bundle != null && bundle.containsKey(MODE_ID)) {
|
||||
String id = bundle.getString(MODE_ID);
|
||||
if (!reloadMode(id)) {
|
||||
Log.d(TAG, "Mode id " + id + " not found");
|
||||
Log.e(TAG, "Mode id " + id + " not found");
|
||||
toastAndFinish();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "Mode id required to set mode config settings");
|
||||
Log.e(TAG, "Mode id required to set mode config settings");
|
||||
toastAndFinish();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -63,9 +63,8 @@ class ZenModeHeaderController extends AbstractZenModePreferenceController {
|
||||
|
||||
FutureUtil.whenDone(
|
||||
zenMode.getIcon(mContext, IconLoader.getInstance()),
|
||||
icon -> mHeaderController.setIcon(icon)
|
||||
.setLabel(zenMode.getRule().getName())
|
||||
.done(false /* rebindActions */),
|
||||
icon -> mHeaderController.setIcon(IconUtil.applyTint(mContext, icon))
|
||||
.done(/* rebindActions= */ false),
|
||||
mContext.getMainExecutor());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.settings.notification.modes;
|
||||
|
||||
import android.app.settings.SettingsEnums;
|
||||
import android.content.Context;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settingslib.core.AbstractPreferenceController;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ZenModeIconPickerFragment extends ZenModeFragmentBase {
|
||||
@Override
|
||||
protected int getPreferenceScreenResId() {
|
||||
return R.xml.modes_icon_picker;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMetricsCategory() {
|
||||
// TODO: b/332937635 - make this the correct metrics category
|
||||
return SettingsEnums.NOTIFICATION_ZEN_MODE_AUTOMATION;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
|
||||
return ImmutableList.of(
|
||||
new ZenModeIconPickerIconPreferenceController(context, "current_icon", this,
|
||||
mBackend),
|
||||
new ZenModeIconPickerListPreferenceController(context, "icon_list", this,
|
||||
mBackend));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.settings.notification.modes;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.dashboard.DashboardFragment;
|
||||
import com.android.settings.widget.EntityHeaderController;
|
||||
import com.android.settingslib.widget.LayoutPreference;
|
||||
|
||||
class ZenModeIconPickerIconPreferenceController extends AbstractZenModePreferenceController {
|
||||
|
||||
private final DashboardFragment mFragment;
|
||||
private EntityHeaderController mHeaderController;
|
||||
|
||||
ZenModeIconPickerIconPreferenceController(@NonNull Context context, @NonNull String key,
|
||||
@NonNull DashboardFragment fragment, @Nullable ZenModesBackend backend) {
|
||||
super(context, key, backend);
|
||||
mFragment = fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
void updateState(Preference preference, @NonNull ZenMode zenMode) {
|
||||
preference.setSelectable(false);
|
||||
|
||||
if (mHeaderController == null) {
|
||||
final LayoutPreference pref = (LayoutPreference) preference;
|
||||
mHeaderController = EntityHeaderController.newInstance(
|
||||
mFragment.getActivity(),
|
||||
mFragment,
|
||||
pref.findViewById(R.id.entity_header));
|
||||
}
|
||||
|
||||
FutureUtil.whenDone(
|
||||
zenMode.getIcon(mContext, IconLoader.getInstance()),
|
||||
icon -> mHeaderController.setIcon(IconUtil.applyTint(mContext, icon))
|
||||
.done(/* rebindActions= */ false),
|
||||
mContext.getMainExecutor());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.settings.notification.modes;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceScreen;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.dashboard.DashboardFragment;
|
||||
import com.android.settingslib.widget.LayoutPreference;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
class ZenModeIconPickerListPreferenceController extends AbstractZenModePreferenceController {
|
||||
|
||||
private final DashboardFragment mFragment;
|
||||
private IconAdapter mAdapter;
|
||||
|
||||
ZenModeIconPickerListPreferenceController(@NonNull Context context, @NonNull String key,
|
||||
@NonNull DashboardFragment fragment, @Nullable ZenModesBackend backend) {
|
||||
super(context, key, backend);
|
||||
mFragment = fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void displayPreference(PreferenceScreen screen) {
|
||||
super.displayPreference(screen);
|
||||
LayoutPreference pref = screen.findPreference(getPreferenceKey());
|
||||
if (pref == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mAdapter == null) {
|
||||
// TODO: b/333901673 - This is just an example; replace with correct list.
|
||||
List<IconInfo> exampleIcons =
|
||||
Arrays.stream(android.R.drawable.class.getFields())
|
||||
.filter(
|
||||
f -> Modifier.isStatic(f.getModifiers())
|
||||
&& f.getName().startsWith("ic_"))
|
||||
.sorted(Comparator.comparing(Field::getName))
|
||||
.limit(20)
|
||||
.map(f -> {
|
||||
try {
|
||||
return new IconInfo(f.getInt(null), f.getName());
|
||||
} catch (IllegalAccessException e) {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
mAdapter = new IconAdapter(exampleIcons);
|
||||
}
|
||||
RecyclerView recyclerView = pref.findViewById(R.id.icon_list);
|
||||
recyclerView.setLayoutManager(new AutoFitGridLayoutManager(mContext));
|
||||
recyclerView.setAdapter(mAdapter);
|
||||
recyclerView.setHasFixedSize(true);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void onIconSelected(@DrawableRes int resId) {
|
||||
saveMode(mode -> {
|
||||
mode.getRule().setIconResId(resId);
|
||||
return mode;
|
||||
});
|
||||
mFragment.finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
void updateState(Preference preference, @NonNull ZenMode zenMode) {
|
||||
// Nothing to do, the current icon is shown in a different preference.
|
||||
}
|
||||
|
||||
private record IconInfo(@DrawableRes int resId, String description) { }
|
||||
|
||||
private class IconHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final ImageView mImageView;
|
||||
|
||||
IconHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
mImageView = itemView.findViewById(R.id.icon_image_view);
|
||||
}
|
||||
|
||||
void bindIcon(IconInfo icon) {
|
||||
mImageView.setImageDrawable(
|
||||
IconUtil.makeIconCircle(itemView.getContext(), icon.resId()));
|
||||
itemView.setContentDescription(icon.description());
|
||||
itemView.setOnClickListener(v -> onIconSelected(icon.resId()));
|
||||
}
|
||||
}
|
||||
|
||||
private class IconAdapter extends RecyclerView.Adapter<IconHolder> {
|
||||
|
||||
private final ImmutableList<IconInfo> mIconResources;
|
||||
|
||||
private IconAdapter(List<IconInfo> iconOptions) {
|
||||
mIconResources = ImmutableList.copyOf(iconOptions);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public IconHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View itemView = LayoutInflater.from(parent.getContext()).inflate(
|
||||
R.layout.modes_icon_list_item, parent, false);
|
||||
return new IconHolder(itemView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull IconHolder holder, int position) {
|
||||
holder.bindIcon(mIconResources.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return mIconResources.size();
|
||||
}
|
||||
}
|
||||
|
||||
private static class AutoFitGridLayoutManager extends GridLayoutManager {
|
||||
private final float mColumnWidth;
|
||||
|
||||
AutoFitGridLayoutManager(Context context) {
|
||||
super(context, /* spanCount= */ 1);
|
||||
this.mColumnWidth = context
|
||||
.getResources()
|
||||
.getDimensionPixelSize(R.dimen.zen_mode_icon_list_item_size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
|
||||
final int totalSpace = getWidth() - getPaddingRight() - getPaddingLeft();
|
||||
final int spanCount = Math.max(1, (int) (totalSpace / mColumnWidth));
|
||||
setSpanCount(spanCount);
|
||||
super.onLayoutChildren(recycler, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,6 @@ import android.os.Bundle;
|
||||
|
||||
import com.android.settings.core.SubSettingLauncher;
|
||||
import com.android.settingslib.RestrictedPreference;
|
||||
import com.android.settingslib.Utils;
|
||||
|
||||
/**
|
||||
* Preference representing a single mode item on the modes aggregator page. Clicking on this
|
||||
@@ -59,11 +58,7 @@ class ZenModeListPreference extends RestrictedPreference {
|
||||
|
||||
FutureUtil.whenDone(
|
||||
mZenMode.getIcon(mContext, IconLoader.getInstance()),
|
||||
icon -> {
|
||||
icon.setTintList(
|
||||
Utils.getColorAttr(mContext, android.R.attr.colorControlNormal));
|
||||
setIcon(icon);
|
||||
},
|
||||
icon -> setIcon(IconUtil.applyTint(mContext, icon)),
|
||||
mContext.getMainExecutor());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.settings.notification.modes;
|
||||
|
||||
import android.app.settings.SettingsEnums;
|
||||
import android.content.Context;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settingslib.core.AbstractPreferenceController;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Settings page to set a schedule for a mode that turns on automatically based on specific days
|
||||
* of the week and times of day.
|
||||
*/
|
||||
public class ZenModeSetScheduleFragment extends ZenModeFragmentBase {
|
||||
|
||||
@Override
|
||||
protected int getPreferenceScreenResId() {
|
||||
return R.xml.modes_set_schedule;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
|
||||
List<AbstractPreferenceController> controllers = new ArrayList<>();
|
||||
controllers.add(
|
||||
new ZenModeSetSchedulePreferenceController(mContext, this, "schedule", mBackend));
|
||||
controllers.add(
|
||||
new ZenModeExitAtAlarmPreferenceController(mContext, "exit_at_alarm", mBackend));
|
||||
return controllers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMetricsCategory() {
|
||||
// TODO: b/332937635 - make this the correct metrics category
|
||||
return SettingsEnums.NOTIFICATION_ZEN_MODE_SCHEDULE_RULE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.settings.notification.modes;
|
||||
|
||||
import android.app.Flags;
|
||||
import android.content.Context;
|
||||
import android.service.notification.SystemZenRules;
|
||||
import android.service.notification.ZenModeConfig;
|
||||
import android.text.format.DateFormat;
|
||||
import android.util.ArraySet;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import android.widget.ToggleButton;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settingslib.widget.LayoutPreference;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.Calendar;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Preference controller for setting the start and end time and days of the week associated with
|
||||
* an automatic zen mode.
|
||||
*/
|
||||
class ZenModeSetSchedulePreferenceController extends AbstractZenModePreferenceController {
|
||||
// per-instance to ensure we're always using the current locale
|
||||
// E = day of the week; "EEEEE" is the shortest version; "EEEE" is the full name
|
||||
private final SimpleDateFormat mShortDayFormat = new SimpleDateFormat("EEEEE");
|
||||
private final SimpleDateFormat mLongDayFormat = new SimpleDateFormat("EEEE");
|
||||
|
||||
private static final String TAG = "ZenModeSetSchedulePreferenceController";
|
||||
private Fragment mParent;
|
||||
private ZenModeConfig.ScheduleInfo mSchedule;
|
||||
|
||||
ZenModeSetSchedulePreferenceController(Context context, Fragment parent, String key,
|
||||
ZenModesBackend backend) {
|
||||
super(context, key, backend);
|
||||
mParent = parent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateState(Preference preference, @NonNull ZenMode zenMode) {
|
||||
mSchedule = ZenModeConfig.tryParseScheduleConditionId(zenMode.getRule().getConditionId());
|
||||
LayoutPreference layoutPref = (LayoutPreference) preference;
|
||||
|
||||
TextView start = layoutPref.findViewById(R.id.start_time);
|
||||
start.setText(timeString(mSchedule.startHour, mSchedule.startMinute));
|
||||
start.setOnClickListener(
|
||||
timePickerLauncher(mSchedule.startHour, mSchedule.startMinute, mStartSetter));
|
||||
|
||||
TextView end = layoutPref.findViewById(R.id.end_time);
|
||||
end.setText(timeString(mSchedule.endHour, mSchedule.endMinute));
|
||||
end.setOnClickListener(
|
||||
timePickerLauncher(mSchedule.endHour, mSchedule.endMinute, mEndSetter));
|
||||
|
||||
TextView durationView = layoutPref.findViewById(R.id.schedule_duration);
|
||||
durationView.setText(getScheduleDurationDescription(mSchedule));
|
||||
|
||||
ViewGroup daysContainer = layoutPref.findViewById(R.id.days_of_week_container);
|
||||
setupDayToggles(daysContainer, mSchedule, Calendar.getInstance());
|
||||
}
|
||||
|
||||
private String timeString(int hour, int minute) {
|
||||
final Calendar c = Calendar.getInstance();
|
||||
c.set(Calendar.HOUR_OF_DAY, hour);
|
||||
c.set(Calendar.MINUTE, minute);
|
||||
return DateFormat.getTimeFormat(mContext).format(c.getTime());
|
||||
}
|
||||
|
||||
private boolean isValidTime(int hour, int minute) {
|
||||
return ZenModeConfig.isValidHour(hour) && ZenModeConfig.isValidMinute(minute);
|
||||
}
|
||||
|
||||
private String getScheduleDurationDescription(ZenModeConfig.ScheduleInfo schedule) {
|
||||
final int startMin = 60 * schedule.startHour + schedule.startMinute;
|
||||
final int endMin = 60 * schedule.endHour + schedule.endMinute;
|
||||
final boolean nextDay = startMin >= endMin;
|
||||
|
||||
Duration scheduleDuration;
|
||||
if (nextDay) {
|
||||
// add one day's worth of minutes (24h x 60min) to end minute for end time calculation
|
||||
int endMinNextDay = endMin + (24 * 60);
|
||||
scheduleDuration = Duration.ofMinutes(endMinNextDay - startMin);
|
||||
} else {
|
||||
scheduleDuration = Duration.ofMinutes(endMin - startMin);
|
||||
}
|
||||
|
||||
int hours = scheduleDuration.toHoursPart();
|
||||
int minutes = scheduleDuration.minusHours(hours).toMinutesPart();
|
||||
return mContext.getString(R.string.zen_mode_schedule_duration, hours, minutes);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
protected Function<ZenMode, ZenMode> updateScheduleMode(ZenModeConfig.ScheduleInfo schedule) {
|
||||
return (zenMode) -> {
|
||||
zenMode.getRule().setConditionId(ZenModeConfig.toScheduleConditionId(schedule));
|
||||
if (Flags.modesApi() && Flags.modesUi()) {
|
||||
zenMode.getRule().setTriggerDescription(
|
||||
SystemZenRules.getTriggerDescriptionForScheduleTime(mContext, schedule));
|
||||
}
|
||||
return zenMode;
|
||||
};
|
||||
}
|
||||
|
||||
private ZenModeTimePickerFragment.TimeSetter mStartSetter = (hour, minute) -> {
|
||||
if (!isValidTime(hour, minute)) {
|
||||
return;
|
||||
}
|
||||
if (hour == mSchedule.startHour && minute == mSchedule.startMinute) {
|
||||
return;
|
||||
}
|
||||
mSchedule.startHour = hour;
|
||||
mSchedule.startMinute = minute;
|
||||
saveMode(updateScheduleMode(mSchedule));
|
||||
};
|
||||
|
||||
private ZenModeTimePickerFragment.TimeSetter mEndSetter = (hour, minute) -> {
|
||||
if (!isValidTime(hour, minute)) {
|
||||
return;
|
||||
}
|
||||
if (hour == mSchedule.endHour && minute == mSchedule.endMinute) {
|
||||
return;
|
||||
}
|
||||
mSchedule.endHour = hour;
|
||||
mSchedule.endMinute = minute;
|
||||
saveMode(updateScheduleMode(mSchedule));
|
||||
};
|
||||
|
||||
private View.OnClickListener timePickerLauncher(int hour, int minute,
|
||||
ZenModeTimePickerFragment.TimeSetter timeSetter) {
|
||||
return v -> {
|
||||
final ZenModeTimePickerFragment frag = new ZenModeTimePickerFragment(mContext, hour,
|
||||
minute, timeSetter);
|
||||
frag.show(mParent.getParentFragmentManager(), TAG);
|
||||
};
|
||||
}
|
||||
|
||||
protected static int[] getDaysOfWeekForLocale(Calendar c) {
|
||||
int[] daysOfWeek = new int[7];
|
||||
int currentDay = c.getFirstDayOfWeek();
|
||||
for (int i = 0; i < daysOfWeek.length; i++) {
|
||||
if (currentDay > 7) currentDay = 1;
|
||||
daysOfWeek[i] = currentDay;
|
||||
currentDay++;
|
||||
}
|
||||
return daysOfWeek;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
protected void setupDayToggles(ViewGroup dayContainer, ZenModeConfig.ScheduleInfo schedule,
|
||||
Calendar c) {
|
||||
int[] daysOfWeek = getDaysOfWeekForLocale(c);
|
||||
|
||||
// Index in daysOfWeek is associated with the [idx]'th object in the list of days in the
|
||||
// layout. Note that because the order of the days of the week may differ per locale, this
|
||||
// is not necessarily the same as the actual value of the day number at that index.
|
||||
for (int i = 0; i < daysOfWeek.length; i++) {
|
||||
ToggleButton dayToggle = dayContainer.findViewById(resIdForDayIndex(i));
|
||||
if (dayToggle == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final int day = daysOfWeek[i];
|
||||
c.set(Calendar.DAY_OF_WEEK, day);
|
||||
|
||||
// find current setting for this day
|
||||
boolean dayEnabled = false;
|
||||
if (schedule.days != null) {
|
||||
for (int idx = 0; idx < schedule.days.length; idx++) {
|
||||
if (schedule.days[idx] == day) {
|
||||
dayEnabled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// On/off is indicated by visuals, and both states share the shortest (one-character)
|
||||
// day label.
|
||||
dayToggle.setTextOn(mShortDayFormat.format(c.getTime()));
|
||||
dayToggle.setTextOff(mShortDayFormat.format(c.getTime()));
|
||||
dayToggle.setContentDescription(mLongDayFormat.format(c.getTime()));
|
||||
|
||||
dayToggle.setChecked(dayEnabled);
|
||||
dayToggle.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
if (updateScheduleDays(schedule, day, isChecked)) {
|
||||
saveMode(updateScheduleMode(schedule));
|
||||
}
|
||||
});
|
||||
|
||||
// If display and text settings cause the text to be larger than its containing box,
|
||||
// don't show scrollbars.
|
||||
dayToggle.setVerticalScrollBarEnabled(false);
|
||||
dayToggle.setHorizontalScrollBarEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Updates the set of enabled days in provided schedule to either turn on or off the given day.
|
||||
// The format of days in ZenModeConfig.ScheduleInfo is an array of days, where inclusion means
|
||||
// the schedule is set to run on that day. Returns whether anything was changed.
|
||||
@VisibleForTesting
|
||||
protected static boolean updateScheduleDays(ZenModeConfig.ScheduleInfo schedule, int day,
|
||||
boolean set) {
|
||||
// Build a set representing the days that are currently set in mSchedule.
|
||||
ArraySet<Integer> daySet = new ArraySet();
|
||||
if (schedule.days != null) {
|
||||
for (int i = 0; i < schedule.days.length; i++) {
|
||||
daySet.add(schedule.days[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (daySet.contains(day) != set) {
|
||||
if (set) {
|
||||
daySet.add(day);
|
||||
} else {
|
||||
daySet.remove(day);
|
||||
}
|
||||
|
||||
// rebuild days array for mSchedule
|
||||
final int[] out = new int[daySet.size()];
|
||||
for (int i = 0; i < daySet.size(); i++) {
|
||||
out[i] = daySet.valueAt(i);
|
||||
}
|
||||
Arrays.sort(out);
|
||||
schedule.days = out;
|
||||
return true;
|
||||
}
|
||||
// If the setting is the same as it was before, no need to update anything.
|
||||
return false;
|
||||
}
|
||||
|
||||
protected static int resIdForDayIndex(int idx) {
|
||||
switch (idx) {
|
||||
case 0:
|
||||
return R.id.day0;
|
||||
case 1:
|
||||
return R.id.day1;
|
||||
case 2:
|
||||
return R.id.day2;
|
||||
case 3:
|
||||
return R.id.day3;
|
||||
case 4:
|
||||
return R.id.day4;
|
||||
case 5:
|
||||
return R.id.day5;
|
||||
case 6:
|
||||
return R.id.day6;
|
||||
default:
|
||||
return 0; // unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
package com.android.settings.notification.modes;
|
||||
|
||||
import static android.app.AutomaticZenRule.TYPE_SCHEDULE_CALENDAR;
|
||||
import static android.app.AutomaticZenRule.TYPE_SCHEDULE_TIME;
|
||||
|
||||
import static com.android.settings.notification.modes.ZenModeFragmentBase.MODE_ID;
|
||||
|
||||
@@ -32,13 +33,13 @@ import com.android.settings.core.SubSettingLauncher;
|
||||
import com.android.settingslib.PrimarySwitchPreference;
|
||||
|
||||
/**
|
||||
* Preference controller for the link
|
||||
* Preference controller for the link to an individual mode's configuration page.
|
||||
*/
|
||||
public class ZenModeSetTriggerLinkPreferenceController extends AbstractZenModePreferenceController {
|
||||
class ZenModeSetTriggerLinkPreferenceController extends AbstractZenModePreferenceController {
|
||||
@VisibleForTesting
|
||||
protected static final String AUTOMATIC_TRIGGER_PREF_KEY = "zen_automatic_trigger_settings";
|
||||
|
||||
public ZenModeSetTriggerLinkPreferenceController(Context context, String key,
|
||||
ZenModeSetTriggerLinkPreferenceController(Context context, String key,
|
||||
ZenModesBackend backend) {
|
||||
super(context, key, backend);
|
||||
}
|
||||
@@ -66,6 +67,16 @@ public class ZenModeSetTriggerLinkPreferenceController extends AbstractZenModePr
|
||||
|
||||
// TODO: b/341961712 - direct preference to app-owned intent if available
|
||||
switch (zenMode.getRule().getType()) {
|
||||
case TYPE_SCHEDULE_TIME:
|
||||
switchPref.setTitle(R.string.zen_mode_set_schedule_link);
|
||||
switchPref.setSummary(zenMode.getRule().getTriggerDescription());
|
||||
switchPref.setIntent(new SubSettingLauncher(mContext)
|
||||
.setDestination(ZenModeSetScheduleFragment.class.getName())
|
||||
// TODO: b/332937635 - set correct metrics category
|
||||
.setSourceMetricsCategory(0)
|
||||
.setArguments(bundle)
|
||||
.toIntent());
|
||||
break;
|
||||
case TYPE_SCHEDULE_CALENDAR:
|
||||
switchPref.setTitle(R.string.zen_mode_set_calendar_link);
|
||||
switchPref.setSummary(zenMode.getRule().getTriggerDescription());
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.settings.notification.modes;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.app.TimePickerDialog;
|
||||
import android.app.settings.SettingsEnums;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.text.format.DateFormat;
|
||||
import android.widget.TimePicker;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
|
||||
|
||||
/**
|
||||
* Dialog that shows when a user selects a (start or end) time to edit for a schedule-based mode.
|
||||
*/
|
||||
public class ZenModeTimePickerFragment extends InstrumentedDialogFragment implements
|
||||
TimePickerDialog.OnTimeSetListener {
|
||||
private final Context mContext;
|
||||
private final TimeSetter mTimeSetter;
|
||||
private final int mHour;
|
||||
private final int mMinute;
|
||||
|
||||
public ZenModeTimePickerFragment(Context context, int hour, int minute,
|
||||
@NonNull TimeSetter timeSetter) {
|
||||
super();
|
||||
mContext = context;
|
||||
mHour = hour;
|
||||
mMinute = minute;
|
||||
mTimeSetter = timeSetter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
return new TimePickerDialog(mContext, this, mHour, mMinute,
|
||||
DateFormat.is24HourFormat(mContext));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the provided TimeSetter's setTime() method when a time is set on the TimePicker.
|
||||
*/
|
||||
public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
|
||||
mTimeSetter.setTime(hourOfDay, minute);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMetricsCategory() {
|
||||
// TODO: b/332937635 - set correct metrics category (or decide to keep this one?)
|
||||
return SettingsEnums.DIALOG_ZEN_TIMEPICKER;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for a method to pass into the TimePickerFragment that specifies what to do when the
|
||||
* time is updated.
|
||||
*/
|
||||
public interface TimeSetter {
|
||||
void setTime(int hour, int minute);
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,8 @@ import static android.app.admin.DevicePolicyResources.Strings.Settings.CONFIRM_W
|
||||
import static android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED;
|
||||
import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
|
||||
|
||||
import static com.android.systemui.biometrics.Utils.toBitmap;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.KeyguardManager;
|
||||
import android.app.RemoteLockscreenValidationSession;
|
||||
@@ -35,6 +37,7 @@ import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.UserProperties;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
import android.hardware.biometrics.BiometricConstants;
|
||||
import android.hardware.biometrics.BiometricPrompt;
|
||||
@@ -215,9 +218,10 @@ public class ConfirmDeviceCredentialActivity extends FragmentActivity {
|
||||
&& android.multiuser.Flags.usePrivateSpaceIconInBiometricPrompt()
|
||||
&& hasSetBiometricDialogAdvanced(mContext, getLaunchedFromUid())
|
||||
) {
|
||||
int iconResId = intent.getIntExtra(CUSTOM_BIOMETRIC_PROMPT_LOGO_RES_ID_KEY, 0);
|
||||
final int iconResId = intent.getIntExtra(CUSTOM_BIOMETRIC_PROMPT_LOGO_RES_ID_KEY, 0);
|
||||
if (iconResId != 0) {
|
||||
promptInfo.setLogoRes(iconResId);
|
||||
final Bitmap iconBitmap = toBitmap(mContext.getDrawable(iconResId));
|
||||
promptInfo.setLogo(iconResId, iconBitmap);
|
||||
}
|
||||
String logoDescription = intent.getStringExtra(
|
||||
CUSTOM_BIOMETRIC_PROMPT_LOGO_DESCRIPTION_KEY);
|
||||
|
||||
@@ -157,22 +157,26 @@ public class PrivateSpaceCreationFragment extends InstrumentedFragment {
|
||||
|
||||
/** Start new activity in private profile to add an account to private profile */
|
||||
private void startAccountLogin() {
|
||||
Intent intent = new Intent(getContext(), PrivateProfileContextHelperActivity.class);
|
||||
intent.putExtra(EXTRA_ACTION_TYPE, ACCOUNT_LOGIN_ACTION);
|
||||
mMetricsFeatureProvider.action(
|
||||
getContext(), SettingsEnums.ACTION_PRIVATE_SPACE_SETUP_ACCOUNT_LOGIN_START);
|
||||
getActivity().startActivityForResult(intent, ACCOUNT_LOGIN_ACTION);
|
||||
if (isAdded() && getContext() != null && getActivity() != null) {
|
||||
Intent intent = new Intent(getContext(), PrivateProfileContextHelperActivity.class);
|
||||
intent.putExtra(EXTRA_ACTION_TYPE, ACCOUNT_LOGIN_ACTION);
|
||||
mMetricsFeatureProvider.action(
|
||||
getContext(), SettingsEnums.ACTION_PRIVATE_SPACE_SETUP_ACCOUNT_LOGIN_START);
|
||||
getActivity().startActivityForResult(intent, ACCOUNT_LOGIN_ACTION);
|
||||
}
|
||||
}
|
||||
|
||||
private void registerReceiver() {
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(Intent.ACTION_PROFILE_ACCESSIBLE);
|
||||
getActivity().registerReceiver(mProfileAccessReceiver, filter);
|
||||
if (getContext() != null) {
|
||||
getContext().registerReceiver(mProfileAccessReceiver, filter);
|
||||
}
|
||||
}
|
||||
|
||||
private void unRegisterReceiver() {
|
||||
if (mProfileAccessReceiver != null) {
|
||||
getActivity().unregisterReceiver(mProfileAccessReceiver);
|
||||
if (mProfileAccessReceiver != null && isAdded() && getContext() != null) {
|
||||
getContext().unregisterReceiver(mProfileAccessReceiver);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ public class UserDetailsSettings extends SettingsPreferenceFragment
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
mSwitchUserPref.setEnabled(canSwitchUserNow());
|
||||
if (mGuestUserAutoCreated) {
|
||||
if (mUserInfo.isGuest() && mGuestUserAutoCreated) {
|
||||
mRemoveUserPref.setEnabled((mUserInfo.flags & UserInfo.FLAG_INITIALIZED) != 0);
|
||||
}
|
||||
}
|
||||
|
||||
44
src/com/android/settings/wifi/repository/WifiRepository.kt
Normal file
44
src/com/android/settings/wifi/repository/WifiRepository.kt
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.settings.wifi.repository
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.wifi.WifiManager
|
||||
import android.util.Log
|
||||
import com.android.settingslib.spaprivileged.framework.common.broadcastReceiverFlow
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
class WifiRepository(
|
||||
private val context: Context,
|
||||
private val wifiStateChangedActionFlow: Flow<Intent> =
|
||||
context.broadcastReceiverFlow(IntentFilter(WifiManager.WIFI_STATE_CHANGED_ACTION)),
|
||||
) {
|
||||
|
||||
fun wifiStateFlow() = wifiStateChangedActionFlow
|
||||
.map { intent ->
|
||||
intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE, WifiManager.WIFI_STATE_UNKNOWN)
|
||||
}
|
||||
.onEach { Log.d(TAG, "wifiStateFlow: $it") }
|
||||
|
||||
private companion object {
|
||||
private const val TAG = "WifiRepository"
|
||||
}
|
||||
}
|
||||
@@ -27,9 +27,12 @@ import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.hardware.input.InputManager;
|
||||
import android.platform.test.annotations.EnableFlags;
|
||||
import android.platform.test.flag.junit.SetFlagsRule;
|
||||
import android.util.FeatureFlagUtils;
|
||||
import android.view.InputDevice;
|
||||
|
||||
@@ -39,13 +42,23 @@ import androidx.preference.PreferenceManager;
|
||||
import androidx.preference.PreferenceScreen;
|
||||
|
||||
import com.android.settings.bluetooth.ConnectedBluetoothDeviceUpdater;
|
||||
import com.android.settings.bluetooth.Utils;
|
||||
import com.android.settings.connecteddevice.dock.DockUpdater;
|
||||
import com.android.settings.connecteddevice.stylus.StylusDeviceUpdater;
|
||||
import com.android.settings.connecteddevice.usb.ConnectedUsbDeviceUpdater;
|
||||
import com.android.settings.dashboard.DashboardFragment;
|
||||
import com.android.settings.flags.Flags;
|
||||
import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
|
||||
import com.android.settings.testutils.shadow.ShadowBluetoothUtils;
|
||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
|
||||
import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
|
||||
import com.android.settingslib.bluetooth.LocalBluetoothManager;
|
||||
import com.android.settingslib.search.SearchIndexableRaw;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Answers;
|
||||
@@ -57,11 +70,16 @@ import org.robolectric.Shadows;
|
||||
import org.robolectric.annotation.Config;
|
||||
import org.robolectric.shadows.ShadowApplicationPackageManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(shadows = {ShadowApplicationPackageManager.class, ShadowBluetoothAdapter.class})
|
||||
@Config(shadows = {ShadowApplicationPackageManager.class, ShadowBluetoothUtils.class,
|
||||
ShadowBluetoothAdapter.class})
|
||||
public class ConnectedDeviceGroupControllerTest {
|
||||
|
||||
private static final String PREFERENCE_KEY_1 = "pref_key_1";
|
||||
private static final String DEVICE_NAME = "device";
|
||||
|
||||
@Mock
|
||||
private DashboardFragment mDashboardFragment;
|
||||
@@ -79,6 +97,14 @@ public class ConnectedDeviceGroupControllerTest {
|
||||
private PreferenceManager mPreferenceManager;
|
||||
@Mock
|
||||
private InputManager mInputManager;
|
||||
@Mock
|
||||
private CachedBluetoothDeviceManager mCachedDeviceManager;
|
||||
@Mock
|
||||
private LocalBluetoothManager mLocalBluetoothManager;
|
||||
@Mock
|
||||
private CachedBluetoothDevice mCachedDevice;
|
||||
@Mock
|
||||
private BluetoothDevice mDevice;
|
||||
|
||||
private ShadowApplicationPackageManager mPackageManager;
|
||||
private PreferenceGroup mPreferenceGroup;
|
||||
@@ -86,6 +112,9 @@ public class ConnectedDeviceGroupControllerTest {
|
||||
private Preference mPreference;
|
||||
private ConnectedDeviceGroupController mConnectedDeviceGroupController;
|
||||
|
||||
@Rule
|
||||
public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
@@ -102,11 +131,20 @@ public class ConnectedDeviceGroupControllerTest {
|
||||
when(mContext.getSystemService(InputManager.class)).thenReturn(mInputManager);
|
||||
when(mInputManager.getInputDeviceIds()).thenReturn(new int[]{});
|
||||
|
||||
ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBluetoothManager;
|
||||
mLocalBluetoothManager = Utils.getLocalBtManager(mContext);
|
||||
when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(mCachedDeviceManager);
|
||||
|
||||
mConnectedDeviceGroupController = new ConnectedDeviceGroupController(mContext);
|
||||
mConnectedDeviceGroupController.init(mConnectedBluetoothDeviceUpdater,
|
||||
mConnectedUsbDeviceUpdater, mConnectedDockUpdater, mStylusDeviceUpdater);
|
||||
mConnectedDeviceGroupController.mPreferenceGroup = mPreferenceGroup;
|
||||
|
||||
when(mCachedDevice.getName()).thenReturn(DEVICE_NAME);
|
||||
when(mCachedDevice.getDevice()).thenReturn(mDevice);
|
||||
when(mCachedDeviceManager.getCachedDevicesCopy()).thenReturn(
|
||||
ImmutableList.of(mCachedDevice));
|
||||
|
||||
FeatureFlagUtils.setEnabled(mContext, FeatureFlagUtils.SETTINGS_SHOW_STYLUS_PREFERENCES,
|
||||
true);
|
||||
}
|
||||
@@ -267,4 +305,27 @@ public class ConnectedDeviceGroupControllerTest {
|
||||
mConnectedDeviceGroupController.onStart();
|
||||
mConnectedDeviceGroupController.onStop();
|
||||
}
|
||||
|
||||
@Test
|
||||
@EnableFlags(Flags.FLAG_ENABLE_BONDED_BLUETOOTH_DEVICE_SEARCHABLE)
|
||||
public void updateDynamicRawDataToIndex_deviceNotBonded_deviceIsNotSearchable() {
|
||||
when(mDevice.getBondState()).thenReturn(BluetoothDevice.BOND_NONE);
|
||||
List<SearchIndexableRaw> searchData = new ArrayList<>();
|
||||
|
||||
mConnectedDeviceGroupController.updateDynamicRawDataToIndex(searchData);
|
||||
|
||||
assertThat(searchData).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@EnableFlags(Flags.FLAG_ENABLE_BONDED_BLUETOOTH_DEVICE_SEARCHABLE)
|
||||
public void updateDynamicRawDataToIndex_deviceBonded_deviceIsSearchable() {
|
||||
when(mDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
|
||||
List<SearchIndexableRaw> searchData = new ArrayList<>();
|
||||
|
||||
mConnectedDeviceGroupController.updateDynamicRawDataToIndex(searchData);
|
||||
|
||||
assertThat(searchData).isNotEmpty();
|
||||
assertThat(searchData.get(0).key).contains(DEVICE_NAME);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doNothing;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
@@ -34,6 +35,7 @@ import static org.robolectric.Shadows.shadowOf;
|
||||
import android.app.settings.SettingsEnums;
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothLeBroadcast;
|
||||
import android.bluetooth.BluetoothLeBroadcastMetadata;
|
||||
import android.bluetooth.BluetoothStatusCodes;
|
||||
import android.content.Context;
|
||||
import android.os.Looper;
|
||||
@@ -94,28 +96,28 @@ public class AudioSharingCompatibilityPreferenceControllerTest {
|
||||
@Mock private LocalBluetoothLeBroadcastAssistant mAssistant;
|
||||
@Mock private VolumeControlProfile mVolumeControl;
|
||||
@Mock private TwoStatePreference mPreference;
|
||||
@Mock private BluetoothLeBroadcastMetadata mMetadata;
|
||||
private AudioSharingCompatibilityPreferenceController mController;
|
||||
private ShadowBluetoothAdapter mShadowBluetoothAdapter;
|
||||
private LocalBluetoothManager mLocalBluetoothManager;
|
||||
private FakeFeatureFactory mFeatureFactory;
|
||||
private Lifecycle mLifecycle;
|
||||
private LifecycleOwner mLifecycleOwner;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
|
||||
mShadowBluetoothAdapter.setEnabled(true);
|
||||
mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
|
||||
ShadowBluetoothAdapter shadowBluetoothAdapter =
|
||||
Shadow.extract(BluetoothAdapter.getDefaultAdapter());
|
||||
shadowBluetoothAdapter.setEnabled(true);
|
||||
shadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
|
||||
BluetoothStatusCodes.FEATURE_SUPPORTED);
|
||||
mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
|
||||
shadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
|
||||
BluetoothStatusCodes.FEATURE_SUPPORTED);
|
||||
mLifecycleOwner = () -> mLifecycle;
|
||||
mLifecycle = new Lifecycle(mLifecycleOwner);
|
||||
ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBtManager;
|
||||
mLocalBluetoothManager = Utils.getLocalBtManager(mContext);
|
||||
LocalBluetoothManager localBluetoothManager = Utils.getLocalBtManager(mContext);
|
||||
mFeatureFactory = FakeFeatureFactory.setupForTest();
|
||||
when(mLocalBluetoothManager.getEventManager()).thenReturn(mBtEventManager);
|
||||
when(mLocalBluetoothManager.getProfileManager()).thenReturn(mBtProfileManager);
|
||||
when(localBluetoothManager.getEventManager()).thenReturn(mBtEventManager);
|
||||
when(localBluetoothManager.getProfileManager()).thenReturn(mBtProfileManager);
|
||||
when(mBtProfileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast);
|
||||
when(mBtProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(mAssistant);
|
||||
when(mBtProfileManager.getVolumeControlProfile()).thenReturn(mVolumeControl);
|
||||
@@ -133,7 +135,7 @@ public class AudioSharingCompatibilityPreferenceControllerTest {
|
||||
verify(mBroadcast)
|
||||
.registerServiceCallBack(
|
||||
any(Executor.class), any(BluetoothLeBroadcast.Callback.class));
|
||||
verify(mBtProfileManager, times(0)).addServiceListener(mController);
|
||||
verify(mBtProfileManager, never()).addServiceListener(mController);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -141,7 +143,7 @@ public class AudioSharingCompatibilityPreferenceControllerTest {
|
||||
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
|
||||
when(mBroadcast.isProfileReady()).thenReturn(false);
|
||||
mController.onStart(mLifecycleOwner);
|
||||
verify(mBroadcast, times(0))
|
||||
verify(mBroadcast, never())
|
||||
.registerServiceCallBack(
|
||||
any(Executor.class), any(BluetoothLeBroadcast.Callback.class));
|
||||
verify(mBtProfileManager).addServiceListener(mController);
|
||||
@@ -151,7 +153,7 @@ public class AudioSharingCompatibilityPreferenceControllerTest {
|
||||
public void onStart_flagOff_doNothing() {
|
||||
mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
|
||||
mController.onStart(mLifecycleOwner);
|
||||
verify(mBroadcast, times(0))
|
||||
verify(mBroadcast, never())
|
||||
.registerServiceCallBack(
|
||||
any(Executor.class), any(BluetoothLeBroadcast.Callback.class));
|
||||
}
|
||||
@@ -170,9 +172,9 @@ public class AudioSharingCompatibilityPreferenceControllerTest {
|
||||
mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
|
||||
mController.setCallbacksRegistered(true);
|
||||
mController.onStop(mLifecycleOwner);
|
||||
verify(mBroadcast, times(0))
|
||||
verify(mBroadcast, never())
|
||||
.unregisterServiceCallBack(any(BluetoothLeBroadcast.Callback.class));
|
||||
verify(mBtProfileManager, times(0)).removeServiceListener(mController);
|
||||
verify(mBtProfileManager, never()).removeServiceListener(mController);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -224,11 +226,10 @@ public class AudioSharingCompatibilityPreferenceControllerTest {
|
||||
mController.displayPreference(mScreen);
|
||||
shadowOf(Looper.getMainLooper()).idle();
|
||||
verify(mPreference).setEnabled(false);
|
||||
verify(mPreference)
|
||||
.setSummary(
|
||||
eq(mContext.getString(
|
||||
R.string
|
||||
.audio_sharing_stream_compatibility_disabled_description)));
|
||||
String expected =
|
||||
mContext.getString(
|
||||
R.string.audio_sharing_stream_compatibility_disabled_description);
|
||||
verify(mPreference).setSummary(eq(expected));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -237,10 +238,9 @@ public class AudioSharingCompatibilityPreferenceControllerTest {
|
||||
mController.displayPreference(mScreen);
|
||||
shadowOf(Looper.getMainLooper()).idle();
|
||||
verify(mPreference).setEnabled(true);
|
||||
verify(mPreference)
|
||||
.setSummary(
|
||||
eq(mContext.getString(
|
||||
R.string.audio_sharing_stream_compatibility_description)));
|
||||
String expected =
|
||||
mContext.getString(R.string.audio_sharing_stream_compatibility_description);
|
||||
verify(mPreference).setSummary(eq(expected));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -272,8 +272,73 @@ public class AudioSharingCompatibilityPreferenceControllerTest {
|
||||
public void setCheckedToCurrentValue_returnsFalse() {
|
||||
when(mBroadcast.getImproveCompatibility()).thenReturn(true);
|
||||
boolean setChecked = mController.setChecked(true);
|
||||
verify(mBroadcast, times(0)).setImproveCompatibility(anyBoolean());
|
||||
verify(mBroadcast, never()).setImproveCompatibility(anyBoolean());
|
||||
verifyNoInteractions(mFeatureFactory.metricsFeatureProvider);
|
||||
assertThat(setChecked).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBluetoothLeBroadcastCallbacks_refreshPreference() {
|
||||
when(mBroadcast.isEnabled(any())).thenReturn(false);
|
||||
mController.displayPreference(mScreen);
|
||||
shadowOf(Looper.getMainLooper()).idle();
|
||||
verify(mPreference).setEnabled(true);
|
||||
String expected =
|
||||
mContext.getString(R.string.audio_sharing_stream_compatibility_description);
|
||||
verify(mPreference).setSummary(eq(expected));
|
||||
|
||||
when(mBroadcast.isEnabled(any())).thenReturn(true);
|
||||
mController.mBroadcastCallback.onBroadcastStarted(/* reason= */ 1, /* broadcastId= */ 1);
|
||||
shadowOf(Looper.getMainLooper()).idle();
|
||||
verify(mPreference).setEnabled(false);
|
||||
expected =
|
||||
mContext.getString(
|
||||
R.string.audio_sharing_stream_compatibility_disabled_description);
|
||||
verify(mPreference).setSummary(eq(expected));
|
||||
|
||||
when(mBroadcast.isEnabled(any())).thenReturn(false);
|
||||
mController.mBroadcastCallback.onBroadcastStopped(/* reason= */ 1, /* broadcastId= */ 1);
|
||||
shadowOf(Looper.getMainLooper()).idle();
|
||||
|
||||
// Verify one extra setEnabled/setSummary is called other than the first call in
|
||||
// displayPreference.
|
||||
verify(mPreference, times(2)).setEnabled(true);
|
||||
expected = mContext.getString(R.string.audio_sharing_stream_compatibility_description);
|
||||
verify(mPreference, times(2)).setSummary(eq(expected));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBluetoothLeBroadcastCallbacks_doNothing() {
|
||||
when(mBroadcast.isEnabled(any())).thenReturn(false);
|
||||
mController.displayPreference(mScreen);
|
||||
shadowOf(Looper.getMainLooper()).idle();
|
||||
verify(mPreference).setEnabled(true);
|
||||
String expected =
|
||||
mContext.getString(R.string.audio_sharing_stream_compatibility_description);
|
||||
verify(mPreference).setSummary(eq(expected));
|
||||
|
||||
// Verify no extra setEnabled/setSummary is called other than call in displayPreference.
|
||||
mController.mBroadcastCallback.onBroadcastMetadataChanged(/* reason= */ 1, mMetadata);
|
||||
verify(mPreference).setEnabled(anyBoolean());
|
||||
verify(mPreference).setSummary(any());
|
||||
mController.mBroadcastCallback.onBroadcastUpdated(/* reason= */ 1, /* broadcastId= */ 1);
|
||||
verify(mPreference).setEnabled(anyBoolean());
|
||||
verify(mPreference).setSummary(any());
|
||||
mController.mBroadcastCallback.onPlaybackStarted(/* reason= */ 1, /* broadcastId= */ 1);
|
||||
verify(mPreference).setEnabled(anyBoolean());
|
||||
verify(mPreference).setSummary(any());
|
||||
mController.mBroadcastCallback.onPlaybackStopped(/* reason= */ 1, /* broadcastId= */ 1);
|
||||
verify(mPreference).setEnabled(anyBoolean());
|
||||
verify(mPreference).setSummary(any());
|
||||
mController.mBroadcastCallback.onBroadcastStartFailed(/* reason= */ 1);
|
||||
verify(mPreference).setEnabled(anyBoolean());
|
||||
verify(mPreference).setSummary(any());
|
||||
mController.mBroadcastCallback.onBroadcastStopFailed(/* reason= */ 1);
|
||||
verify(mPreference).setEnabled(anyBoolean());
|
||||
verify(mPreference).setSummary(any());
|
||||
mController.mBroadcastCallback.onBroadcastUpdateFailed(
|
||||
/* reason= */ 1, /* broadcastId= */ 1);
|
||||
verify(mPreference).setEnabled(anyBoolean());
|
||||
verify(mPreference).setSummary(any());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.settings.connecteddevice.audiosharing;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.robolectric.shadows.ShadowLooper.shadowMainLooper;
|
||||
|
||||
import android.app.settings.SettingsEnums;
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothStatusCodes;
|
||||
import android.platform.test.flag.junit.SetFlagsRule;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
|
||||
import com.android.settings.testutils.shadow.ShadowAlertDialogCompat;
|
||||
import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
|
||||
import com.android.settingslib.flags.Flags;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.junit.MockitoJUnit;
|
||||
import org.mockito.junit.MockitoRule;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
import org.robolectric.shadow.api.Shadow;
|
||||
import org.robolectric.shadows.androidx.fragment.FragmentController;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(
|
||||
shadows = {
|
||||
ShadowAlertDialogCompat.class,
|
||||
ShadowBluetoothAdapter.class,
|
||||
})
|
||||
public class AudioSharingConfirmDialogFragmentTest {
|
||||
@Rule public final MockitoRule mocks = MockitoJUnit.rule();
|
||||
@Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
|
||||
|
||||
private Fragment mParent;
|
||||
private AudioSharingConfirmDialogFragment mFragment;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
cleanUpDialogs();
|
||||
ShadowBluetoothAdapter shadowBluetoothAdapter =
|
||||
Shadow.extract(BluetoothAdapter.getDefaultAdapter());
|
||||
shadowBluetoothAdapter.setEnabled(true);
|
||||
shadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
|
||||
BluetoothStatusCodes.FEATURE_SUPPORTED);
|
||||
shadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
|
||||
BluetoothStatusCodes.FEATURE_SUPPORTED);
|
||||
mFragment = new AudioSharingConfirmDialogFragment();
|
||||
mParent = new Fragment();
|
||||
FragmentController.setupFragment(
|
||||
mParent, FragmentActivity.class, /* containerViewId= */ 0, /* bundle= */ null);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
cleanUpDialogs();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getMetricsCategory_correctValue() {
|
||||
assertThat(mFragment.getMetricsCategory())
|
||||
.isEqualTo(SettingsEnums.DIALOG_AUDIO_SHARING_CONFIRMATION);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onCreateDialog_flagOff_dialogNotExist() {
|
||||
mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
|
||||
AudioSharingConfirmDialogFragment.show(mParent);
|
||||
shadowMainLooper().idle();
|
||||
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
|
||||
assertThat(dialog).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onCreateDialog_flagOn_showDialog() {
|
||||
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
|
||||
AudioSharingConfirmDialogFragment.show(mParent);
|
||||
shadowMainLooper().idle();
|
||||
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
|
||||
assertThat(dialog).isNotNull();
|
||||
assertThat(dialog.isShowing()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onCreateDialog_clickOk_dialogDismiss() {
|
||||
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
|
||||
AudioSharingConfirmDialogFragment.show(mParent);
|
||||
shadowMainLooper().idle();
|
||||
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
|
||||
assertThat(dialog).isNotNull();
|
||||
View btnView = dialog.findViewById(android.R.id.button1);
|
||||
assertThat(btnView).isNotNull();
|
||||
btnView.performClick();
|
||||
shadowMainLooper().idle();
|
||||
assertThat(dialog.isShowing()).isFalse();
|
||||
}
|
||||
|
||||
private void cleanUpDialogs() {
|
||||
AlertDialog latestAlertDialog = ShadowAlertDialogCompat.getLatestAlertDialog();
|
||||
if (latestAlertDialog != null) {
|
||||
latestAlertDialog.dismiss();
|
||||
ShadowAlertDialogCompat.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,22 +18,45 @@ package com.android.settings.connecteddevice.audiosharing;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import android.app.settings.SettingsEnums;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.SettingsActivity;
|
||||
import com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsCategoryController;
|
||||
import com.android.settings.testutils.shadow.ShadowFragment;
|
||||
import com.android.settings.widget.SettingsMainSwitchBar;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnit;
|
||||
import org.mockito.junit.MockitoRule;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(shadows = {ShadowFragment.class})
|
||||
public class AudioSharingDashboardFragmentTest {
|
||||
|
||||
@Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
|
||||
|
||||
@Mock private SettingsActivity mActivity;
|
||||
@Mock private SettingsMainSwitchBar mSwitchBar;
|
||||
@Mock private AudioSharingDeviceVolumeGroupController mVolumeGroupController;
|
||||
@Mock private AudioSharingCallAudioPreferenceController mCallAudioController;
|
||||
@Mock private AudioSharingPlaySoundPreferenceController mPlaySoundController;
|
||||
@Mock private AudioStreamsCategoryController mStreamsCategoryController;
|
||||
private final Context mContext = ApplicationProvider.getApplicationContext();
|
||||
private AudioSharingDashboardFragment mFragment;
|
||||
|
||||
@Before
|
||||
@@ -59,7 +82,42 @@ public class AudioSharingDashboardFragmentTest {
|
||||
|
||||
@Test
|
||||
public void getHelpResource_returnsCorrectResource() {
|
||||
assertThat(mFragment.getHelpResource())
|
||||
.isEqualTo(R.string.help_url_audio_sharing);
|
||||
assertThat(mFragment.getHelpResource()).isEqualTo(R.string.help_url_audio_sharing);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onActivityCreated_showSwitchBar() {
|
||||
doReturn(mSwitchBar).when(mActivity).getSwitchBar();
|
||||
mFragment = spy(new AudioSharingDashboardFragment());
|
||||
doReturn(mActivity).when(mFragment).getActivity();
|
||||
doReturn(mContext).when(mFragment).getContext();
|
||||
mFragment.onAttach(mContext);
|
||||
mFragment.onActivityCreated(new Bundle());
|
||||
verify(mSwitchBar).show();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onAudioSharingStateChanged_updateVisibilityForControllers() {
|
||||
mFragment.setControllers(
|
||||
mVolumeGroupController,
|
||||
mCallAudioController,
|
||||
mPlaySoundController,
|
||||
mStreamsCategoryController);
|
||||
mFragment.onAudioSharingStateChanged();
|
||||
verify(mVolumeGroupController).updateVisibility();
|
||||
verify(mCallAudioController).updateVisibility();
|
||||
verify(mPlaySoundController).updateVisibility();
|
||||
verify(mStreamsCategoryController).updateVisibility();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onAudioSharingProfilesConnected_registerCallbacksForVolumeGroupController() {
|
||||
mFragment.setControllers(
|
||||
mVolumeGroupController,
|
||||
mCallAudioController,
|
||||
mPlaySoundController,
|
||||
mStreamsCategoryController);
|
||||
mFragment.onAudioSharingProfilesConnected();
|
||||
verify(mVolumeGroupController).onAudioSharingProfilesConnected();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,4 +63,19 @@ public class AudioSharingDeviceItemTest {
|
||||
public void creator_newArray() {
|
||||
assertThat(AudioSharingDeviceItem.CREATOR.newArray(2)).hasLength(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void creator_createFromParcel() {
|
||||
AudioSharingDeviceItem item =
|
||||
new AudioSharingDeviceItem(TEST_NAME, TEST_GROUP_ID, TEST_IS_ACTIVE);
|
||||
Parcel parcel = Parcel.obtain();
|
||||
item.writeToParcel(parcel, 0);
|
||||
parcel.setDataPosition(0);
|
||||
AudioSharingDeviceItem itemFromParcel =
|
||||
AudioSharingDeviceItem.CREATOR.createFromParcel(parcel);
|
||||
parcel.recycle();
|
||||
assertThat(itemFromParcel.getName()).isEqualTo(TEST_NAME);
|
||||
assertThat(itemFromParcel.getGroupId()).isEqualTo(TEST_GROUP_ID);
|
||||
assertThat(itemFromParcel.isActive()).isEqualTo(TEST_IS_ACTIVE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,11 +18,17 @@ package com.android.settings.connecteddevice.audiosharing;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.robolectric.shadows.ShadowLooper.shadowMainLooper;
|
||||
|
||||
import android.app.settings.SettingsEnums;
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothStatusCodes;
|
||||
import android.content.Context;
|
||||
import android.platform.test.flag.junit.SetFlagsRule;
|
||||
import android.util.Pair;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
@@ -34,6 +40,7 @@ import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.testutils.FakeFeatureFactory;
|
||||
import com.android.settings.testutils.shadow.ShadowAlertDialogCompat;
|
||||
import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
|
||||
import com.android.settingslib.flags.Flags;
|
||||
@@ -72,30 +79,50 @@ public class AudioSharingDialogFragmentTest {
|
||||
new AudioSharingDeviceItem(TEST_DEVICE_NAME2, /* groupId= */ 2, /* isActive= */ false);
|
||||
private static final AudioSharingDeviceItem TEST_DEVICE_ITEM3 =
|
||||
new AudioSharingDeviceItem(TEST_DEVICE_NAME3, /* groupId= */ 3, /* isActive= */ false);
|
||||
private static final AudioSharingDialogFragment.DialogEventListener EMPTY_EVENT_LISTENER =
|
||||
new AudioSharingDialogFragment.DialogEventListener() {
|
||||
@Override
|
||||
public void onItemClick(AudioSharingDeviceItem item) {}
|
||||
|
||||
@Override
|
||||
public void onCancelClick() {}
|
||||
};
|
||||
private static final Pair<Integer, Object> TEST_EVENT_DATA = Pair.create(1, 1);
|
||||
private static final Pair<Integer, Object>[] TEST_EVENT_DATA_LIST =
|
||||
new Pair[] {TEST_EVENT_DATA};
|
||||
|
||||
private Fragment mParent;
|
||||
private AudioSharingDialogFragment mFragment;
|
||||
private ShadowBluetoothAdapter mShadowBluetoothAdapter;
|
||||
private FakeFeatureFactory mFeatureFactory;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
ShadowAlertDialogCompat.reset();
|
||||
mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
|
||||
mShadowBluetoothAdapter.setEnabled(true);
|
||||
mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
|
||||
ShadowBluetoothAdapter shadowBluetoothAdapter =
|
||||
Shadow.extract(BluetoothAdapter.getDefaultAdapter());
|
||||
shadowBluetoothAdapter.setEnabled(true);
|
||||
shadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
|
||||
BluetoothStatusCodes.FEATURE_SUPPORTED);
|
||||
mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
|
||||
shadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
|
||||
BluetoothStatusCodes.FEATURE_SUPPORTED);
|
||||
mFeatureFactory = FakeFeatureFactory.setupForTest();
|
||||
mFragment = new AudioSharingDialogFragment();
|
||||
mParent = new Fragment();
|
||||
FragmentController.setupFragment(
|
||||
mParent, FragmentActivity.class, /* containerViewId= */ 0, /* bundle= */ null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getMetricsCategory_correctValue() {
|
||||
assertThat(mFragment.getMetricsCategory())
|
||||
.isEqualTo(SettingsEnums.DIALOG_AUDIO_SHARING_ADD_DEVICE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onCreateDialog_flagOff_dialogNotExist() {
|
||||
mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
|
||||
mFragment.show(mParent, new ArrayList<>(), (item) -> {});
|
||||
AudioSharingDialogFragment.show(
|
||||
mParent, new ArrayList<>(), EMPTY_EVENT_LISTENER, TEST_EVENT_DATA_LIST);
|
||||
shadowMainLooper().idle();
|
||||
|
||||
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
|
||||
@@ -105,14 +132,20 @@ public class AudioSharingDialogFragmentTest {
|
||||
@Test
|
||||
public void onCreateDialog_flagOn_noConnectedDevice() {
|
||||
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
|
||||
mFragment.show(mParent, new ArrayList<>(), (item) -> {});
|
||||
AudioSharingDialogFragment.show(
|
||||
mParent, new ArrayList<>(), EMPTY_EVENT_LISTENER, TEST_EVENT_DATA_LIST);
|
||||
shadowMainLooper().idle();
|
||||
|
||||
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
|
||||
assertThat(dialog).isNotNull();
|
||||
TextView description = dialog.findViewById(R.id.description_text);
|
||||
assertThat(description).isNotNull();
|
||||
ImageView image = dialog.findViewById(R.id.description_image);
|
||||
assertThat(image).isNotNull();
|
||||
Button shareBtn = dialog.findViewById(R.id.positive_btn);
|
||||
assertThat(shareBtn).isNotNull();
|
||||
Button cancelBtn = dialog.findViewById(R.id.negative_btn);
|
||||
assertThat(cancelBtn).isNotNull();
|
||||
assertThat(dialog.isShowing()).isTrue();
|
||||
assertThat(description.getVisibility()).isEqualTo(View.VISIBLE);
|
||||
assertThat(description.getText().toString())
|
||||
@@ -125,13 +158,22 @@ public class AudioSharingDialogFragmentTest {
|
||||
@Test
|
||||
public void onCreateDialog_noConnectedDevice_dialogDismiss() {
|
||||
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
|
||||
mFragment.show(mParent, new ArrayList<>(), (item) -> {});
|
||||
AudioSharingDialogFragment.show(
|
||||
mParent, new ArrayList<>(), EMPTY_EVENT_LISTENER, TEST_EVENT_DATA_LIST);
|
||||
shadowMainLooper().idle();
|
||||
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
|
||||
dialog.findViewById(android.R.id.button2).performClick();
|
||||
assertThat(dialog).isNotNull();
|
||||
View btnView = dialog.findViewById(android.R.id.button2);
|
||||
assertThat(btnView).isNotNull();
|
||||
btnView.performClick();
|
||||
shadowMainLooper().idle();
|
||||
|
||||
assertThat(dialog.isShowing()).isFalse();
|
||||
verify(mFeatureFactory.metricsFeatureProvider)
|
||||
.action(
|
||||
any(Context.class),
|
||||
eq(SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_NEGATIVE_BTN_CLICKED),
|
||||
eq(TEST_EVENT_DATA));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -139,15 +181,21 @@ public class AudioSharingDialogFragmentTest {
|
||||
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
|
||||
ArrayList<AudioSharingDeviceItem> list = new ArrayList<>();
|
||||
list.add(TEST_DEVICE_ITEM1);
|
||||
mFragment.show(mParent, list, (item) -> {});
|
||||
AudioSharingDialogFragment.show(mParent, list, EMPTY_EVENT_LISTENER, TEST_EVENT_DATA_LIST);
|
||||
shadowMainLooper().idle();
|
||||
|
||||
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
|
||||
assertThat(dialog).isNotNull();
|
||||
TextView title = dialog.findViewById(R.id.title_text);
|
||||
assertThat(title).isNotNull();
|
||||
TextView description = dialog.findViewById(R.id.description_text);
|
||||
assertThat(description).isNotNull();
|
||||
ImageView image = dialog.findViewById(R.id.description_image);
|
||||
assertThat(image).isNotNull();
|
||||
Button shareBtn = dialog.findViewById(R.id.positive_btn);
|
||||
assertThat(shareBtn).isNotNull();
|
||||
Button cancelBtn = dialog.findViewById(R.id.negative_btn);
|
||||
assertThat(cancelBtn).isNotNull();
|
||||
assertThat(dialog.isShowing()).isTrue();
|
||||
assertThat(title.getText().toString())
|
||||
.isEqualTo(
|
||||
@@ -166,12 +214,22 @@ public class AudioSharingDialogFragmentTest {
|
||||
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
|
||||
ArrayList<AudioSharingDeviceItem> list = new ArrayList<>();
|
||||
list.add(TEST_DEVICE_ITEM1);
|
||||
mFragment.show(mParent, list, (item) -> {});
|
||||
AudioSharingDialogFragment.show(mParent, list, EMPTY_EVENT_LISTENER, TEST_EVENT_DATA_LIST);
|
||||
shadowMainLooper().idle();
|
||||
|
||||
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
|
||||
dialog.findViewById(R.id.negative_btn).performClick();
|
||||
assertThat(dialog).isNotNull();
|
||||
View btnView = dialog.findViewById(R.id.negative_btn);
|
||||
assertThat(btnView).isNotNull();
|
||||
btnView.performClick();
|
||||
shadowMainLooper().idle();
|
||||
|
||||
assertThat(dialog.isShowing()).isFalse();
|
||||
verify(mFeatureFactory.metricsFeatureProvider)
|
||||
.action(
|
||||
any(Context.class),
|
||||
eq(SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_NEGATIVE_BTN_CLICKED),
|
||||
eq(TEST_EVENT_DATA));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -180,13 +238,35 @@ public class AudioSharingDialogFragmentTest {
|
||||
ArrayList<AudioSharingDeviceItem> list = new ArrayList<>();
|
||||
list.add(TEST_DEVICE_ITEM1);
|
||||
AtomicBoolean isShareBtnClicked = new AtomicBoolean(false);
|
||||
mFragment.show(mParent, list, (item) -> isShareBtnClicked.set(true));
|
||||
AudioSharingDialogFragment.show(
|
||||
mParent,
|
||||
list,
|
||||
new AudioSharingDialogFragment.DialogEventListener() {
|
||||
@Override
|
||||
public void onItemClick(AudioSharingDeviceItem item) {
|
||||
isShareBtnClicked.set(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancelClick() {}
|
||||
},
|
||||
TEST_EVENT_DATA_LIST);
|
||||
shadowMainLooper().idle();
|
||||
|
||||
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
|
||||
dialog.findViewById(R.id.positive_btn).performClick();
|
||||
assertThat(dialog).isNotNull();
|
||||
View btnView = dialog.findViewById(R.id.positive_btn);
|
||||
assertThat(btnView).isNotNull();
|
||||
btnView.performClick();
|
||||
shadowMainLooper().idle();
|
||||
|
||||
assertThat(dialog.isShowing()).isFalse();
|
||||
assertThat(isShareBtnClicked.get()).isTrue();
|
||||
verify(mFeatureFactory.metricsFeatureProvider)
|
||||
.action(
|
||||
any(Context.class),
|
||||
eq(SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_POSITIVE_BTN_CLICKED),
|
||||
eq(TEST_EVENT_DATA));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -196,15 +276,21 @@ public class AudioSharingDialogFragmentTest {
|
||||
list.add(TEST_DEVICE_ITEM1);
|
||||
list.add(TEST_DEVICE_ITEM2);
|
||||
list.add(TEST_DEVICE_ITEM3);
|
||||
mFragment.show(mParent, list, (item) -> {});
|
||||
AudioSharingDialogFragment.show(mParent, list, EMPTY_EVENT_LISTENER, TEST_EVENT_DATA_LIST);
|
||||
shadowMainLooper().idle();
|
||||
|
||||
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
|
||||
assertThat(dialog).isNotNull();
|
||||
TextView description = dialog.findViewById(R.id.description_text);
|
||||
assertThat(description).isNotNull();
|
||||
ImageView image = dialog.findViewById(R.id.description_image);
|
||||
assertThat(image).isNotNull();
|
||||
Button shareBtn = dialog.findViewById(R.id.positive_btn);
|
||||
assertThat(shareBtn).isNotNull();
|
||||
Button cancelBtn = dialog.findViewById(R.id.negative_btn);
|
||||
assertThat(cancelBtn).isNotNull();
|
||||
RecyclerView recyclerView = dialog.findViewById(R.id.device_btn_list);
|
||||
assertThat(recyclerView).isNotNull();
|
||||
assertThat(dialog.isShowing()).isTrue();
|
||||
assertThat(description.getVisibility()).isEqualTo(View.VISIBLE);
|
||||
assertThat(description.getText().toString())
|
||||
@@ -223,11 +309,35 @@ public class AudioSharingDialogFragmentTest {
|
||||
list.add(TEST_DEVICE_ITEM1);
|
||||
list.add(TEST_DEVICE_ITEM2);
|
||||
list.add(TEST_DEVICE_ITEM3);
|
||||
mFragment.show(mParent, list, (item) -> {});
|
||||
AtomicBoolean isCancelBtnClicked = new AtomicBoolean(false);
|
||||
AudioSharingDialogFragment.show(
|
||||
mParent,
|
||||
list,
|
||||
new AudioSharingDialogFragment.DialogEventListener() {
|
||||
@Override
|
||||
public void onItemClick(AudioSharingDeviceItem item) {}
|
||||
|
||||
@Override
|
||||
public void onCancelClick() {
|
||||
isCancelBtnClicked.set(true);
|
||||
}
|
||||
},
|
||||
TEST_EVENT_DATA_LIST);
|
||||
shadowMainLooper().idle();
|
||||
|
||||
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
|
||||
dialog.findViewById(R.id.negative_btn).performClick();
|
||||
assertThat(dialog).isNotNull();
|
||||
View btnView = dialog.findViewById(R.id.negative_btn);
|
||||
assertThat(btnView).isNotNull();
|
||||
btnView.performClick();
|
||||
shadowMainLooper().idle();
|
||||
|
||||
assertThat(dialog.isShowing()).isFalse();
|
||||
assertThat(isCancelBtnClicked.get()).isTrue();
|
||||
verify(mFeatureFactory.metricsFeatureProvider)
|
||||
.action(
|
||||
any(Context.class),
|
||||
eq(SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_NEGATIVE_BTN_CLICKED),
|
||||
eq(TEST_EVENT_DATA));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.robolectric.Shadows.shadowOf;
|
||||
|
||||
import android.app.settings.SettingsEnums;
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.bluetooth.BluetoothLeBroadcastReceiveState;
|
||||
@@ -32,6 +33,7 @@ import android.bluetooth.BluetoothStatusCodes;
|
||||
import android.content.Context;
|
||||
import android.os.Looper;
|
||||
import android.platform.test.flag.junit.SetFlagsRule;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.Fragment;
|
||||
@@ -39,6 +41,7 @@ import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
|
||||
import com.android.settings.bluetooth.Utils;
|
||||
import com.android.settings.testutils.FakeFeatureFactory;
|
||||
import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
|
||||
import com.android.settings.testutils.shadow.ShadowBluetoothUtils;
|
||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
|
||||
@@ -51,6 +54,7 @@ import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
|
||||
import com.android.settingslib.flags.Flags;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.truth.Correspondence;
|
||||
|
||||
import org.junit.Before;
|
||||
@@ -87,6 +91,7 @@ public class AudioSharingDialogHandlerTest {
|
||||
Correspondence.from(
|
||||
(Fragment fragment, String tag) ->
|
||||
fragment instanceof DialogFragment
|
||||
&& ((DialogFragment) fragment).getTag() != null
|
||||
&& ((DialogFragment) fragment).getTag().equals(tag),
|
||||
"is equal to");
|
||||
|
||||
@@ -107,20 +112,22 @@ public class AudioSharingDialogHandlerTest {
|
||||
private Fragment mParentFragment;
|
||||
@Mock private BluetoothLeBroadcastReceiveState mState;
|
||||
private Context mContext;
|
||||
private ShadowBluetoothAdapter mShadowBluetoothAdapter;
|
||||
private AudioSharingDialogHandler mHandler;
|
||||
private FakeFeatureFactory mFeatureFactory;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
mContext = ApplicationProvider.getApplicationContext();
|
||||
ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBtManager;
|
||||
mLocalBtManager = Utils.getLocalBtManager(mContext);
|
||||
mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
|
||||
mShadowBluetoothAdapter.setEnabled(true);
|
||||
mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
|
||||
ShadowBluetoothAdapter shadowBluetoothAdapter =
|
||||
Shadow.extract(BluetoothAdapter.getDefaultAdapter());
|
||||
shadowBluetoothAdapter.setEnabled(true);
|
||||
shadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
|
||||
BluetoothStatusCodes.FEATURE_SUPPORTED);
|
||||
mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
|
||||
shadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
|
||||
BluetoothStatusCodes.FEATURE_SUPPORTED);
|
||||
mFeatureFactory = FakeFeatureFactory.setupForTest();
|
||||
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
|
||||
when(mLocalBtManager.getProfileManager()).thenReturn(mLocalBtProfileManager);
|
||||
when(mLocalBtProfileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast);
|
||||
@@ -183,9 +190,33 @@ public class AudioSharingDialogHandlerTest {
|
||||
when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of(mState));
|
||||
mHandler.handleDeviceConnected(mCachedDevice2, /* userTriggered= */ true);
|
||||
shadowOf(Looper.getMainLooper()).idle();
|
||||
assertThat(mParentFragment.getChildFragmentManager().getFragments())
|
||||
List<Fragment> childFragments = mParentFragment.getChildFragmentManager().getFragments();
|
||||
assertThat(childFragments)
|
||||
.comparingElementsUsing(TAG_EQUALS)
|
||||
.containsExactly(AudioSharingStopDialogFragment.tag());
|
||||
|
||||
AudioSharingStopDialogFragment fragment =
|
||||
(AudioSharingStopDialogFragment) Iterables.getOnlyElement(childFragments);
|
||||
Pair<Integer, Object>[] eventData = fragment.getEventData();
|
||||
assertThat(eventData)
|
||||
.asList()
|
||||
.containsExactly(
|
||||
Pair.create(
|
||||
AudioSharingUtils.MetricKey.METRIC_KEY_SOURCE_PAGE_ID.ordinal(),
|
||||
SettingsEnums.SETTINGS_CONNECTED_DEVICE_CATEGORY),
|
||||
Pair.create(
|
||||
AudioSharingUtils.MetricKey.METRIC_KEY_PAGE_ID.ordinal(),
|
||||
SettingsEnums.DIALOG_STOP_AUDIO_SHARING),
|
||||
Pair.create(
|
||||
AudioSharingUtils.MetricKey.METRIC_KEY_USER_TRIGGERED.ordinal(), 1),
|
||||
Pair.create(
|
||||
AudioSharingUtils.MetricKey.METRIC_KEY_DEVICE_COUNT_IN_SHARING
|
||||
.ordinal(),
|
||||
1),
|
||||
Pair.create(
|
||||
AudioSharingUtils.MetricKey.METRIC_KEY_CANDIDATE_DEVICE_COUNT
|
||||
.ordinal(),
|
||||
0));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -211,9 +242,33 @@ public class AudioSharingDialogHandlerTest {
|
||||
when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of());
|
||||
mHandler.handleDeviceConnected(mCachedDevice1, /* userTriggered= */ true);
|
||||
shadowOf(Looper.getMainLooper()).idle();
|
||||
assertThat(mParentFragment.getChildFragmentManager().getFragments())
|
||||
List<Fragment> childFragments = mParentFragment.getChildFragmentManager().getFragments();
|
||||
assertThat(childFragments)
|
||||
.comparingElementsUsing(TAG_EQUALS)
|
||||
.containsExactly(AudioSharingJoinDialogFragment.tag());
|
||||
|
||||
AudioSharingJoinDialogFragment fragment =
|
||||
(AudioSharingJoinDialogFragment) Iterables.getOnlyElement(childFragments);
|
||||
Pair<Integer, Object>[] eventData = fragment.getEventData();
|
||||
assertThat(eventData)
|
||||
.asList()
|
||||
.containsExactly(
|
||||
Pair.create(
|
||||
AudioSharingUtils.MetricKey.METRIC_KEY_SOURCE_PAGE_ID.ordinal(),
|
||||
SettingsEnums.SETTINGS_CONNECTED_DEVICE_CATEGORY),
|
||||
Pair.create(
|
||||
AudioSharingUtils.MetricKey.METRIC_KEY_PAGE_ID.ordinal(),
|
||||
SettingsEnums.DIALOG_START_AUDIO_SHARING),
|
||||
Pair.create(
|
||||
AudioSharingUtils.MetricKey.METRIC_KEY_USER_TRIGGERED.ordinal(), 1),
|
||||
Pair.create(
|
||||
AudioSharingUtils.MetricKey.METRIC_KEY_DEVICE_COUNT_IN_SHARING
|
||||
.ordinal(),
|
||||
0),
|
||||
Pair.create(
|
||||
AudioSharingUtils.MetricKey.METRIC_KEY_CANDIDATE_DEVICE_COUNT
|
||||
.ordinal(),
|
||||
2));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -227,9 +282,33 @@ public class AudioSharingDialogHandlerTest {
|
||||
when(mAssistant.getAllSources(mDevice3)).thenReturn(ImmutableList.of(mState));
|
||||
mHandler.handleDeviceConnected(mCachedDevice1, /* userTriggered= */ true);
|
||||
shadowOf(Looper.getMainLooper()).idle();
|
||||
assertThat(mParentFragment.getChildFragmentManager().getFragments())
|
||||
List<Fragment> childFragments = mParentFragment.getChildFragmentManager().getFragments();
|
||||
assertThat(childFragments)
|
||||
.comparingElementsUsing(TAG_EQUALS)
|
||||
.containsExactly(AudioSharingJoinDialogFragment.tag());
|
||||
|
||||
AudioSharingJoinDialogFragment fragment =
|
||||
(AudioSharingJoinDialogFragment) Iterables.getOnlyElement(childFragments);
|
||||
Pair<Integer, Object>[] eventData = fragment.getEventData();
|
||||
assertThat(eventData)
|
||||
.asList()
|
||||
.containsExactly(
|
||||
Pair.create(
|
||||
AudioSharingUtils.MetricKey.METRIC_KEY_SOURCE_PAGE_ID.ordinal(),
|
||||
SettingsEnums.SETTINGS_CONNECTED_DEVICE_CATEGORY),
|
||||
Pair.create(
|
||||
AudioSharingUtils.MetricKey.METRIC_KEY_PAGE_ID.ordinal(),
|
||||
SettingsEnums.DIALOG_AUDIO_SHARING_ADD_DEVICE),
|
||||
Pair.create(
|
||||
AudioSharingUtils.MetricKey.METRIC_KEY_USER_TRIGGERED.ordinal(), 1),
|
||||
Pair.create(
|
||||
AudioSharingUtils.MetricKey.METRIC_KEY_DEVICE_COUNT_IN_SHARING
|
||||
.ordinal(),
|
||||
1),
|
||||
Pair.create(
|
||||
AudioSharingUtils.MetricKey.METRIC_KEY_CANDIDATE_DEVICE_COUNT
|
||||
.ordinal(),
|
||||
1));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -245,9 +324,33 @@ public class AudioSharingDialogHandlerTest {
|
||||
when(mAssistant.getAllSources(mDevice4)).thenReturn(ImmutableList.of(mState));
|
||||
mHandler.handleDeviceConnected(mCachedDevice1, /* userTriggered= */ true);
|
||||
shadowOf(Looper.getMainLooper()).idle();
|
||||
assertThat(mParentFragment.getChildFragmentManager().getFragments())
|
||||
List<Fragment> childFragments = mParentFragment.getChildFragmentManager().getFragments();
|
||||
assertThat(childFragments)
|
||||
.comparingElementsUsing(TAG_EQUALS)
|
||||
.containsExactly(AudioSharingDisconnectDialogFragment.tag());
|
||||
|
||||
AudioSharingDisconnectDialogFragment fragment =
|
||||
(AudioSharingDisconnectDialogFragment) Iterables.getOnlyElement(childFragments);
|
||||
Pair<Integer, Object>[] eventData = fragment.getEventData();
|
||||
assertThat(eventData)
|
||||
.asList()
|
||||
.containsExactly(
|
||||
Pair.create(
|
||||
AudioSharingUtils.MetricKey.METRIC_KEY_SOURCE_PAGE_ID.ordinal(),
|
||||
SettingsEnums.SETTINGS_CONNECTED_DEVICE_CATEGORY),
|
||||
Pair.create(
|
||||
AudioSharingUtils.MetricKey.METRIC_KEY_PAGE_ID.ordinal(),
|
||||
SettingsEnums.DIALOG_AUDIO_SHARING_SWITCH_DEVICE),
|
||||
Pair.create(
|
||||
AudioSharingUtils.MetricKey.METRIC_KEY_USER_TRIGGERED.ordinal(), 1),
|
||||
Pair.create(
|
||||
AudioSharingUtils.MetricKey.METRIC_KEY_DEVICE_COUNT_IN_SHARING
|
||||
.ordinal(),
|
||||
2),
|
||||
Pair.create(
|
||||
AudioSharingUtils.MetricKey.METRIC_KEY_CANDIDATE_DEVICE_COUNT
|
||||
.ordinal(),
|
||||
1));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -273,9 +376,33 @@ public class AudioSharingDialogHandlerTest {
|
||||
when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of(mState));
|
||||
mHandler.handleDeviceConnected(mCachedDevice2, /* userTriggered= */ false);
|
||||
shadowOf(Looper.getMainLooper()).idle();
|
||||
assertThat(mParentFragment.getChildFragmentManager().getFragments())
|
||||
List<Fragment> childFragments = mParentFragment.getChildFragmentManager().getFragments();
|
||||
assertThat(childFragments)
|
||||
.comparingElementsUsing(TAG_EQUALS)
|
||||
.containsExactly(AudioSharingStopDialogFragment.tag());
|
||||
|
||||
AudioSharingStopDialogFragment fragment =
|
||||
(AudioSharingStopDialogFragment) Iterables.getOnlyElement(childFragments);
|
||||
Pair<Integer, Object>[] eventData = fragment.getEventData();
|
||||
assertThat(eventData)
|
||||
.asList()
|
||||
.containsExactly(
|
||||
Pair.create(
|
||||
AudioSharingUtils.MetricKey.METRIC_KEY_SOURCE_PAGE_ID.ordinal(),
|
||||
SettingsEnums.SETTINGS_CONNECTED_DEVICE_CATEGORY),
|
||||
Pair.create(
|
||||
AudioSharingUtils.MetricKey.METRIC_KEY_PAGE_ID.ordinal(),
|
||||
SettingsEnums.DIALOG_STOP_AUDIO_SHARING),
|
||||
Pair.create(
|
||||
AudioSharingUtils.MetricKey.METRIC_KEY_USER_TRIGGERED.ordinal(), 0),
|
||||
Pair.create(
|
||||
AudioSharingUtils.MetricKey.METRIC_KEY_DEVICE_COUNT_IN_SHARING
|
||||
.ordinal(),
|
||||
1),
|
||||
Pair.create(
|
||||
AudioSharingUtils.MetricKey.METRIC_KEY_CANDIDATE_DEVICE_COUNT
|
||||
.ordinal(),
|
||||
0));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -301,9 +428,33 @@ public class AudioSharingDialogHandlerTest {
|
||||
when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of());
|
||||
mHandler.handleDeviceConnected(mCachedDevice1, /* userTriggered= */ false);
|
||||
shadowOf(Looper.getMainLooper()).idle();
|
||||
assertThat(mParentFragment.getChildFragmentManager().getFragments())
|
||||
List<Fragment> childFragments = mParentFragment.getChildFragmentManager().getFragments();
|
||||
assertThat(childFragments)
|
||||
.comparingElementsUsing(TAG_EQUALS)
|
||||
.containsExactly(AudioSharingJoinDialogFragment.tag());
|
||||
|
||||
AudioSharingJoinDialogFragment fragment =
|
||||
(AudioSharingJoinDialogFragment) Iterables.getOnlyElement(childFragments);
|
||||
Pair<Integer, Object>[] eventData = fragment.getEventData();
|
||||
assertThat(eventData)
|
||||
.asList()
|
||||
.containsExactly(
|
||||
Pair.create(
|
||||
AudioSharingUtils.MetricKey.METRIC_KEY_SOURCE_PAGE_ID.ordinal(),
|
||||
SettingsEnums.SETTINGS_CONNECTED_DEVICE_CATEGORY),
|
||||
Pair.create(
|
||||
AudioSharingUtils.MetricKey.METRIC_KEY_PAGE_ID.ordinal(),
|
||||
SettingsEnums.DIALOG_START_AUDIO_SHARING),
|
||||
Pair.create(
|
||||
AudioSharingUtils.MetricKey.METRIC_KEY_USER_TRIGGERED.ordinal(), 0),
|
||||
Pair.create(
|
||||
AudioSharingUtils.MetricKey.METRIC_KEY_DEVICE_COUNT_IN_SHARING
|
||||
.ordinal(),
|
||||
0),
|
||||
Pair.create(
|
||||
AudioSharingUtils.MetricKey.METRIC_KEY_CANDIDATE_DEVICE_COUNT
|
||||
.ordinal(),
|
||||
2));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -317,9 +468,33 @@ public class AudioSharingDialogHandlerTest {
|
||||
when(mAssistant.getAllSources(mDevice3)).thenReturn(ImmutableList.of(mState));
|
||||
mHandler.handleDeviceConnected(mCachedDevice1, /* userTriggered= */ false);
|
||||
shadowOf(Looper.getMainLooper()).idle();
|
||||
assertThat(mParentFragment.getChildFragmentManager().getFragments())
|
||||
List<Fragment> childFragments = mParentFragment.getChildFragmentManager().getFragments();
|
||||
assertThat(childFragments)
|
||||
.comparingElementsUsing(TAG_EQUALS)
|
||||
.containsExactly(AudioSharingJoinDialogFragment.tag());
|
||||
|
||||
AudioSharingJoinDialogFragment fragment =
|
||||
(AudioSharingJoinDialogFragment) Iterables.getOnlyElement(childFragments);
|
||||
Pair<Integer, Object>[] eventData = fragment.getEventData();
|
||||
assertThat(eventData)
|
||||
.asList()
|
||||
.containsExactly(
|
||||
Pair.create(
|
||||
AudioSharingUtils.MetricKey.METRIC_KEY_SOURCE_PAGE_ID.ordinal(),
|
||||
SettingsEnums.SETTINGS_CONNECTED_DEVICE_CATEGORY),
|
||||
Pair.create(
|
||||
AudioSharingUtils.MetricKey.METRIC_KEY_PAGE_ID.ordinal(),
|
||||
SettingsEnums.DIALOG_AUDIO_SHARING_ADD_DEVICE),
|
||||
Pair.create(
|
||||
AudioSharingUtils.MetricKey.METRIC_KEY_USER_TRIGGERED.ordinal(), 0),
|
||||
Pair.create(
|
||||
AudioSharingUtils.MetricKey.METRIC_KEY_DEVICE_COUNT_IN_SHARING
|
||||
.ordinal(),
|
||||
1),
|
||||
Pair.create(
|
||||
AudioSharingUtils.MetricKey.METRIC_KEY_CANDIDATE_DEVICE_COUNT
|
||||
.ordinal(),
|
||||
1));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -334,9 +509,33 @@ public class AudioSharingDialogHandlerTest {
|
||||
when(mAssistant.getAllSources(mDevice4)).thenReturn(ImmutableList.of(mState));
|
||||
mHandler.handleDeviceConnected(mCachedDevice1, /* userTriggered= */ false);
|
||||
shadowOf(Looper.getMainLooper()).idle();
|
||||
assertThat(mParentFragment.getChildFragmentManager().getFragments())
|
||||
List<Fragment> childFragments = mParentFragment.getChildFragmentManager().getFragments();
|
||||
assertThat(childFragments)
|
||||
.comparingElementsUsing(TAG_EQUALS)
|
||||
.containsExactly(AudioSharingDisconnectDialogFragment.tag());
|
||||
|
||||
AudioSharingDisconnectDialogFragment fragment =
|
||||
(AudioSharingDisconnectDialogFragment) Iterables.getOnlyElement(childFragments);
|
||||
Pair<Integer, Object>[] eventData = fragment.getEventData();
|
||||
assertThat(eventData)
|
||||
.asList()
|
||||
.containsExactly(
|
||||
Pair.create(
|
||||
AudioSharingUtils.MetricKey.METRIC_KEY_SOURCE_PAGE_ID.ordinal(),
|
||||
SettingsEnums.SETTINGS_CONNECTED_DEVICE_CATEGORY),
|
||||
Pair.create(
|
||||
AudioSharingUtils.MetricKey.METRIC_KEY_PAGE_ID.ordinal(),
|
||||
SettingsEnums.DIALOG_AUDIO_SHARING_SWITCH_DEVICE),
|
||||
Pair.create(
|
||||
AudioSharingUtils.MetricKey.METRIC_KEY_USER_TRIGGERED.ordinal(), 0),
|
||||
Pair.create(
|
||||
AudioSharingUtils.MetricKey.METRIC_KEY_DEVICE_COUNT_IN_SHARING
|
||||
.ordinal(),
|
||||
2),
|
||||
Pair.create(
|
||||
AudioSharingUtils.MetricKey.METRIC_KEY_CANDIDATE_DEVICE_COUNT
|
||||
.ordinal(),
|
||||
1));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -357,6 +556,11 @@ public class AudioSharingDialogHandlerTest {
|
||||
mHandler.closeOpeningDialogsForLeaDevice(mCachedDevice1);
|
||||
shadowOf(Looper.getMainLooper()).idle();
|
||||
assertThat(mParentFragment.getChildFragmentManager().getFragments()).isEmpty();
|
||||
verify(mFeatureFactory.metricsFeatureProvider)
|
||||
.action(
|
||||
mContext,
|
||||
SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_AUTO_DISMISS,
|
||||
SettingsEnums.DIALOG_START_AUDIO_SHARING);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -377,6 +581,11 @@ public class AudioSharingDialogHandlerTest {
|
||||
mHandler.closeOpeningDialogsForNonLeaDevice(mCachedDevice2);
|
||||
shadowOf(Looper.getMainLooper()).idle();
|
||||
assertThat(mParentFragment.getChildFragmentManager().getFragments()).isEmpty();
|
||||
verify(mFeatureFactory.metricsFeatureProvider)
|
||||
.action(
|
||||
mContext,
|
||||
SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_AUTO_DISMISS,
|
||||
SettingsEnums.DIALOG_STOP_AUDIO_SHARING);
|
||||
}
|
||||
|
||||
private void setUpBroadcast(boolean isBroadcasting) {
|
||||
|
||||
@@ -18,13 +18,21 @@ package com.android.settings.connecteddevice.audiosharing;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.robolectric.shadows.ShadowLooper.shadowMainLooper;
|
||||
|
||||
import android.app.settings.SettingsEnums;
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.bluetooth.BluetoothStatusCodes;
|
||||
import android.content.Context;
|
||||
import android.platform.test.flag.junit.SetFlagsRule;
|
||||
import android.util.Pair;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
@@ -33,6 +41,7 @@ import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.testutils.FakeFeatureFactory;
|
||||
import com.android.settings.testutils.shadow.ShadowAlertDialogCompat;
|
||||
import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
|
||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
|
||||
@@ -78,15 +87,19 @@ public class AudioSharingDisconnectDialogFragmentTest {
|
||||
new AudioSharingDeviceItem(TEST_DEVICE_NAME2, TEST_GROUP_ID2, /* isActive= */ false);
|
||||
private static final AudioSharingDeviceItem TEST_DEVICE_ITEM3 =
|
||||
new AudioSharingDeviceItem(TEST_DEVICE_NAME3, TEST_GROUP_ID3, /* isActive= */ false);
|
||||
private static final AudioSharingDisconnectDialogFragment.DialogEventListener
|
||||
EMPTY_EVENT_LISTENER = (AudioSharingDeviceItem item) -> {};
|
||||
private static final Pair<Integer, Object> TEST_EVENT_DATA = Pair.create(1, 1);
|
||||
private static final Pair<Integer, Object>[] TEST_EVENT_DATA_LIST =
|
||||
new Pair[] {TEST_EVENT_DATA};
|
||||
|
||||
@Mock private BluetoothDevice mDevice1;
|
||||
@Mock private BluetoothDevice mDevice3;
|
||||
|
||||
@Mock private CachedBluetoothDevice mCachedDevice1;
|
||||
@Mock private CachedBluetoothDevice mCachedDevice3;
|
||||
private FakeFeatureFactory mFeatureFactory;
|
||||
private Fragment mParent;
|
||||
private AudioSharingDisconnectDialogFragment mFragment;
|
||||
private ShadowBluetoothAdapter mShadowBluetoothAdapter;
|
||||
private ArrayList<AudioSharingDeviceItem> mDeviceItems = new ArrayList<>();
|
||||
|
||||
@Before
|
||||
@@ -96,12 +109,14 @@ public class AudioSharingDisconnectDialogFragmentTest {
|
||||
latestAlertDialog.dismiss();
|
||||
ShadowAlertDialogCompat.reset();
|
||||
}
|
||||
mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
|
||||
mShadowBluetoothAdapter.setEnabled(true);
|
||||
mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
|
||||
ShadowBluetoothAdapter shadowBluetoothAdapter =
|
||||
Shadow.extract(BluetoothAdapter.getDefaultAdapter());
|
||||
shadowBluetoothAdapter.setEnabled(true);
|
||||
shadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
|
||||
BluetoothStatusCodes.FEATURE_SUPPORTED);
|
||||
mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
|
||||
shadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
|
||||
BluetoothStatusCodes.FEATURE_SUPPORTED);
|
||||
mFeatureFactory = FakeFeatureFactory.setupForTest();
|
||||
when(mDevice1.getAnonymizedAddress()).thenReturn(TEST_ADDRESS1);
|
||||
when(mDevice3.getAnonymizedAddress()).thenReturn(TEST_ADDRESS3);
|
||||
when(mCachedDevice1.getName()).thenReturn(TEST_DEVICE_NAME1);
|
||||
@@ -116,13 +131,20 @@ public class AudioSharingDisconnectDialogFragmentTest {
|
||||
mParent, FragmentActivity.class, /* containerViewId= */ 0, /* bundle= */ null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getMetricsCategory_correctValue() {
|
||||
assertThat(mFragment.getMetricsCategory())
|
||||
.isEqualTo(SettingsEnums.DIALOG_AUDIO_SHARING_SWITCH_DEVICE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onCreateDialog_flagOff_dialogNotExist() {
|
||||
mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
|
||||
mDeviceItems = new ArrayList<>();
|
||||
mDeviceItems.add(TEST_DEVICE_ITEM1);
|
||||
mDeviceItems.add(TEST_DEVICE_ITEM2);
|
||||
mFragment.show(mParent, mDeviceItems, mCachedDevice3, (item) -> {});
|
||||
AudioSharingDisconnectDialogFragment.show(
|
||||
mParent, mDeviceItems, mCachedDevice3, EMPTY_EVENT_LISTENER, TEST_EVENT_DATA_LIST);
|
||||
shadowMainLooper().idle();
|
||||
|
||||
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
|
||||
@@ -135,12 +157,15 @@ public class AudioSharingDisconnectDialogFragmentTest {
|
||||
mDeviceItems = new ArrayList<>();
|
||||
mDeviceItems.add(TEST_DEVICE_ITEM1);
|
||||
mDeviceItems.add(TEST_DEVICE_ITEM2);
|
||||
mFragment.show(mParent, mDeviceItems, mCachedDevice3, (item) -> {});
|
||||
AudioSharingDisconnectDialogFragment.show(
|
||||
mParent, mDeviceItems, mCachedDevice3, EMPTY_EVENT_LISTENER, TEST_EVENT_DATA_LIST);
|
||||
shadowMainLooper().idle();
|
||||
|
||||
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
|
||||
assertThat(dialog).isNotNull();
|
||||
assertThat(dialog.isShowing()).isTrue();
|
||||
RecyclerView view = dialog.findViewById(R.id.device_btn_list);
|
||||
assertThat(view).isNotNull();
|
||||
assertThat(view.getAdapter().getItemCount()).isEqualTo(2);
|
||||
}
|
||||
|
||||
@@ -150,12 +175,14 @@ public class AudioSharingDisconnectDialogFragmentTest {
|
||||
mDeviceItems = new ArrayList<>();
|
||||
mDeviceItems.add(TEST_DEVICE_ITEM1);
|
||||
mDeviceItems.add(TEST_DEVICE_ITEM2);
|
||||
mFragment.show(mParent, mDeviceItems, mCachedDevice3, (item) -> {});
|
||||
AudioSharingDisconnectDialogFragment.show(
|
||||
mParent, mDeviceItems, mCachedDevice3, EMPTY_EVENT_LISTENER, TEST_EVENT_DATA_LIST);
|
||||
shadowMainLooper().idle();
|
||||
AtomicBoolean isItemBtnClicked = new AtomicBoolean(false);
|
||||
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
|
||||
assertThat(dialog).isNotNull();
|
||||
assertThat(dialog.isShowing()).isTrue();
|
||||
RecyclerView view = dialog.findViewById(R.id.device_btn_list);
|
||||
assertThat(view).isNotNull();
|
||||
assertThat(view.getAdapter().getItemCount()).isEqualTo(2);
|
||||
Button btn1 =
|
||||
view.findViewHolderForAdapterPosition(0).itemView.findViewById(R.id.device_button);
|
||||
@@ -173,37 +200,71 @@ public class AudioSharingDisconnectDialogFragmentTest {
|
||||
TEST_DEVICE_NAME2));
|
||||
|
||||
// Update dialog content for device with same group
|
||||
mFragment.show(mParent, mDeviceItems, mCachedDevice3, (item) -> isItemBtnClicked.set(true));
|
||||
AtomicBoolean isItemBtnClicked = new AtomicBoolean(false);
|
||||
AudioSharingDisconnectDialogFragment.show(
|
||||
mParent,
|
||||
mDeviceItems,
|
||||
mCachedDevice3,
|
||||
(AudioSharingDeviceItem item) -> isItemBtnClicked.set(true),
|
||||
TEST_EVENT_DATA_LIST);
|
||||
shadowMainLooper().idle();
|
||||
dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
|
||||
assertThat(dialog.isShowing()).isTrue();
|
||||
verify(mFeatureFactory.metricsFeatureProvider, times(0))
|
||||
.action(
|
||||
any(Context.class),
|
||||
eq(SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_AUTO_DISMISS),
|
||||
eq(SettingsEnums.DIALOG_AUDIO_SHARING_SWITCH_DEVICE));
|
||||
|
||||
btn1 = view.findViewHolderForAdapterPosition(0).itemView.findViewById(R.id.device_button);
|
||||
btn1.performClick();
|
||||
shadowMainLooper().idle();
|
||||
assertThat(dialog.isShowing()).isFalse();
|
||||
assertThat(isItemBtnClicked.get()).isTrue();
|
||||
verify(mFeatureFactory.metricsFeatureProvider)
|
||||
.action(
|
||||
any(Context.class),
|
||||
eq(SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_POSITIVE_BTN_CLICKED),
|
||||
eq(TEST_EVENT_DATA));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onCreateDialog_dialogIsShowingForNewGroup_updateDialog() {
|
||||
public void onCreateDialog_dialogIsShowingForNewGroup_showNewDialog() {
|
||||
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
|
||||
mDeviceItems = new ArrayList<>();
|
||||
mDeviceItems.add(TEST_DEVICE_ITEM1);
|
||||
mDeviceItems.add(TEST_DEVICE_ITEM2);
|
||||
mFragment.show(mParent, mDeviceItems, mCachedDevice3, (item) -> {});
|
||||
AudioSharingDisconnectDialogFragment.show(
|
||||
mParent, mDeviceItems, mCachedDevice3, EMPTY_EVENT_LISTENER, TEST_EVENT_DATA_LIST);
|
||||
shadowMainLooper().idle();
|
||||
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
|
||||
assertThat(dialog).isNotNull();
|
||||
assertThat(dialog.isShowing()).isTrue();
|
||||
RecyclerView view = dialog.findViewById(R.id.device_btn_list);
|
||||
assertThat(view).isNotNull();
|
||||
assertThat(view.getAdapter().getItemCount()).isEqualTo(2);
|
||||
|
||||
// Show new dialog for device with new group
|
||||
ArrayList<AudioSharingDeviceItem> newDeviceItems = new ArrayList<>();
|
||||
newDeviceItems.add(TEST_DEVICE_ITEM2);
|
||||
newDeviceItems.add(TEST_DEVICE_ITEM3);
|
||||
mFragment.show(mParent, newDeviceItems, mCachedDevice1, (item) -> {});
|
||||
AudioSharingDisconnectDialogFragment.show(
|
||||
mParent,
|
||||
newDeviceItems,
|
||||
mCachedDevice1,
|
||||
EMPTY_EVENT_LISTENER,
|
||||
TEST_EVENT_DATA_LIST);
|
||||
shadowMainLooper().idle();
|
||||
dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
|
||||
assertThat(dialog.isShowing()).isTrue();
|
||||
verify(mFeatureFactory.metricsFeatureProvider)
|
||||
.action(
|
||||
any(Context.class),
|
||||
eq(SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_AUTO_DISMISS),
|
||||
eq(SettingsEnums.DIALOG_AUDIO_SHARING_SWITCH_DEVICE));
|
||||
|
||||
view = dialog.findViewById(R.id.device_btn_list);
|
||||
assertThat(view).isNotNull();
|
||||
assertThat(view.getAdapter().getItemCount()).isEqualTo(2);
|
||||
Button btn1 =
|
||||
view.findViewHolderForAdapterPosition(0).itemView.findViewById(R.id.device_button);
|
||||
@@ -227,12 +288,27 @@ public class AudioSharingDisconnectDialogFragmentTest {
|
||||
mDeviceItems = new ArrayList<>();
|
||||
mDeviceItems.add(TEST_DEVICE_ITEM1);
|
||||
mDeviceItems.add(TEST_DEVICE_ITEM2);
|
||||
mFragment.show(mParent, mDeviceItems, mCachedDevice3, (item) -> {});
|
||||
AudioSharingDisconnectDialogFragment.show(
|
||||
mParent, mDeviceItems, mCachedDevice3, EMPTY_EVENT_LISTENER, TEST_EVENT_DATA_LIST);
|
||||
shadowMainLooper().idle();
|
||||
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
|
||||
assertThat(dialog).isNotNull();
|
||||
assertThat(dialog.isShowing()).isTrue();
|
||||
View btnView = dialog.findViewById(R.id.negative_btn);
|
||||
assertThat(btnView).isNotNull();
|
||||
btnView.performClick();
|
||||
shadowMainLooper().idle();
|
||||
|
||||
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
|
||||
assertThat(dialog.isShowing()).isTrue();
|
||||
dialog.findViewById(R.id.negative_btn).performClick();
|
||||
assertThat(dialog.isShowing()).isFalse();
|
||||
verify(mFeatureFactory.metricsFeatureProvider, times(0))
|
||||
.action(
|
||||
any(Context.class),
|
||||
eq(SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_AUTO_DISMISS),
|
||||
eq(SettingsEnums.DIALOG_AUDIO_SHARING_SWITCH_DEVICE));
|
||||
verify(mFeatureFactory.metricsFeatureProvider)
|
||||
.action(
|
||||
any(Context.class),
|
||||
eq(SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_NEGATIVE_BTN_CLICKED),
|
||||
eq(TEST_EVENT_DATA));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,13 +18,19 @@ package com.android.settings.connecteddevice.audiosharing;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.robolectric.shadows.ShadowLooper.shadowMainLooper;
|
||||
|
||||
import android.app.settings.SettingsEnums;
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothStatusCodes;
|
||||
import android.content.Context;
|
||||
import android.platform.test.flag.junit.SetFlagsRule;
|
||||
import android.util.Pair;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.Fragment;
|
||||
@@ -32,6 +38,7 @@ import androidx.fragment.app.FragmentActivity;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.bluetooth.Utils;
|
||||
import com.android.settings.testutils.FakeFeatureFactory;
|
||||
import com.android.settings.testutils.shadow.ShadowAlertDialogCompat;
|
||||
import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
|
||||
import com.android.settings.testutils.shadow.ShadowBluetoothUtils;
|
||||
@@ -82,6 +89,9 @@ public class AudioSharingJoinDialogFragmentTest {
|
||||
@Override
|
||||
public void onCancelClick() {}
|
||||
};
|
||||
private static final Pair<Integer, Object> TEST_EVENT_DATA = Pair.create(1, 1);
|
||||
private static final Pair<Integer, Object>[] TEST_EVENT_DATA_LIST =
|
||||
new Pair[] {TEST_EVENT_DATA};
|
||||
|
||||
@Mock private CachedBluetoothDevice mCachedDevice1;
|
||||
@Mock private CachedBluetoothDevice mCachedDevice2;
|
||||
@@ -90,7 +100,7 @@ public class AudioSharingJoinDialogFragmentTest {
|
||||
@Mock private LocalBluetoothLeBroadcast mBroadcast;
|
||||
private Fragment mParent;
|
||||
private AudioSharingJoinDialogFragment mFragment;
|
||||
private ShadowBluetoothAdapter mShadowBluetoothAdapter;
|
||||
private FakeFeatureFactory mFeatureFactory;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
@@ -99,12 +109,14 @@ public class AudioSharingJoinDialogFragmentTest {
|
||||
latestAlertDialog.dismiss();
|
||||
ShadowAlertDialogCompat.reset();
|
||||
}
|
||||
mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
|
||||
mShadowBluetoothAdapter.setEnabled(true);
|
||||
mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
|
||||
ShadowBluetoothAdapter shadowBluetoothAdapter =
|
||||
Shadow.extract(BluetoothAdapter.getDefaultAdapter());
|
||||
shadowBluetoothAdapter.setEnabled(true);
|
||||
shadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
|
||||
BluetoothStatusCodes.FEATURE_SUPPORTED);
|
||||
mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
|
||||
shadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
|
||||
BluetoothStatusCodes.FEATURE_SUPPORTED);
|
||||
mFeatureFactory = FakeFeatureFactory.setupForTest();
|
||||
when(mCachedDevice1.getName()).thenReturn(TEST_DEVICE_NAME1);
|
||||
when(mCachedDevice2.getName()).thenReturn(TEST_DEVICE_NAME2);
|
||||
mFragment = new AudioSharingJoinDialogFragment();
|
||||
@@ -137,7 +149,12 @@ public class AudioSharingJoinDialogFragmentTest {
|
||||
@Test
|
||||
public void onCreateDialog_flagOff_dialogNotExist() {
|
||||
mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
|
||||
mFragment.show(mParent, new ArrayList<>(), mCachedDevice2, EMPTY_EVENT_LISTENER);
|
||||
AudioSharingJoinDialogFragment.show(
|
||||
mParent,
|
||||
new ArrayList<>(),
|
||||
mCachedDevice2,
|
||||
EMPTY_EVENT_LISTENER,
|
||||
TEST_EVENT_DATA_LIST);
|
||||
shadowMainLooper().idle();
|
||||
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
|
||||
assertThat(dialog).isNull();
|
||||
@@ -146,7 +163,12 @@ public class AudioSharingJoinDialogFragmentTest {
|
||||
@Test
|
||||
public void onCreateDialog_flagOn_dialogShowTextForSingleDevice() {
|
||||
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
|
||||
mFragment.show(mParent, new ArrayList<>(), mCachedDevice2, EMPTY_EVENT_LISTENER);
|
||||
AudioSharingJoinDialogFragment.show(
|
||||
mParent,
|
||||
new ArrayList<>(),
|
||||
mCachedDevice2,
|
||||
EMPTY_EVENT_LISTENER,
|
||||
TEST_EVENT_DATA_LIST);
|
||||
shadowMainLooper().idle();
|
||||
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
|
||||
assertThat(dialog).isNotNull();
|
||||
@@ -160,7 +182,8 @@ public class AudioSharingJoinDialogFragmentTest {
|
||||
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
|
||||
ArrayList<AudioSharingDeviceItem> list = new ArrayList<>();
|
||||
list.add(TEST_DEVICE_ITEM1);
|
||||
mFragment.show(mParent, list, mCachedDevice2, EMPTY_EVENT_LISTENER);
|
||||
AudioSharingJoinDialogFragment.show(
|
||||
mParent, list, mCachedDevice2, EMPTY_EVENT_LISTENER, TEST_EVENT_DATA_LIST);
|
||||
shadowMainLooper().idle();
|
||||
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
|
||||
assertThat(dialog).isNotNull();
|
||||
@@ -179,7 +202,8 @@ public class AudioSharingJoinDialogFragmentTest {
|
||||
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
|
||||
ArrayList<AudioSharingDeviceItem> list = new ArrayList<>();
|
||||
list.add(TEST_DEVICE_ITEM1);
|
||||
mFragment.show(mParent, list, mCachedDevice2, EMPTY_EVENT_LISTENER);
|
||||
AudioSharingJoinDialogFragment.show(
|
||||
mParent, list, mCachedDevice2, EMPTY_EVENT_LISTENER, TEST_EVENT_DATA_LIST);
|
||||
shadowMainLooper().idle();
|
||||
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
|
||||
assertThat(dialog).isNotNull();
|
||||
@@ -188,7 +212,8 @@ public class AudioSharingJoinDialogFragmentTest {
|
||||
// Update the content
|
||||
ArrayList<AudioSharingDeviceItem> list2 = new ArrayList<>();
|
||||
list2.add(TEST_DEVICE_ITEM2);
|
||||
mFragment.show(mParent, list2, mCachedDevice1, EMPTY_EVENT_LISTENER);
|
||||
AudioSharingJoinDialogFragment.show(
|
||||
mParent, list2, mCachedDevice1, EMPTY_EVENT_LISTENER, TEST_EVENT_DATA_LIST);
|
||||
shadowMainLooper().idle();
|
||||
dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
|
||||
assertThat(dialog).isNotNull();
|
||||
@@ -205,11 +230,25 @@ public class AudioSharingJoinDialogFragmentTest {
|
||||
@Test
|
||||
public void onCreateDialog_clickCancel_dialogDismiss() {
|
||||
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
|
||||
mFragment.show(mParent, new ArrayList<>(), mCachedDevice2, EMPTY_EVENT_LISTENER);
|
||||
AudioSharingJoinDialogFragment.show(
|
||||
mParent,
|
||||
new ArrayList<>(),
|
||||
mCachedDevice2,
|
||||
EMPTY_EVENT_LISTENER,
|
||||
TEST_EVENT_DATA_LIST);
|
||||
shadowMainLooper().idle();
|
||||
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
|
||||
dialog.findViewById(R.id.negative_btn).performClick();
|
||||
assertThat(dialog).isNotNull();
|
||||
View btnView = dialog.findViewById(R.id.negative_btn);
|
||||
assertThat(btnView).isNotNull();
|
||||
btnView.performClick();
|
||||
shadowMainLooper().idle();
|
||||
assertThat(dialog.isShowing()).isFalse();
|
||||
verify(mFeatureFactory.metricsFeatureProvider)
|
||||
.action(
|
||||
any(Context.class),
|
||||
eq(SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_NEGATIVE_BTN_CLICKED),
|
||||
eq(TEST_EVENT_DATA));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -228,12 +267,22 @@ public class AudioSharingJoinDialogFragmentTest {
|
||||
|
||||
@Override
|
||||
public void onCancelClick() {}
|
||||
});
|
||||
},
|
||||
TEST_EVENT_DATA_LIST);
|
||||
shadowMainLooper().idle();
|
||||
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
|
||||
dialog.findViewById(R.id.positive_btn).performClick();
|
||||
assertThat(dialog).isNotNull();
|
||||
View btnView = dialog.findViewById(R.id.positive_btn);
|
||||
assertThat(btnView).isNotNull();
|
||||
btnView.performClick();
|
||||
shadowMainLooper().idle();
|
||||
assertThat(dialog.isShowing()).isFalse();
|
||||
assertThat(isShareBtnClicked.get()).isTrue();
|
||||
verify(mFeatureFactory.metricsFeatureProvider)
|
||||
.action(
|
||||
any(Context.class),
|
||||
eq(SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_POSITIVE_BTN_CLICKED),
|
||||
eq(TEST_EVENT_DATA));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -252,11 +301,21 @@ public class AudioSharingJoinDialogFragmentTest {
|
||||
public void onCancelClick() {
|
||||
isCancelBtnClicked.set(true);
|
||||
}
|
||||
});
|
||||
},
|
||||
TEST_EVENT_DATA_LIST);
|
||||
shadowMainLooper().idle();
|
||||
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
|
||||
dialog.findViewById(R.id.negative_btn).performClick();
|
||||
assertThat(dialog).isNotNull();
|
||||
View btnView = dialog.findViewById(R.id.negative_btn);
|
||||
assertThat(btnView).isNotNull();
|
||||
btnView.performClick();
|
||||
shadowMainLooper().idle();
|
||||
assertThat(dialog.isShowing()).isFalse();
|
||||
assertThat(isCancelBtnClicked.get()).isTrue();
|
||||
verify(mFeatureFactory.metricsFeatureProvider)
|
||||
.action(
|
||||
any(Context.class),
|
||||
eq(SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_NEGATIVE_BTN_CLICKED),
|
||||
eq(TEST_EVENT_DATA));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,12 +25,15 @@ import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.robolectric.Shadows.shadowOf;
|
||||
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothLeBroadcast;
|
||||
import android.bluetooth.BluetoothLeBroadcastMetadata;
|
||||
import android.bluetooth.BluetoothStatusCodes;
|
||||
import android.content.Context;
|
||||
import android.os.Looper;
|
||||
@@ -84,47 +87,67 @@ public class AudioSharingPreferenceControllerTest {
|
||||
@Mock private BluetoothEventManager mBtEventManager;
|
||||
@Mock private LocalBluetoothProfileManager mLocalBtProfileManager;
|
||||
@Mock private LocalBluetoothLeBroadcast mBroadcast;
|
||||
@Mock private BluetoothLeBroadcastMetadata mMetadata;
|
||||
private AudioSharingPreferenceController mController;
|
||||
private ShadowBluetoothAdapter mShadowBluetoothAdapter;
|
||||
private LocalBluetoothManager mLocalBluetoothManager;
|
||||
private Lifecycle mLifecycle;
|
||||
private LifecycleOwner mLifecycleOwner;
|
||||
private Preference mPreference;
|
||||
@Spy private Preference mPreference;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
|
||||
mShadowBluetoothAdapter.setEnabled(true);
|
||||
mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
|
||||
ShadowBluetoothAdapter shadowBluetoothAdapter =
|
||||
Shadow.extract(BluetoothAdapter.getDefaultAdapter());
|
||||
shadowBluetoothAdapter.setEnabled(true);
|
||||
shadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
|
||||
BluetoothStatusCodes.FEATURE_SUPPORTED);
|
||||
mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
|
||||
shadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
|
||||
BluetoothStatusCodes.FEATURE_SUPPORTED);
|
||||
mLifecycleOwner = () -> mLifecycle;
|
||||
mLifecycle = new Lifecycle(mLifecycleOwner);
|
||||
ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBtManager;
|
||||
mLocalBluetoothManager = Utils.getLocalBtManager(mContext);
|
||||
when(mLocalBluetoothManager.getEventManager()).thenReturn(mBtEventManager);
|
||||
when(mLocalBluetoothManager.getProfileManager()).thenReturn(mLocalBtProfileManager);
|
||||
LocalBluetoothManager localBluetoothManager = Utils.getLocalBtManager(mContext);
|
||||
when(localBluetoothManager.getEventManager()).thenReturn(mBtEventManager);
|
||||
when(localBluetoothManager.getProfileManager()).thenReturn(mLocalBtProfileManager);
|
||||
when(mLocalBtProfileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast);
|
||||
mController = new AudioSharingPreferenceController(mContext, PREF_KEY);
|
||||
mPreference = new Preference(mContext);
|
||||
mPreference = spy(new Preference(mContext));
|
||||
when(mScreen.findPreference(PREF_KEY)).thenReturn(mPreference);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onStart_registerCallback() {
|
||||
public void onStart_flagOn_registerCallback() {
|
||||
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
|
||||
mController.onStart(mLifecycleOwner);
|
||||
verify(mBtEventManager).registerCallback(mController);
|
||||
verify(mBroadcast).registerServiceCallBack(any(), any(BluetoothLeBroadcast.Callback.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onStop_unregisterCallback() {
|
||||
public void onStart_flagOff_skipRegisterCallback() {
|
||||
mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
|
||||
mController.onStart(mLifecycleOwner);
|
||||
verify(mBtEventManager, never()).registerCallback(mController);
|
||||
verify(mBroadcast, never())
|
||||
.registerServiceCallBack(any(), any(BluetoothLeBroadcast.Callback.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onStop_flagOn_unregisterCallback() {
|
||||
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
|
||||
mController.onStop(mLifecycleOwner);
|
||||
verify(mBtEventManager).unregisterCallback(mController);
|
||||
verify(mBroadcast).unregisterServiceCallBack(any(BluetoothLeBroadcast.Callback.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onStop_flagOff_skipUnregisterCallback() {
|
||||
mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
|
||||
mController.onStop(mLifecycleOwner);
|
||||
verify(mBtEventManager, never()).unregisterCallback(mController);
|
||||
verify(mBroadcast, never())
|
||||
.unregisterServiceCallBack(any(BluetoothLeBroadcast.Callback.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAvailabilityStatus_flagOn() {
|
||||
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
|
||||
@@ -166,4 +189,42 @@ public class AudioSharingPreferenceControllerTest {
|
||||
assertThat(mPreference.getSummary().toString())
|
||||
.isEqualTo(mContext.getString(R.string.audio_sharing_summary_off));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBluetoothLeBroadcastCallbacks_refreshSummary() {
|
||||
mController.displayPreference(mScreen);
|
||||
|
||||
when(mBroadcast.isEnabled(any())).thenReturn(true);
|
||||
mController.mBroadcastCallback.onBroadcastStarted(/* reason= */ 1, /* broadcastId= */ 1);
|
||||
shadowOf(Looper.getMainLooper()).idle();
|
||||
assertThat(mPreference.getSummary().toString())
|
||||
.isEqualTo(mContext.getString(R.string.audio_sharing_summary_on));
|
||||
|
||||
when(mBroadcast.isEnabled(any())).thenReturn(false);
|
||||
mController.mBroadcastCallback.onBroadcastStopped(/* reason= */ 1, /* broadcastId= */ 1);
|
||||
shadowOf(Looper.getMainLooper()).idle();
|
||||
assertThat(mPreference.getSummary().toString())
|
||||
.isEqualTo(mContext.getString(R.string.audio_sharing_summary_off));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBluetoothLeBroadcastCallbacks_doNothing() {
|
||||
mController.displayPreference(mScreen);
|
||||
|
||||
mController.mBroadcastCallback.onBroadcastMetadataChanged(/* reason= */ 1, mMetadata);
|
||||
verify(mPreference, never()).setSummary(any());
|
||||
mController.mBroadcastCallback.onBroadcastUpdated(/* reason= */ 1, /* broadcastId= */ 1);
|
||||
verify(mPreference, never()).setSummary(any());
|
||||
mController.mBroadcastCallback.onPlaybackStarted(/* reason= */ 1, /* broadcastId= */ 1);
|
||||
verify(mPreference, never()).setSummary(any());
|
||||
mController.mBroadcastCallback.onPlaybackStopped(/* reason= */ 1, /* broadcastId= */ 1);
|
||||
verify(mPreference, never()).setSummary(any());
|
||||
mController.mBroadcastCallback.onBroadcastStartFailed(/* reason= */ 1);
|
||||
verify(mPreference, never()).setSummary(any());
|
||||
mController.mBroadcastCallback.onBroadcastStopFailed(/* reason= */ 1);
|
||||
verify(mPreference, never()).setSummary(any());
|
||||
mController.mBroadcastCallback.onBroadcastUpdateFailed(
|
||||
/* reason= */ 1, /* broadcastId= */ 1);
|
||||
verify(mPreference, never()).setSummary(any());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,13 +18,21 @@ package com.android.settings.connecteddevice.audiosharing;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.robolectric.shadows.ShadowLooper.shadowMainLooper;
|
||||
|
||||
import android.app.settings.SettingsEnums;
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.bluetooth.BluetoothStatusCodes;
|
||||
import android.content.Context;
|
||||
import android.platform.test.flag.junit.SetFlagsRule;
|
||||
import android.util.Pair;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
@@ -32,6 +40,7 @@ import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.testutils.FakeFeatureFactory;
|
||||
import com.android.settings.testutils.shadow.ShadowAlertDialogCompat;
|
||||
import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
|
||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
|
||||
@@ -76,14 +85,19 @@ public class AudioSharingStopDialogFragmentTest {
|
||||
private static final AudioSharingDeviceItem TEST_DEVICE_ITEM3 =
|
||||
new AudioSharingDeviceItem(
|
||||
TEST_DEVICE_NAME3, TEST_DEVICE_GROUP_ID3, /* isActive= */ false);
|
||||
private static final AudioSharingStopDialogFragment.DialogEventListener EMPTY_EVENT_LISTENER =
|
||||
() -> {};
|
||||
private static final Pair<Integer, Object> TEST_EVENT_DATA = Pair.create(1, 1);
|
||||
private static final Pair<Integer, Object>[] TEST_EVENT_DATA_LIST =
|
||||
new Pair[] {TEST_EVENT_DATA};
|
||||
|
||||
@Mock private CachedBluetoothDevice mCachedDevice1;
|
||||
@Mock private CachedBluetoothDevice mCachedDevice2;
|
||||
@Mock private BluetoothDevice mDevice1;
|
||||
@Mock private BluetoothDevice mDevice2;
|
||||
private FakeFeatureFactory mFeatureFactory;
|
||||
private Fragment mParent;
|
||||
private AudioSharingStopDialogFragment mFragment;
|
||||
private ShadowBluetoothAdapter mShadowBluetoothAdapter;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
@@ -92,12 +106,14 @@ public class AudioSharingStopDialogFragmentTest {
|
||||
latestAlertDialog.dismiss();
|
||||
ShadowAlertDialogCompat.reset();
|
||||
}
|
||||
mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
|
||||
mShadowBluetoothAdapter.setEnabled(true);
|
||||
mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
|
||||
ShadowBluetoothAdapter shadowBluetoothAdapter =
|
||||
Shadow.extract(BluetoothAdapter.getDefaultAdapter());
|
||||
shadowBluetoothAdapter.setEnabled(true);
|
||||
shadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
|
||||
BluetoothStatusCodes.FEATURE_SUPPORTED);
|
||||
mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
|
||||
shadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
|
||||
BluetoothStatusCodes.FEATURE_SUPPORTED);
|
||||
mFeatureFactory = FakeFeatureFactory.setupForTest();
|
||||
when(mCachedDevice1.getName()).thenReturn(TEST_DEVICE_NAME1);
|
||||
when(mCachedDevice1.getGroupId()).thenReturn(TEST_DEVICE_GROUP_ID1);
|
||||
when(mCachedDevice1.getDevice()).thenReturn(mDevice1);
|
||||
@@ -110,10 +126,21 @@ public class AudioSharingStopDialogFragmentTest {
|
||||
mParent, FragmentActivity.class, /* containerViewId= */ 0, /* bundle= */ null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getMetricsCategory_correctValue() {
|
||||
assertThat(mFragment.getMetricsCategory())
|
||||
.isEqualTo(SettingsEnums.DIALOG_STOP_AUDIO_SHARING);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onCreateDialog_flagOff_dialogNotExist() {
|
||||
mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
|
||||
mFragment.show(mParent, ImmutableList.of(), mCachedDevice1, () -> {});
|
||||
AudioSharingStopDialogFragment.show(
|
||||
mParent,
|
||||
ImmutableList.of(),
|
||||
mCachedDevice1,
|
||||
EMPTY_EVENT_LISTENER,
|
||||
TEST_EVENT_DATA_LIST);
|
||||
shadowMainLooper().idle();
|
||||
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
|
||||
assertThat(dialog).isNull();
|
||||
@@ -122,12 +149,18 @@ public class AudioSharingStopDialogFragmentTest {
|
||||
@Test
|
||||
public void onCreateDialog_oneDeviceInSharing_showDialogWithCorrectMessage() {
|
||||
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
|
||||
mFragment.show(mParent, ImmutableList.of(TEST_DEVICE_ITEM2), mCachedDevice1, () -> {});
|
||||
AudioSharingStopDialogFragment.show(
|
||||
mParent,
|
||||
ImmutableList.of(TEST_DEVICE_ITEM2),
|
||||
mCachedDevice1,
|
||||
EMPTY_EVENT_LISTENER,
|
||||
TEST_EVENT_DATA_LIST);
|
||||
shadowMainLooper().idle();
|
||||
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
|
||||
assertThat(dialog).isNotNull();
|
||||
assertThat(dialog.isShowing()).isTrue();
|
||||
TextView view = dialog.findViewById(R.id.description_text);
|
||||
assertThat(view).isNotNull();
|
||||
assertThat(view.getText().toString())
|
||||
.isEqualTo(
|
||||
mParent.getString(
|
||||
@@ -137,16 +170,18 @@ public class AudioSharingStopDialogFragmentTest {
|
||||
@Test
|
||||
public void onCreateDialog_twoDeviceInSharing_showDialogWithCorrectMessage() {
|
||||
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
|
||||
mFragment.show(
|
||||
AudioSharingStopDialogFragment.show(
|
||||
mParent,
|
||||
ImmutableList.of(TEST_DEVICE_ITEM2, TEST_DEVICE_ITEM3),
|
||||
mCachedDevice1,
|
||||
() -> {});
|
||||
EMPTY_EVENT_LISTENER,
|
||||
TEST_EVENT_DATA_LIST);
|
||||
shadowMainLooper().idle();
|
||||
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
|
||||
assertThat(dialog).isNotNull();
|
||||
assertThat(dialog.isShowing()).isTrue();
|
||||
TextView view = dialog.findViewById(R.id.description_text);
|
||||
assertThat(view).isNotNull();
|
||||
assertThat(view.getText().toString())
|
||||
.isEqualTo(
|
||||
mParent.getString(
|
||||
@@ -158,57 +193,99 @@ public class AudioSharingStopDialogFragmentTest {
|
||||
@Test
|
||||
public void onCreateDialog_dialogIsShowingForSameDevice_updateDialog() {
|
||||
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
|
||||
mFragment.show(mParent, ImmutableList.of(), mCachedDevice1, () -> {});
|
||||
AudioSharingStopDialogFragment.show(
|
||||
mParent,
|
||||
ImmutableList.of(),
|
||||
mCachedDevice1,
|
||||
EMPTY_EVENT_LISTENER,
|
||||
TEST_EVENT_DATA_LIST);
|
||||
shadowMainLooper().idle();
|
||||
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
|
||||
assertThat(dialog).isNotNull();
|
||||
assertThat(dialog.isShowing()).isTrue();
|
||||
TextView view = dialog.findViewById(R.id.description_text);
|
||||
assertThat(view).isNotNull();
|
||||
assertThat(view.getText().toString())
|
||||
.isEqualTo(mParent.getString(R.string.audio_sharing_stop_dialog_with_more_content));
|
||||
|
||||
// Update the content
|
||||
AtomicBoolean isStopBtnClicked = new AtomicBoolean(false);
|
||||
mFragment.show(
|
||||
mParent, ImmutableList.of(), mCachedDevice1, () -> isStopBtnClicked.set(true));
|
||||
AudioSharingStopDialogFragment.show(
|
||||
mParent,
|
||||
ImmutableList.of(),
|
||||
mCachedDevice1,
|
||||
() -> isStopBtnClicked.set(true),
|
||||
TEST_EVENT_DATA_LIST);
|
||||
shadowMainLooper().idle();
|
||||
dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
|
||||
assertThat(dialog).isNotNull();
|
||||
assertThat(dialog.isShowing()).isTrue();
|
||||
verify(mFeatureFactory.metricsFeatureProvider, times(0))
|
||||
.action(
|
||||
any(Context.class),
|
||||
eq(SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_AUTO_DISMISS),
|
||||
eq(SettingsEnums.DIALOG_STOP_AUDIO_SHARING));
|
||||
|
||||
dialog.findViewById(android.R.id.button1).performClick();
|
||||
View btnView = dialog.findViewById(android.R.id.button1);
|
||||
assertThat(btnView).isNotNull();
|
||||
btnView.performClick();
|
||||
shadowMainLooper().idle();
|
||||
assertThat(dialog.isShowing()).isFalse();
|
||||
assertThat(isStopBtnClicked.get()).isTrue();
|
||||
verify(mFeatureFactory.metricsFeatureProvider)
|
||||
.action(
|
||||
any(Context.class),
|
||||
eq(SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_POSITIVE_BTN_CLICKED),
|
||||
eq(TEST_EVENT_DATA));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onCreateDialog_dialogIsShowingForNewDevice_showNewDialog() {
|
||||
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
|
||||
mFragment.show(mParent, ImmutableList.of(), mCachedDevice1, () -> {});
|
||||
AudioSharingStopDialogFragment.show(
|
||||
mParent,
|
||||
ImmutableList.of(),
|
||||
mCachedDevice1,
|
||||
EMPTY_EVENT_LISTENER,
|
||||
TEST_EVENT_DATA_LIST);
|
||||
shadowMainLooper().idle();
|
||||
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
|
||||
assertThat(dialog).isNotNull();
|
||||
assertThat(dialog.isShowing()).isTrue();
|
||||
TextView view = dialog.findViewById(R.id.description_text);
|
||||
assertThat(view).isNotNull();
|
||||
assertThat(view.getText().toString())
|
||||
.isEqualTo(mParent.getString(R.string.audio_sharing_stop_dialog_with_more_content));
|
||||
TextView title = dialog.findViewById(R.id.title_text);
|
||||
assertThat(title).isNotNull();
|
||||
assertThat(title.getText().toString())
|
||||
.isEqualTo(
|
||||
mParent.getString(
|
||||
R.string.audio_sharing_stop_dialog_title, TEST_DEVICE_NAME1));
|
||||
|
||||
// Show new dialog
|
||||
mFragment.show(mParent, ImmutableList.of(), mCachedDevice2, () -> {});
|
||||
AudioSharingStopDialogFragment.show(
|
||||
mParent,
|
||||
ImmutableList.of(),
|
||||
mCachedDevice2,
|
||||
EMPTY_EVENT_LISTENER,
|
||||
TEST_EVENT_DATA_LIST);
|
||||
shadowMainLooper().idle();
|
||||
dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
|
||||
assertThat(dialog).isNotNull();
|
||||
assertThat(dialog.isShowing()).isTrue();
|
||||
verify(mFeatureFactory.metricsFeatureProvider)
|
||||
.action(
|
||||
any(Context.class),
|
||||
eq(SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_AUTO_DISMISS),
|
||||
eq(SettingsEnums.DIALOG_STOP_AUDIO_SHARING));
|
||||
|
||||
view = dialog.findViewById(R.id.description_text);
|
||||
assertThat(view).isNotNull();
|
||||
assertThat(view.getText().toString())
|
||||
.isEqualTo(mParent.getString(R.string.audio_sharing_stop_dialog_with_more_content));
|
||||
title = dialog.findViewById(R.id.title_text);
|
||||
assertThat(title).isNotNull();
|
||||
assertThat(title.getText().toString())
|
||||
.isEqualTo(
|
||||
mParent.getString(
|
||||
@@ -218,25 +295,60 @@ public class AudioSharingStopDialogFragmentTest {
|
||||
@Test
|
||||
public void onCreateDialog_clickCancel_dialogDismiss() {
|
||||
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
|
||||
mFragment.show(mParent, ImmutableList.of(), mCachedDevice1, () -> {});
|
||||
AudioSharingStopDialogFragment.show(
|
||||
mParent,
|
||||
ImmutableList.of(),
|
||||
mCachedDevice1,
|
||||
EMPTY_EVENT_LISTENER,
|
||||
TEST_EVENT_DATA_LIST);
|
||||
shadowMainLooper().idle();
|
||||
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
|
||||
dialog.findViewById(android.R.id.button2).performClick();
|
||||
assertThat(dialog).isNotNull();
|
||||
View btnView = dialog.findViewById(android.R.id.button2);
|
||||
assertThat(btnView).isNotNull();
|
||||
btnView.performClick();
|
||||
shadowMainLooper().idle();
|
||||
assertThat(dialog.isShowing()).isFalse();
|
||||
verify(mFeatureFactory.metricsFeatureProvider, times(0))
|
||||
.action(
|
||||
any(Context.class),
|
||||
eq(SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_AUTO_DISMISS),
|
||||
eq(SettingsEnums.DIALOG_STOP_AUDIO_SHARING));
|
||||
verify(mFeatureFactory.metricsFeatureProvider)
|
||||
.action(
|
||||
any(Context.class),
|
||||
eq(SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_NEGATIVE_BTN_CLICKED),
|
||||
eq(TEST_EVENT_DATA));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onCreateDialog_clickShare_callbackTriggered() {
|
||||
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
|
||||
AtomicBoolean isStopBtnClicked = new AtomicBoolean(false);
|
||||
mFragment.show(
|
||||
mParent, ImmutableList.of(), mCachedDevice1, () -> isStopBtnClicked.set(true));
|
||||
AudioSharingStopDialogFragment.show(
|
||||
mParent,
|
||||
ImmutableList.of(),
|
||||
mCachedDevice1,
|
||||
() -> isStopBtnClicked.set(true),
|
||||
TEST_EVENT_DATA_LIST);
|
||||
shadowMainLooper().idle();
|
||||
AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
|
||||
dialog.findViewById(android.R.id.button1).performClick();
|
||||
assertThat(dialog).isNotNull();
|
||||
View btnView = dialog.findViewById(android.R.id.button1);
|
||||
assertThat(btnView).isNotNull();
|
||||
btnView.performClick();
|
||||
shadowMainLooper().idle();
|
||||
assertThat(dialog.isShowing()).isFalse();
|
||||
assertThat(isStopBtnClicked.get()).isTrue();
|
||||
verify(mFeatureFactory.metricsFeatureProvider, times(0))
|
||||
.action(
|
||||
any(Context.class),
|
||||
eq(SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_AUTO_DISMISS),
|
||||
eq(SettingsEnums.DIALOG_STOP_AUDIO_SHARING));
|
||||
verify(mFeatureFactory.metricsFeatureProvider)
|
||||
.action(
|
||||
any(Context.class),
|
||||
eq(SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_POSITIVE_BTN_CLICKED),
|
||||
eq(TEST_EVENT_DATA));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ 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.doNothing;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
@@ -30,6 +31,7 @@ import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.robolectric.Shadows.shadowOf;
|
||||
|
||||
import android.app.settings.SettingsEnums;
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.bluetooth.BluetoothLeBroadcast;
|
||||
@@ -43,12 +45,17 @@ import android.content.IntentFilter;
|
||||
import android.os.Looper;
|
||||
import android.platform.test.flag.junit.SetFlagsRule;
|
||||
import android.util.FeatureFlagUtils;
|
||||
import android.util.Pair;
|
||||
import android.widget.CompoundButton;
|
||||
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
|
||||
import com.android.settings.bluetooth.Utils;
|
||||
import com.android.settings.testutils.FakeFeatureFactory;
|
||||
import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
|
||||
import com.android.settings.testutils.shadow.ShadowBluetoothUtils;
|
||||
import com.android.settings.testutils.shadow.ShadowThreadUtils;
|
||||
@@ -65,6 +72,8 @@ import com.android.settingslib.core.lifecycle.Lifecycle;
|
||||
import com.android.settingslib.flags.Flags;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.truth.Correspondence;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
@@ -77,7 +86,9 @@ import org.mockito.junit.MockitoRule;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
import org.robolectric.shadow.api.Shadow;
|
||||
import org.robolectric.shadows.androidx.fragment.FragmentController;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@@ -88,6 +99,18 @@ import java.util.concurrent.Executor;
|
||||
ShadowThreadUtils.class,
|
||||
})
|
||||
public class AudioSharingSwitchBarControllerTest {
|
||||
private static final String TEST_DEVICE_NAME1 = "test1";
|
||||
private static final String TEST_DEVICE_NAME2 = "test2";
|
||||
private static final int TEST_DEVICE_GROUP_ID1 = 1;
|
||||
private static final int TEST_DEVICE_GROUP_ID2 = 2;
|
||||
private static final Correspondence<Fragment, String> TAG_EQUALS =
|
||||
Correspondence.from(
|
||||
(Fragment fragment, String tag) ->
|
||||
fragment instanceof DialogFragment
|
||||
&& ((DialogFragment) fragment).getTag() != null
|
||||
&& ((DialogFragment) fragment).getTag().equals(tag),
|
||||
"is equal to");
|
||||
|
||||
@Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
|
||||
@Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
|
||||
|
||||
@@ -99,17 +122,19 @@ public class AudioSharingSwitchBarControllerTest {
|
||||
@Mock private LocalBluetoothLeBroadcastAssistant mAssistant;
|
||||
@Mock private VolumeControlProfile mVolumeControl;
|
||||
@Mock private CompoundButton mBtnView;
|
||||
@Mock private CachedBluetoothDevice mCachedDevice;
|
||||
@Mock private BluetoothDevice mDevice;
|
||||
@Mock private CachedBluetoothDevice mCachedDevice1;
|
||||
@Mock private CachedBluetoothDevice mCachedDevice2;
|
||||
@Mock private BluetoothDevice mDevice1;
|
||||
@Mock private BluetoothDevice mDevice2;
|
||||
private SettingsMainSwitchBar mSwitchBar;
|
||||
private AudioSharingSwitchBarController mController;
|
||||
private AudioSharingSwitchBarController.OnAudioSharingStateChangedListener mListener;
|
||||
private FakeFeatureFactory mFeatureFactory;
|
||||
private Lifecycle mLifecycle;
|
||||
private LifecycleOwner mLifecycleOwner;
|
||||
private boolean mOnAudioSharingStateChanged;
|
||||
private boolean mOnAudioSharingServiceConnected;
|
||||
private ShadowBluetoothAdapter mShadowBluetoothAdapter;
|
||||
private LocalBluetoothManager mLocalBluetoothManager;
|
||||
private Fragment mParentFragment;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
@@ -122,13 +147,20 @@ public class AudioSharingSwitchBarControllerTest {
|
||||
mLifecycleOwner = () -> mLifecycle;
|
||||
mLifecycle = new Lifecycle(mLifecycleOwner);
|
||||
ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBtManager;
|
||||
mLocalBluetoothManager = Utils.getLocalBtManager(mContext);
|
||||
when(mLocalBluetoothManager.getProfileManager()).thenReturn(mBtProfileManager);
|
||||
when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(mDeviceManager);
|
||||
when(mDeviceManager.findDevice(mDevice)).thenReturn(mCachedDevice);
|
||||
when(mCachedDevice.getDevice()).thenReturn(mDevice);
|
||||
when(mCachedDevice.getGroupId()).thenReturn(1);
|
||||
when(mCachedDevice.getName()).thenReturn("test");
|
||||
LocalBluetoothManager localBluetoothManager = Utils.getLocalBtManager(mContext);
|
||||
mFeatureFactory = FakeFeatureFactory.setupForTest();
|
||||
when(localBluetoothManager.getProfileManager()).thenReturn(mBtProfileManager);
|
||||
when(localBluetoothManager.getCachedDeviceManager()).thenReturn(mDeviceManager);
|
||||
when(mDeviceManager.findDevice(mDevice1)).thenReturn(mCachedDevice1);
|
||||
when(mCachedDevice1.getDevice()).thenReturn(mDevice1);
|
||||
when(mCachedDevice1.getGroupId()).thenReturn(TEST_DEVICE_GROUP_ID1);
|
||||
when(mCachedDevice1.getName()).thenReturn(TEST_DEVICE_NAME1);
|
||||
when(mCachedDevice1.isActiveDevice(BluetoothProfile.LE_AUDIO)).thenReturn(false);
|
||||
when(mDeviceManager.findDevice(mDevice2)).thenReturn(mCachedDevice2);
|
||||
when(mCachedDevice2.getDevice()).thenReturn(mDevice2);
|
||||
when(mCachedDevice2.getGroupId()).thenReturn(TEST_DEVICE_GROUP_ID2);
|
||||
when(mCachedDevice2.getName()).thenReturn(TEST_DEVICE_NAME2);
|
||||
when(mCachedDevice2.isActiveDevice(BluetoothProfile.LE_AUDIO)).thenReturn(true);
|
||||
when(mBtProfileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast);
|
||||
when(mBtProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(mAssistant);
|
||||
when(mBtProfileManager.getVolumeControlProfile()).thenReturn(mVolumeControl);
|
||||
@@ -153,7 +185,7 @@ public class AudioSharingSwitchBarControllerTest {
|
||||
mSwitchBar.setDisabledByAdmin(mock(RestrictedLockUtils.EnforcedAdmin.class));
|
||||
mOnAudioSharingStateChanged = false;
|
||||
mOnAudioSharingServiceConnected = false;
|
||||
mListener =
|
||||
AudioSharingSwitchBarController.OnAudioSharingStateChangedListener listener =
|
||||
new AudioSharingSwitchBarController.OnAudioSharingStateChangedListener() {
|
||||
@Override
|
||||
public void onAudioSharingStateChanged() {
|
||||
@@ -165,7 +197,14 @@ public class AudioSharingSwitchBarControllerTest {
|
||||
mOnAudioSharingServiceConnected = true;
|
||||
}
|
||||
};
|
||||
mController = new AudioSharingSwitchBarController(mContext, mSwitchBar, mListener);
|
||||
mController = new AudioSharingSwitchBarController(mContext, mSwitchBar, listener);
|
||||
mParentFragment = new Fragment();
|
||||
FragmentController.setupFragment(
|
||||
mParentFragment,
|
||||
FragmentActivity.class,
|
||||
0 /* containerViewId */,
|
||||
null /* bundle */);
|
||||
mController.init(mParentFragment);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -356,7 +395,7 @@ public class AudioSharingSwitchBarControllerTest {
|
||||
when(mBtnView.isEnabled()).thenReturn(true);
|
||||
when(mAssistant.getDevicesMatchingConnectionStates(
|
||||
new int[] {BluetoothProfile.STATE_CONNECTED}))
|
||||
.thenReturn(ImmutableList.of(mDevice));
|
||||
.thenReturn(ImmutableList.of(mDevice1));
|
||||
doNothing().when(mBroadcast).startPrivateBroadcast();
|
||||
mController.onCheckedChanged(mBtnView, /* isChecked= */ true);
|
||||
verify(mBroadcast).startPrivateBroadcast();
|
||||
@@ -380,4 +419,50 @@ public class AudioSharingSwitchBarControllerTest {
|
||||
mController.onCheckedChanged(mBtnView, /* isChecked= */ false);
|
||||
verify(mBroadcast).stopBroadcast(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onPlaybackStarted_showJoinAudioSharingDialog() {
|
||||
FeatureFlagUtils.setEnabled(
|
||||
mContext, FeatureFlagUtils.SETTINGS_NEED_CONNECTED_BLE_DEVICE_FOR_BROADCAST, true);
|
||||
when(mBtnView.isEnabled()).thenReturn(true);
|
||||
when(mAssistant.getDevicesMatchingConnectionStates(
|
||||
new int[] {BluetoothProfile.STATE_CONNECTED}))
|
||||
.thenReturn(ImmutableList.of(mDevice2, mDevice1));
|
||||
doNothing().when(mBroadcast).startPrivateBroadcast();
|
||||
mController.onCheckedChanged(mBtnView, /* isChecked= */ true);
|
||||
verify(mBroadcast).startPrivateBroadcast();
|
||||
mController.mBroadcastCallback.onPlaybackStarted(0, 0);
|
||||
shadowOf(Looper.getMainLooper()).idle();
|
||||
|
||||
verify(mFeatureFactory.metricsFeatureProvider)
|
||||
.action(any(Context.class), eq(SettingsEnums.ACTION_AUTO_JOIN_AUDIO_SHARING));
|
||||
|
||||
List<Fragment> childFragments = mParentFragment.getChildFragmentManager().getFragments();
|
||||
assertThat(childFragments)
|
||||
.comparingElementsUsing(TAG_EQUALS)
|
||||
.containsExactly(AudioSharingDialogFragment.tag());
|
||||
|
||||
AudioSharingDialogFragment fragment =
|
||||
(AudioSharingDialogFragment) Iterables.getOnlyElement(childFragments);
|
||||
Pair<Integer, Object>[] eventData = fragment.getEventData();
|
||||
assertThat(eventData)
|
||||
.asList()
|
||||
.containsExactly(
|
||||
Pair.create(
|
||||
AudioSharingUtils.MetricKey.METRIC_KEY_SOURCE_PAGE_ID.ordinal(),
|
||||
SettingsEnums.AUDIO_SHARING_SETTINGS),
|
||||
Pair.create(
|
||||
AudioSharingUtils.MetricKey.METRIC_KEY_PAGE_ID.ordinal(),
|
||||
SettingsEnums.DIALOG_AUDIO_SHARING_ADD_DEVICE),
|
||||
Pair.create(
|
||||
AudioSharingUtils.MetricKey.METRIC_KEY_USER_TRIGGERED.ordinal(), 0),
|
||||
Pair.create(
|
||||
AudioSharingUtils.MetricKey.METRIC_KEY_DEVICE_COUNT_IN_SHARING
|
||||
.ordinal(),
|
||||
1),
|
||||
Pair.create(
|
||||
AudioSharingUtils.MetricKey.METRIC_KEY_CANDIDATE_DEVICE_COUNT
|
||||
.ordinal(),
|
||||
1));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.settings.notification.modes;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import android.app.AutomaticZenRule;
|
||||
import android.content.Context;
|
||||
import android.service.notification.ZenModeConfig;
|
||||
|
||||
import androidx.preference.TwoStatePreference;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
|
||||
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 org.robolectric.RobolectricTestRunner;
|
||||
|
||||
import java.util.Calendar;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class ZenModeExitAtAlarmPreferenceControllerTest {
|
||||
private Context mContext;
|
||||
@Mock
|
||||
private ZenModesBackend mBackend;
|
||||
|
||||
private ZenModeExitAtAlarmPreferenceController mPrefController;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
mContext = ApplicationProvider.getApplicationContext();
|
||||
mPrefController = new ZenModeExitAtAlarmPreferenceController(mContext, "exit_at_alarm",
|
||||
mBackend);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateState() {
|
||||
TwoStatePreference preference = mock(TwoStatePreference.class);
|
||||
|
||||
// previously: don't exit at alarm
|
||||
ZenModeConfig.ScheduleInfo scheduleInfo = new ZenModeConfig.ScheduleInfo();
|
||||
scheduleInfo.days = new int[] { Calendar.MONDAY };
|
||||
scheduleInfo.startHour = 1;
|
||||
scheduleInfo.endHour = 2;
|
||||
scheduleInfo.exitAtAlarm = false;
|
||||
|
||||
ZenMode mode = new ZenMode("id",
|
||||
new AutomaticZenRule.Builder("name",
|
||||
ZenModeConfig.toScheduleConditionId(scheduleInfo)).build(),
|
||||
true); // is active
|
||||
|
||||
// need to call updateZenMode for the first call
|
||||
mPrefController.updateZenMode(preference, mode);
|
||||
verify(preference).setChecked(false);
|
||||
|
||||
// Now update state after changing exitAtAlarm
|
||||
scheduleInfo.exitAtAlarm = true;
|
||||
mode.getRule().setConditionId(ZenModeConfig.toScheduleConditionId(scheduleInfo));
|
||||
|
||||
// now can just call updateState
|
||||
mPrefController.updateState(preference, mode);
|
||||
verify(preference).setChecked(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnPreferenceChange() {
|
||||
TwoStatePreference preference = mock(TwoStatePreference.class);
|
||||
|
||||
// previously: exit at alarm
|
||||
ZenModeConfig.ScheduleInfo scheduleInfo = new ZenModeConfig.ScheduleInfo();
|
||||
scheduleInfo.days = new int[] { Calendar.MONDAY };
|
||||
scheduleInfo.startHour = 1;
|
||||
scheduleInfo.endHour = 2;
|
||||
scheduleInfo.exitAtAlarm = true;
|
||||
|
||||
ZenMode mode = new ZenMode("id",
|
||||
new AutomaticZenRule.Builder("name",
|
||||
ZenModeConfig.toScheduleConditionId(scheduleInfo)).build(),
|
||||
true); // is active
|
||||
mPrefController.updateZenMode(preference, mode);
|
||||
|
||||
// turn off exit at alarm
|
||||
mPrefController.onPreferenceChange(preference, false);
|
||||
ArgumentCaptor<ZenMode> captor = ArgumentCaptor.forClass(ZenMode.class);
|
||||
verify(mBackend).updateMode(captor.capture());
|
||||
ZenModeConfig.ScheduleInfo newSchedule = ZenModeConfig.tryParseScheduleConditionId(
|
||||
captor.getValue().getRule().getConditionId());
|
||||
assertThat(newSchedule.exitAtAlarm).isFalse();
|
||||
|
||||
// other properties remain the same
|
||||
assertThat(newSchedule.startHour).isEqualTo(1);
|
||||
assertThat(newSchedule.endHour).isEqualTo(2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.settings.notification.modes;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.app.AutomaticZenRule;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.preference.PreferenceScreen;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.dashboard.DashboardFragment;
|
||||
import com.android.settingslib.widget.LayoutPreference;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.RuntimeEnvironment;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class ZenModeIconPickerListPreferenceControllerTest {
|
||||
|
||||
private static final ZenMode ZEN_MODE = new ZenMode(
|
||||
"mode_id",
|
||||
new AutomaticZenRule.Builder("mode name", Uri.parse("mode")).build(),
|
||||
/* isActive= */ false);
|
||||
|
||||
private ZenModesBackend mBackend;
|
||||
private ZenModeIconPickerListPreferenceController mController;
|
||||
private PreferenceScreen mPreferenceScreen;
|
||||
private RecyclerView mRecyclerView;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
Context context = RuntimeEnvironment.getApplication();
|
||||
mBackend = mock(ZenModesBackend.class);
|
||||
|
||||
DashboardFragment fragment = mock(DashboardFragment.class);
|
||||
mController = new ZenModeIconPickerListPreferenceController(
|
||||
RuntimeEnvironment.getApplication(), "icon_list", fragment, mBackend);
|
||||
|
||||
mRecyclerView = new RecyclerView(context);
|
||||
mRecyclerView.setId(R.id.icon_list);
|
||||
LayoutPreference layoutPreference = new LayoutPreference(context, mRecyclerView);
|
||||
mPreferenceScreen = mock(PreferenceScreen.class);
|
||||
when(mPreferenceScreen.findPreference(eq("icon_list"))).thenReturn(layoutPreference);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void displayPreference_loadsIcons() {
|
||||
mController.displayPreference(mPreferenceScreen);
|
||||
|
||||
assertThat(mRecyclerView.getAdapter()).isNotNull();
|
||||
assertThat(mRecyclerView.getAdapter().getItemCount()).isEqualTo(20);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void selectIcon_updatesMode() {
|
||||
mController.setZenMode(ZEN_MODE);
|
||||
|
||||
mController.onIconSelected(R.drawable.ic_android);
|
||||
|
||||
ArgumentCaptor<ZenMode> captor = ArgumentCaptor.forClass(ZenMode.class);
|
||||
verify(mBackend).updateMode(captor.capture());
|
||||
assertThat(captor.getValue().getRule().getIconResId()).isEqualTo(R.drawable.ic_android);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.settings.notification.modes;
|
||||
|
||||
import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.app.AutomaticZenRule;
|
||||
import android.app.Flags;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.platform.test.annotations.EnableFlags;
|
||||
import android.platform.test.flag.junit.SetFlagsRule;
|
||||
import android.service.notification.ZenModeConfig;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ToggleButton;
|
||||
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
|
||||
import com.android.settings.R;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
|
||||
import java.util.Calendar;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class ZenModeSetSchedulePreferenceControllerTest {
|
||||
@Rule
|
||||
public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT);
|
||||
|
||||
@Mock
|
||||
private ZenModesBackend mBackend;
|
||||
private Context mContext;
|
||||
|
||||
@Mock
|
||||
private Fragment mParent;
|
||||
@Mock
|
||||
private Calendar mCalendar;
|
||||
@Mock
|
||||
private ViewGroup mDaysContainer;
|
||||
@Mock
|
||||
private ToggleButton mDay0, mDay1, mDay2, mDay3, mDay4, mDay5, mDay6;
|
||||
|
||||
private ZenModeSetSchedulePreferenceController mPrefController;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
mContext = ApplicationProvider.getApplicationContext();
|
||||
mPrefController = new ZenModeSetSchedulePreferenceController(mContext, mParent, "schedule",
|
||||
mBackend);
|
||||
setupMockDayContainer();
|
||||
}
|
||||
|
||||
@Test
|
||||
@EnableFlags({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI})
|
||||
public void updateScheduleRule_updatesConditionAndTriggerDescription() {
|
||||
ZenMode mode = new ZenMode("id",
|
||||
new AutomaticZenRule.Builder("name", Uri.parse("condition")).build(),
|
||||
true); // is active
|
||||
|
||||
ZenModeConfig.ScheduleInfo scheduleInfo = new ZenModeConfig.ScheduleInfo();
|
||||
scheduleInfo.days = new int[] { Calendar.MONDAY };
|
||||
scheduleInfo.startHour = 1;
|
||||
scheduleInfo.endHour = 2;
|
||||
ZenMode out = mPrefController.updateScheduleMode(scheduleInfo).apply(mode);
|
||||
|
||||
assertThat(out.getRule().getConditionId())
|
||||
.isEqualTo(ZenModeConfig.toScheduleConditionId(scheduleInfo));
|
||||
assertThat(out.getRule().getTriggerDescription()).isNotEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateScheduleDays() {
|
||||
// Confirm that adding/subtracting/etc days works as expected
|
||||
// starting from null: no days set
|
||||
ZenModeConfig.ScheduleInfo schedule = new ZenModeConfig.ScheduleInfo();
|
||||
|
||||
// Unset a day that's already unset: nothing should change
|
||||
assertThat(ZenModeSetSchedulePreferenceController.updateScheduleDays(schedule,
|
||||
Calendar.TUESDAY, false)).isFalse();
|
||||
// not explicitly checking whether schedule.days is still null here, as we don't necessarily
|
||||
// want to require nullness as distinct from an empty list of days.
|
||||
|
||||
// set a few new days
|
||||
assertThat(ZenModeSetSchedulePreferenceController.updateScheduleDays(schedule,
|
||||
Calendar.MONDAY, true)).isTrue();
|
||||
assertThat(ZenModeSetSchedulePreferenceController.updateScheduleDays(schedule,
|
||||
Calendar.FRIDAY, true)).isTrue();
|
||||
assertThat(schedule.days).hasLength(2);
|
||||
assertThat(schedule.days).asList().containsExactly(Calendar.MONDAY, Calendar.FRIDAY);
|
||||
|
||||
// remove an existing day to make sure that works
|
||||
assertThat(ZenModeSetSchedulePreferenceController.updateScheduleDays(schedule,
|
||||
Calendar.MONDAY, false)).isTrue();
|
||||
assertThat(schedule.days).hasLength(1);
|
||||
assertThat(schedule.days).asList().containsExactly(Calendar.FRIDAY);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetupDayToggles_daysOfWeekOrder() {
|
||||
// Confirm that days are correctly associated with the actual day of the week independent
|
||||
// of when the first day of the week is for the given calendar.
|
||||
ZenModeConfig.ScheduleInfo schedule = new ZenModeConfig.ScheduleInfo();
|
||||
schedule.days = new int[] { Calendar.SUNDAY, Calendar.TUESDAY, Calendar.FRIDAY };
|
||||
schedule.startHour = 1;
|
||||
schedule.endHour = 5;
|
||||
|
||||
// Start mCalendar on Wednesday, arbitrarily
|
||||
when(mCalendar.getFirstDayOfWeek()).thenReturn(Calendar.WEDNESDAY);
|
||||
|
||||
// Setup the day toggles
|
||||
mPrefController.setupDayToggles(mDaysContainer, schedule, mCalendar);
|
||||
|
||||
// we should see toggle 0 associated with the first day of the week, etc.
|
||||
// in this week order, schedule turns on friday (2), sunday (4), tuesday (6) so those
|
||||
// should be checked while everything else should not be checked.
|
||||
verify(mDay0).setChecked(false); // weds
|
||||
verify(mDay1).setChecked(false); // thurs
|
||||
verify(mDay2).setChecked(true); // fri
|
||||
verify(mDay3).setChecked(false); // sat
|
||||
verify(mDay4).setChecked(true); // sun
|
||||
verify(mDay5).setChecked(false); // mon
|
||||
verify(mDay6).setChecked(true); // tues
|
||||
}
|
||||
|
||||
private void setupMockDayContainer() {
|
||||
// associate each index (regardless of associated day of the week) with the appropriate
|
||||
// res id in the days container
|
||||
when(mDaysContainer.findViewById(R.id.day0)).thenReturn(mDay0);
|
||||
when(mDaysContainer.findViewById(R.id.day1)).thenReturn(mDay1);
|
||||
when(mDaysContainer.findViewById(R.id.day2)).thenReturn(mDay2);
|
||||
when(mDaysContainer.findViewById(R.id.day3)).thenReturn(mDay3);
|
||||
when(mDaysContainer.findViewById(R.id.day4)).thenReturn(mDay4);
|
||||
when(mDaysContainer.findViewById(R.id.day5)).thenReturn(mDay5);
|
||||
when(mDaysContainer.findViewById(R.id.day6)).thenReturn(mDay6);
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
package com.android.settings.notification.modes;
|
||||
|
||||
import static android.app.AutomaticZenRule.TYPE_SCHEDULE_CALENDAR;
|
||||
import static android.app.AutomaticZenRule.TYPE_SCHEDULE_TIME;
|
||||
import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
|
||||
import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
|
||||
|
||||
@@ -53,6 +54,8 @@ import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
|
||||
import java.util.Calendar;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class ZenModeSetTriggerLinkPreferenceControllerTest {
|
||||
@Rule
|
||||
@@ -167,4 +170,29 @@ public class ZenModeSetTriggerLinkPreferenceControllerTest {
|
||||
captor.getValue().getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT)).isEqualTo(
|
||||
ZenModeSetCalendarFragment.class.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRuleLink_schedule() {
|
||||
ZenModeConfig.ScheduleInfo scheduleInfo = new ZenModeConfig.ScheduleInfo();
|
||||
scheduleInfo.days = new int[] { Calendar.MONDAY, Calendar.TUESDAY, Calendar.THURSDAY };
|
||||
scheduleInfo.startHour = 1;
|
||||
scheduleInfo.endHour = 15;
|
||||
ZenMode mode = new ZenMode("id", new AutomaticZenRule.Builder("name",
|
||||
ZenModeConfig.toScheduleConditionId(scheduleInfo))
|
||||
.setType(TYPE_SCHEDULE_TIME)
|
||||
.setTriggerDescription("some schedule")
|
||||
.build(),
|
||||
true); // is active
|
||||
mPrefController.updateZenMode(mPrefCategory, mode);
|
||||
|
||||
verify(mPreference).setTitle(R.string.zen_mode_set_schedule_link);
|
||||
verify(mPreference).setSummary(mode.getRule().getTriggerDescription());
|
||||
|
||||
ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
|
||||
verify(mPreference).setIntent(captor.capture());
|
||||
// Destination as written into the intent by SubSettingLauncher
|
||||
assertThat(
|
||||
captor.getValue().getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT)).isEqualTo(
|
||||
ZenModeSetScheduleFragment.class.getName());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@ import com.android.settings.testutils.shadow.ShadowAlertDialogCompat;
|
||||
import com.android.settings.testutils.shadow.ShadowLockPatternUtils;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
@@ -47,7 +46,6 @@ import org.robolectric.shadows.androidx.fragment.FragmentController;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(shadows = {ShadowAlertDialogCompat.class, ShadowLockPatternUtils.class})
|
||||
@Ignore("b/342667939")
|
||||
public class ChooseLockTypeDialogFragmentTest {
|
||||
|
||||
private Context mContext;
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.settings.network
|
||||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.ConnectivityManager.NetworkCallback
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull
|
||||
import com.android.settingslib.spa.testutils.toListWithTimeout
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.kotlin.any
|
||||
import org.mockito.kotlin.doAnswer
|
||||
import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.mock
|
||||
import org.mockito.kotlin.spy
|
||||
import org.mockito.kotlin.stub
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ConnectivityRepositoryTest {
|
||||
|
||||
private var networkCallback: NetworkCallback? = null
|
||||
|
||||
private val mockConnectivityManager = mock<ConnectivityManager> {
|
||||
on { registerDefaultNetworkCallback(any()) } doAnswer {
|
||||
networkCallback = it.arguments[0] as NetworkCallback
|
||||
}
|
||||
}
|
||||
|
||||
private val context: Context = spy(ApplicationProvider.getApplicationContext()) {
|
||||
on { getSystemService(ConnectivityManager::class.java) } doReturn mockConnectivityManager
|
||||
}
|
||||
|
||||
private val connectivityRepository = ConnectivityRepository(context)
|
||||
|
||||
@Test
|
||||
fun networkCapabilitiesFlow_activeNetworkIsNull_noCrash() = runBlocking {
|
||||
mockConnectivityManager.stub {
|
||||
on { activeNetwork } doReturn null
|
||||
on { getNetworkCapabilities(null) } doReturn null
|
||||
}
|
||||
|
||||
val networkCapabilities =
|
||||
connectivityRepository.networkCapabilitiesFlow().firstWithTimeoutOrNull()!!
|
||||
|
||||
assertThat(networkCapabilities.transportTypes).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun networkCapabilitiesFlow_getInitialValue() = runBlocking {
|
||||
val expectedNetworkCapabilities = NetworkCapabilities.Builder().apply {
|
||||
addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
}.build()
|
||||
mockConnectivityManager.stub {
|
||||
on { getNetworkCapabilities(null) } doReturn expectedNetworkCapabilities
|
||||
}
|
||||
|
||||
val actualNetworkCapabilities =
|
||||
connectivityRepository.networkCapabilitiesFlow().firstWithTimeoutOrNull()!!
|
||||
|
||||
assertThat(actualNetworkCapabilities).isSameInstanceAs(expectedNetworkCapabilities)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun networkCapabilitiesFlow_getUpdatedValue() = runBlocking {
|
||||
val expectedNetworkCapabilities = NetworkCapabilities.Builder().apply {
|
||||
addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
}.build()
|
||||
|
||||
val deferredList = async {
|
||||
connectivityRepository.networkCapabilitiesFlow().toListWithTimeout()
|
||||
}
|
||||
delay(100)
|
||||
networkCallback?.onCapabilitiesChanged(mock<Network>(), expectedNetworkCapabilities)
|
||||
|
||||
assertThat(deferredList.await().last()).isSameInstanceAs(expectedNetworkCapabilities)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.settings.network
|
||||
|
||||
import android.content.Context
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.wifi.WifiManager
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.android.settings.R
|
||||
import com.android.settings.wifi.WifiSummaryRepository
|
||||
import com.android.settings.wifi.repository.WifiRepository
|
||||
import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.mock
|
||||
import org.mockito.kotlin.stub
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class InternetPreferenceRepositoryTest {
|
||||
|
||||
private val context: Context = ApplicationProvider.getApplicationContext()
|
||||
|
||||
private val mockConnectivityRepository = mock<ConnectivityRepository>()
|
||||
private val mockWifiSummaryRepository = mock<WifiSummaryRepository>()
|
||||
private val mockWifiRepository = mock<WifiRepository>()
|
||||
private val airplaneModeOnFlow = MutableStateFlow(false)
|
||||
|
||||
private val repository = InternetPreferenceRepository(
|
||||
context = context,
|
||||
connectivityRepository = mockConnectivityRepository,
|
||||
wifiSummaryRepository = mockWifiSummaryRepository,
|
||||
wifiRepository = mockWifiRepository,
|
||||
airplaneModeOnFlow = airplaneModeOnFlow,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun summaryFlow_wifi() = runBlocking {
|
||||
val wifiNetworkCapabilities = NetworkCapabilities.Builder().apply {
|
||||
addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||
}.build()
|
||||
mockConnectivityRepository.stub {
|
||||
on { networkCapabilitiesFlow() } doReturn flowOf(wifiNetworkCapabilities)
|
||||
}
|
||||
mockWifiSummaryRepository.stub {
|
||||
on { summaryFlow() } doReturn flowOf(SUMMARY)
|
||||
}
|
||||
|
||||
val summary = repository.summaryFlow().firstWithTimeoutOrNull()
|
||||
|
||||
assertThat(summary).isEqualTo(SUMMARY)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun summaryFlow_airplaneModeOnAndWifiOn() = runBlocking {
|
||||
mockConnectivityRepository.stub {
|
||||
on { networkCapabilitiesFlow() } doReturn flowOf(NetworkCapabilities())
|
||||
}
|
||||
airplaneModeOnFlow.value = true
|
||||
mockWifiRepository.stub {
|
||||
on { wifiStateFlow() } doReturn flowOf(WifiManager.WIFI_STATE_ENABLED)
|
||||
}
|
||||
|
||||
val summary = repository.summaryFlow().firstWithTimeoutOrNull()
|
||||
|
||||
assertThat(summary).isEqualTo(context.getString(R.string.networks_available))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun summaryFlow_airplaneModeOnAndWifiOff() = runBlocking {
|
||||
mockConnectivityRepository.stub {
|
||||
on { networkCapabilitiesFlow() } doReturn flowOf(NetworkCapabilities())
|
||||
}
|
||||
airplaneModeOnFlow.value = true
|
||||
mockWifiRepository.stub {
|
||||
on { wifiStateFlow() } doReturn flowOf(WifiManager.WIFI_STATE_DISABLED)
|
||||
}
|
||||
|
||||
val summary = repository.summaryFlow().firstWithTimeoutOrNull()
|
||||
|
||||
assertThat(summary).isEqualTo(context.getString(R.string.condition_airplane_title))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun summaryFlow_airplaneModeOff() = runBlocking {
|
||||
mockConnectivityRepository.stub {
|
||||
on { networkCapabilitiesFlow() } doReturn flowOf(NetworkCapabilities())
|
||||
}
|
||||
airplaneModeOnFlow.value = false
|
||||
mockWifiRepository.stub {
|
||||
on { wifiStateFlow() } doReturn flowOf(WifiManager.WIFI_STATE_DISABLED)
|
||||
}
|
||||
|
||||
val summary = repository.summaryFlow().firstWithTimeoutOrNull()
|
||||
|
||||
assertThat(summary).isEqualTo(context.getString(R.string.networks_available))
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val SUMMARY = "Summary"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.settings.wifi.repository
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.wifi.WifiManager
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class WifiRepositoryTest {
|
||||
|
||||
private val context: Context = ApplicationProvider.getApplicationContext()
|
||||
|
||||
private val mockWifiStateChangedActionFlow = flowOf(Intent().apply {
|
||||
putExtra(WifiManager.EXTRA_WIFI_STATE, WifiManager.WIFI_STATE_ENABLED)
|
||||
})
|
||||
|
||||
private val repository = WifiRepository(context, mockWifiStateChangedActionFlow)
|
||||
|
||||
@Test
|
||||
fun wifiStateFlow() = runBlocking {
|
||||
val wifiState = repository.wifiStateFlow().firstWithTimeoutOrNull()
|
||||
|
||||
assertThat(wifiState).isEqualTo(WifiManager.WIFI_STATE_ENABLED)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user