Snap for 11954976 from 9692e940ec to 24Q3-release

Change-Id: I9082e0365a00a7d78b18d3e19d4aaa5de12be117
This commit is contained in:
Android Build Coastguard Worker
2024-06-11 23:23:32 +00:00
69 changed files with 4037 additions and 304 deletions

View File

@@ -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
}
}

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@@ -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>

View File

@@ -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>

View 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>

View File

@@ -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">

View 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>

View File

@@ -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();

View File

@@ -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);
}
}
}

View File

@@ -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) {

View File

@@ -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();

View File

@@ -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();
}
}

View File

@@ -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);
});
}
}

View File

@@ -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();
}
}

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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);
}
});
}

View File

@@ -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)
};
}
}

View File

@@ -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);

View File

@@ -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());

View 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"
}
}

View File

@@ -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
}

View File

@@ -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"
}
}

View File

@@ -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);

View 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)));
}
}

View File

@@ -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;
}

View File

@@ -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();
});
}
}

View File

@@ -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;
}
}

View File

@@ -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(

View File

@@ -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;
}

View File

@@ -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());
}
}

View File

@@ -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));
}
}

View File

@@ -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());
}
}

View File

@@ -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);
}
}
}

View File

@@ -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());
}
}

View File

@@ -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;
}
}

View File

@@ -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
}
}
}

View File

@@ -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());

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View 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"
}
}

View File

@@ -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);
}
}

View File

@@ -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());
}
}

View File

@@ -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();
}
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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));
}
}

View File

@@ -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) {

View File

@@ -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));
}
}

View File

@@ -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));
}
}

View File

@@ -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());
}
}

View File

@@ -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));
}
}

View File

@@ -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));
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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());
}
}

View File

@@ -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;

View File

@@ -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)
}
}

View File

@@ -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"
}
}

View File

@@ -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)
}
}