Snap for 12368321 from 428829bfc5 to 24Q4-release

Change-Id: I657935e185a2cf0d3543c26f0e553ea87fcc5ec3
This commit is contained in:
Android Build Coastguard Worker
2024-09-14 01:22:08 +00:00
65 changed files with 1789 additions and 704 deletions

View File

@@ -94,8 +94,10 @@ android_library {
"MediaDrmSettingsFlagsLib", "MediaDrmSettingsFlagsLib",
"Settings-change-ids", "Settings-change-ids",
"SettingsLib", "SettingsLib",
"SettingsLibDataStore",
"SettingsLibActivityEmbedding", "SettingsLibActivityEmbedding",
"SettingsLibDataStore",
"SettingsLibMetadata",
"SettingsLibPreference",
"aconfig_settings_flags_lib", "aconfig_settings_flags_lib",
"accessibility_settings_flags_lib", "accessibility_settings_flags_lib",
"contextualcards", "contextualcards",

View File

@@ -1297,7 +1297,7 @@
<activity <activity
android:name="Settings$ModesSettingsActivity" android:name="Settings$ModesSettingsActivity"
android:label="@string/zen_modes_list_title" android:label="@string/zen_modes_list_title"
android:icon="@drawable/ic_homepage_notification" android:icon="@*android:drawable/ic_zen_priority_modes"
android:exported="true"> android:exported="true">
<intent-filter android:priority="1" <intent-filter android:priority="1"
android:featureFlag="android.app.modes_ui"> android:featureFlag="android.app.modes_ui">
@@ -2813,6 +2813,9 @@
<activity android:name=".biometrics.fingerprint.FingerprintEnrollFinish" android:exported="false"/> <activity android:name=".biometrics.fingerprint.FingerprintEnrollFinish" android:exported="false"/>
<activity android:name=".biometrics.fingerprint.FingerprintEnrollParentalConsent" android:exported="false"/> <activity android:name=".biometrics.fingerprint.FingerprintEnrollParentalConsent" android:exported="false"/>
<activity android:name=".biometrics.fingerprint.FingerprintEnrollIntroduction" <activity android:name=".biometrics.fingerprint.FingerprintEnrollIntroduction"
android:exported="false"
android:theme="@style/GlifTheme.Light" />
<activity android:name=".biometrics.fingerprint.FingerprintEnroll"
android:exported="true" android:exported="true"
android:theme="@style/GlifTheme.Light"> android:theme="@style/GlifTheme.Light">
<intent-filter> <intent-filter>
@@ -2823,9 +2826,13 @@
</activity> </activity>
<activity android:name=".biometrics.fingerprint.FingerprintEnrollIntroductionInternal" <activity android:name=".biometrics.fingerprint.FingerprintEnrollIntroductionInternal"
android:exported="false" android:exported="false"
android:theme="@style/GlifTheme.Light" android:theme="@style/GlifTheme.Light"
android:taskAffinity="com.android.settings.root" /> android:taskAffinity="com.android.settings.root" />
<activity android:name=".biometrics.fingerprint.FingerprintEnroll$InternalActivity"
android:exported="false"
android:theme="@style/GlifTheme.Light"
android:taskAffinity="com.android.settings.root" />
<activity android:name=".biometrics.fingerprint.SetupFingerprintEnrollFindSensor" <activity android:name=".biometrics.fingerprint.SetupFingerprintEnrollFindSensor"
android:exported="false" android:exported="false"
@@ -2833,6 +2840,10 @@
<activity android:name=".biometrics.fingerprint.SetupFingerprintEnrollEnrolling" android:exported="false"/> <activity android:name=".biometrics.fingerprint.SetupFingerprintEnrollEnrolling" android:exported="false"/>
<activity android:name=".biometrics.fingerprint.SetupFingerprintEnrollFinish" android:exported="false"/> <activity android:name=".biometrics.fingerprint.SetupFingerprintEnrollFinish" android:exported="false"/>
<activity android:name=".biometrics.fingerprint.SetupFingerprintEnrollIntroduction" <activity android:name=".biometrics.fingerprint.SetupFingerprintEnrollIntroduction"
android:exported="false"
android:permission="android.permission.MANAGE_FINGERPRINT"
android:theme="@style/GlifTheme.Light" />
<activity android:name=".biometrics.fingerprint.FingerprintEnroll$SetupActivity"
android:exported="true" android:exported="true"
android:permission="android.permission.MANAGE_FINGERPRINT" android:permission="android.permission.MANAGE_FINGERPRINT"
android:theme="@style/GlifTheme.Light"> android:theme="@style/GlifTheme.Light">
@@ -2842,7 +2853,6 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name=".biometrics.fingerprint2.ui.enrollment.activity.FingerprintEnrollmentV2Activity" <activity android:name=".biometrics.fingerprint2.ui.enrollment.activity.FingerprintEnrollmentV2Activity"
android:exported="true" android:exported="true"
android:permission="android.permission.MANAGE_FINGERPRINT" android:permission="android.permission.MANAGE_FINGERPRINT"

View File

@@ -7867,7 +7867,9 @@
<string name="keywords_keyboard_vibration">keyboard, haptics, vibrate,</string> <string name="keywords_keyboard_vibration">keyboard, haptics, vibrate,</string>
<!-- Summary for sound settings, explaining a few important settings under it [CHAR LIMIT=NONE]--> <!-- Summary for sound settings, explaining a few important settings under it [CHAR LIMIT=NONE]-->
<string name="sound_dashboard_summary">Volume, vibration, Do Not Disturb</string> <string name="sound_dashboard_summary">Volume and vibration</string>
<!-- Summary for sound settings, explaining a few important settings under it [CHAR LIMIT=NONE]-->
<string name="sound_dashboard_summary_with_dnd">Volume, vibration, Do Not Disturb</string>
<!-- Sound: Title for the option managing media volume. [CHAR LIMIT=30] --> <!-- Sound: Title for the option managing media volume. [CHAR LIMIT=30] -->
<string name="media_volume_option_title">Media volume</string> <string name="media_volume_option_title">Media volume</string>
@@ -13366,9 +13368,9 @@
<!-- Summary of the Live Caption enabled state. --> <!-- Summary of the Live Caption enabled state. -->
<string name="live_caption_enabled">On</string> <string name="live_caption_enabled">On</string>
<!-- State description for the Audio Balance seek bar, with left reported before right. --> <!-- State description for the Audio Balance seek bar, with left reported before right. -->
<string name="audio_seek_bar_state_left_first">Audio %1$d%% left, %2$d%% right</string> <string name="audio_seek_bar_state_left_first">Audio <xliff:g id="percent_left">%1$s</xliff:g> left, <xliff:g id="percent_right">%2$s</xliff:g> right</string>
<!-- State description for the Audio Balance seek bar, with right reported before left. --> <!-- State description for the Audio Balance seek bar, with right reported before left. -->
<string name="audio_seek_bar_state_right_first">Audio %1$d%% right, %2$d%% left</string> <string name="audio_seek_bar_state_right_first">Audio <xliff:g id="percent_right">%1$s</xliff:g> right, <xliff:g id="percent_left">%2$s</xliff:g> left</string>
<!-- Warning text about the visibility of device name. [CHAR LIMIT=NONE] --> <!-- Warning text about the visibility of device name. [CHAR LIMIT=NONE] -->
<string name="about_phone_device_name_warning">Your device name is visible to apps you installed. It may also be seen by other people when you connect to Bluetooth devices, connect to a Wi-Fi network or set up a Wi-Fi hotspot.</string> <string name="about_phone_device_name_warning">Your device name is visible to apps you installed. It may also be seen by other people when you connect to Bluetooth devices, connect to a Wi-Fi network or set up a Wi-Fi hotspot.</string>
@@ -13577,6 +13579,8 @@
<string name="audio_streams_dialog_cannot_play">Can\u0027t play this audio stream on <xliff:g example="LE headset" id="device_name">%1$s</xliff:g>.</string> <string name="audio_streams_dialog_cannot_play">Can\u0027t play this audio stream on <xliff:g example="LE headset" id="device_name">%1$s</xliff:g>.</string>
<!-- The preference summary when add source succeed [CHAR LIMIT=NONE] --> <!-- The preference summary when add source succeed [CHAR LIMIT=NONE] -->
<string name="audio_streams_listening_now">Listening now</string> <string name="audio_streams_listening_now">Listening now</string>
<!-- The preference summary when source is present on sinks [CHAR LIMIT=NONE] -->
<string name="audio_streams_present_now">Paused by host</string>
<!-- Le audio streams service notification leave broadcast text [CHAR LIMIT=NONE] --> <!-- Le audio streams service notification leave broadcast text [CHAR LIMIT=NONE] -->
<string name="audio_streams_media_service_notification_leave_broadcast_text">Stop listening</string> <string name="audio_streams_media_service_notification_leave_broadcast_text">Stop listening</string>
<!-- Le audio streams no le device dialog title [CHAR LIMIT=NONE] --> <!-- Le audio streams no le device dialog title [CHAR LIMIT=NONE] -->

View File

@@ -104,8 +104,9 @@
android:key="top_level_sound" android:key="top_level_sound"
android:order="-90" android:order="-90"
android:title="@string/sound_settings" android:title="@string/sound_settings"
android:summary="@string/sound_dashboard_summary" android:summary="@string/sound_dashboard_summary_with_dnd"
settings:highlightableMenuKey="@string/menu_key_sound"/> settings:highlightableMenuKey="@string/menu_key_sound"
settings:controller="com.android.settings.sound.TopLevelSoundPreferenceController"/>
<com.android.settings.widget.HomepagePreference <com.android.settings.widget.HomepagePreference
android:fragment="com.android.settings.DisplaySettings" android:fragment="com.android.settings.DisplaySettings"

View File

@@ -79,8 +79,9 @@
android:key="top_level_sound" android:key="top_level_sound"
android:order="-40" android:order="-40"
android:title="@string/sound_settings" android:title="@string/sound_settings"
android:summary="@string/sound_dashboard_summary" android:summary="@string/sound_dashboard_summary_with_dnd"
settings:highlightableMenuKey="@string/menu_key_sound"/> settings:highlightableMenuKey="@string/menu_key_sound"
settings:controller="com.android.settings.sound.TopLevelSoundPreferenceController"/>
<com.android.settings.widget.RestrictedHomepagePreference <com.android.settings.widget.RestrictedHomepagePreference
android:fragment="com.android.settings.notification.modes.ZenModesListFragment" android:fragment="com.android.settings.notification.modes.ZenModesListFragment"

View File

@@ -26,6 +26,7 @@ import android.graphics.drawable.Drawable;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import com.android.settings.R; import com.android.settings.R;
@@ -101,6 +102,11 @@ public class AccessibilityActivityPreference extends RestrictedPreference {
return mLabel; return mLabel;
} }
@NonNull
public ComponentName getComponentName() {
return mComponentName;
}
private Drawable getA11yActivityIcon() { private Drawable getA11yActivityIcon() {
ActivityInfo activityInfo = mA11yShortcutInfo.getActivityInfo(); ActivityInfo activityInfo = mA11yShortcutInfo.getActivityInfo();
Drawable serviceIcon; Drawable serviceIcon;

View File

@@ -16,8 +16,12 @@
package com.android.settings.accessibility; package com.android.settings.accessibility;
import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.settingslib.search.SearchIndexableRaw; import com.android.settingslib.search.SearchIndexableRaw;
import java.util.List; import java.util.List;
@@ -28,10 +32,22 @@ import java.util.List;
public interface AccessibilitySearchFeatureProvider { public interface AccessibilitySearchFeatureProvider {
/** /**
* Returns a list of raw data for indexing. See {@link SearchIndexableRaw} * Returns accessibility features to be searched where the accessibility features are always on
* the device and their feature names won't change.
* *
* @param context a valid context {@link Context} instance * @param context a valid context {@link Context} instance
* @return a list of {@link SearchIndexableRaw} references. Can be null. * @return a list of {@link SearchIndexableRaw} references
*/ */
@Nullable
List<SearchIndexableRaw> getSearchIndexableRawData(Context context); List<SearchIndexableRaw> getSearchIndexableRawData(Context context);
/**
* Returns synonyms of the Accessibility component that is used for search.
*
* @param context the context that is used for grabbing resources
* @param componentName the ComponentName of the accessibility feature
* @return a comma separated synonyms e.g. "wifi, wi-fi, network connection"
*/
@NonNull
String getSynonymsForComponent(@NonNull Context context, @NonNull ComponentName componentName);
} }

View File

@@ -16,8 +16,12 @@
package com.android.settings.accessibility; package com.android.settings.accessibility;
import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.settingslib.search.SearchIndexableRaw; import com.android.settingslib.search.SearchIndexableRaw;
import java.util.List; import java.util.List;
@@ -27,8 +31,16 @@ import java.util.List;
*/ */
public class AccessibilitySearchFeatureProviderImpl implements AccessibilitySearchFeatureProvider { public class AccessibilitySearchFeatureProviderImpl implements AccessibilitySearchFeatureProvider {
@Nullable
@Override @Override
public List<SearchIndexableRaw> getSearchIndexableRawData(Context context) { public List<SearchIndexableRaw> getSearchIndexableRawData(Context context) {
return null; return null;
} }
@NonNull
@Override
public String getSynonymsForComponent(@NonNull Context context,
@NonNull ComponentName componentName) {
return "";
}
} }

View File

@@ -26,6 +26,7 @@ import android.graphics.drawable.Drawable;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import com.android.settings.R; import com.android.settings.R;
@@ -95,6 +96,11 @@ public class AccessibilityServicePreference extends RestrictedPreference {
super.performClick(); super.performClick();
} }
@NonNull
public ComponentName getComponentName() {
return mComponentName;
}
private Drawable getA11yServiceIcon() { private Drawable getA11yServiceIcon() {
ResolveInfo resolveInfo = mA11yServiceInfo.getResolveInfo(); ResolveInfo resolveInfo = mA11yServiceInfo.getResolveInfo();
Drawable serviceIcon; Drawable serviceIcon;

View File

@@ -473,7 +473,7 @@ public class AccessibilitySettings extends DashboardFragment implements
* @param installedShortcutList A list of installed {@link AccessibilityShortcutInfo}s. * @param installedShortcutList A list of installed {@link AccessibilityShortcutInfo}s.
* @param installedServiceList A list of installed {@link AccessibilityServiceInfo}s. * @param installedServiceList A list of installed {@link AccessibilityServiceInfo}s.
*/ */
private List<RestrictedPreference> getInstalledAccessibilityPreferences(Context context, private static List<RestrictedPreference> getInstalledAccessibilityPreferences(Context context,
List<AccessibilityShortcutInfo> installedShortcutList, List<AccessibilityShortcutInfo> installedShortcutList,
List<AccessibilityServiceInfo> installedServiceList) { List<AccessibilityServiceInfo> installedServiceList) {
final RestrictedPreferenceHelper preferenceHelper = new RestrictedPreferenceHelper(context); final RestrictedPreferenceHelper preferenceHelper = new RestrictedPreferenceHelper(context);
@@ -623,6 +623,51 @@ public class AccessibilitySettings extends DashboardFragment implements
.getAccessibilitySearchFeatureProvider().getSearchIndexableRawData( .getAccessibilitySearchFeatureProvider().getSearchIndexableRawData(
context); context);
} }
@Override
public List<SearchIndexableRaw> getDynamicRawDataToIndex(Context context,
boolean enabled) {
List<SearchIndexableRaw> dynamicRawData = super.getDynamicRawDataToIndex(
context, enabled);
if (dynamicRawData == null) {
dynamicRawData = new ArrayList<>();
}
if (!Flags.fixA11ySettingsSearch()) {
return dynamicRawData;
}
AccessibilityManager a11yManager = context.getSystemService(
AccessibilityManager.class);
AccessibilitySearchFeatureProvider a11ySearchFeatureProvider =
FeatureFactory.getFeatureFactory()
.getAccessibilitySearchFeatureProvider();
List<RestrictedPreference> installedA11yFeaturesPref =
AccessibilitySettings.getInstalledAccessibilityPreferences(
context,
a11yManager.getInstalledAccessibilityShortcutListAsUser(
context, UserHandle.myUserId()),
a11yManager.getInstalledAccessibilityServiceList()
);
for (RestrictedPreference pref : installedA11yFeaturesPref) {
SearchIndexableRaw indexableRaw = new SearchIndexableRaw(context);
indexableRaw.key = pref.getKey();
indexableRaw.title = pref.getTitle().toString();
@NonNull String synonyms = "";
if (pref instanceof AccessibilityServicePreference) {
synonyms = a11ySearchFeatureProvider.getSynonymsForComponent(
context,
((AccessibilityServicePreference) pref).getComponentName());
} else if (pref instanceof AccessibilityActivityPreference) {
synonyms = a11ySearchFeatureProvider.getSynonymsForComponent(
context,
((AccessibilityActivityPreference) pref).getComponentName());
}
indexableRaw.keywords = synonyms;
dynamicRawData.add(indexableRaw);
}
return dynamicRawData;
}
}; };
@Override @Override

View File

@@ -36,6 +36,7 @@ import android.widget.SeekBar;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import com.android.settings.R; import com.android.settings.R;
import com.android.settings.Utils;
/** /**
* A custom seekbar for the balance setting. * A custom seekbar for the balance setting.
@@ -178,10 +179,12 @@ public class BalanceSeekBar extends SeekBar {
== LAYOUT_DIRECTION_RTL; == LAYOUT_DIRECTION_RTL;
final int rightPercent = (int) (100 * (progress / max)); final int rightPercent = (int) (100 * (progress / max));
final int leftPercent = 100 - rightPercent; final int leftPercent = 100 - rightPercent;
final String rightPercentString = Utils.formatPercentage(rightPercent);
final String leftPercentString = Utils.formatPercentage(leftPercent);
if (rightPercent > leftPercent || (rightPercent == leftPercent && isLayoutRtl)) { if (rightPercent > leftPercent || (rightPercent == leftPercent && isLayoutRtl)) {
return context.getString(resIdRightFirst, rightPercent, leftPercent); return context.getString(resIdRightFirst, rightPercentString, leftPercentString);
} else { } else {
return context.getString(resIdLeftFirst, leftPercent, rightPercent); return context.getString(resIdLeftFirst, leftPercentString, rightPercentString);
} }
} }
} }

View File

@@ -40,9 +40,8 @@ import com.android.settings.SettingsActivity;
import com.android.settings.SubSettings; import com.android.settings.SubSettings;
import com.android.settings.biometrics.face.FaceEnrollIntroduction; import com.android.settings.biometrics.face.FaceEnrollIntroduction;
import com.android.settings.biometrics.face.FaceEnrollIntroductionInternal; import com.android.settings.biometrics.face.FaceEnrollIntroductionInternal;
import com.android.settings.biometrics.fingerprint.FingerprintEnrollActivityClassProvider;
import com.android.settings.biometrics.fingerprint.FingerprintEnrollEnrolling; import com.android.settings.biometrics.fingerprint.FingerprintEnrollEnrolling;
import com.android.settings.biometrics.fingerprint.FingerprintEnrollIntroduction;
import com.android.settings.biometrics.fingerprint.FingerprintEnrollIntroductionInternal;
import com.android.settings.core.FeatureFlags; import com.android.settings.core.FeatureFlags;
import com.android.settings.homepage.DeepLinkHomepageActivity; import com.android.settings.homepage.DeepLinkHomepageActivity;
import com.android.settings.homepage.DeepLinkHomepageActivityInternal; import com.android.settings.homepage.DeepLinkHomepageActivityInternal;
@@ -255,8 +254,12 @@ public class ActivityEmbeddingRulesController {
.buildSearchIntent(mContext, SettingsEnums.SETTINGS_HOMEPAGE); .buildSearchIntent(mContext, SettingsEnums.SETTINGS_HOMEPAGE);
addActivityFilter(activityFilters, searchIntent); addActivityFilter(activityFilters, searchIntent);
} }
addActivityFilter(activityFilters, FingerprintEnrollIntroduction.class); final FingerprintEnrollActivityClassProvider fpClassProvider = FeatureFactory
addActivityFilter(activityFilters, FingerprintEnrollIntroductionInternal.class); .getFeatureFactory()
.getFingerprintFeatureProvider()
.getEnrollActivityClassProvider();
addActivityFilter(activityFilters, fpClassProvider.getDefault());
addActivityFilter(activityFilters, fpClassProvider.getInternal());
addActivityFilter(activityFilters, FingerprintEnrollEnrolling.class); addActivityFilter(activityFilters, FingerprintEnrollEnrolling.class);
addActivityFilter(activityFilters, FaceEnrollIntroductionInternal.class); addActivityFilter(activityFilters, FaceEnrollIntroductionInternal.class);
addActivityFilter(activityFilters, FaceEnrollIntroduction.class); addActivityFilter(activityFilters, FaceEnrollIntroduction.class);

View File

@@ -44,10 +44,9 @@ import com.android.internal.widget.VerifyCredentialResponse;
import com.android.settings.R; import com.android.settings.R;
import com.android.settings.SetupWizardUtils; import com.android.settings.SetupWizardUtils;
import com.android.settings.biometrics.face.FaceEnrollIntroduction; import com.android.settings.biometrics.face.FaceEnrollIntroduction;
import com.android.settings.biometrics.fingerprint.FingerprintEnroll;
import com.android.settings.biometrics.fingerprint.FingerprintEnrollFindSensor; import com.android.settings.biometrics.fingerprint.FingerprintEnrollFindSensor;
import com.android.settings.biometrics.fingerprint.FingerprintEnrollIntroduction;
import com.android.settings.biometrics.fingerprint.SetupFingerprintEnrollFindSensor; import com.android.settings.biometrics.fingerprint.SetupFingerprintEnrollFindSensor;
import com.android.settings.biometrics.fingerprint.SetupFingerprintEnrollIntroduction;
import com.android.settings.overlay.FeatureFactory; import com.android.settings.overlay.FeatureFactory;
import com.android.settings.password.ChooseLockGeneric; import com.android.settings.password.ChooseLockGeneric;
import com.android.settings.password.ChooseLockSettingsHelper; import com.android.settings.password.ChooseLockSettingsHelper;
@@ -262,13 +261,13 @@ public class BiometricUtils {
/** /**
* @param context caller's context * @param context caller's context
* @param activityIntent The intent that started the caller's activity * @param activityIntent The intent that started the caller's activity
* @return Intent for starting FingerprintEnrollIntroduction * @return Intent for starting FingerprintEnroll
*/ */
public static Intent getFingerprintIntroIntent(@NonNull Context context, public static Intent getFingerprintIntroIntent(@NonNull Context context,
@NonNull Intent activityIntent) { @NonNull Intent activityIntent) {
final boolean isSuw = WizardManagerHelper.isAnySetupWizard(activityIntent); final boolean isSuw = WizardManagerHelper.isAnySetupWizard(activityIntent);
final Intent intent = new Intent(context, isSuw final Intent intent = new Intent(context, isSuw
? SetupFingerprintEnrollIntroduction.class : FingerprintEnrollIntroduction.class); ? FingerprintEnroll.SetupActivity.class : FingerprintEnroll.class);
if (isSuw) { if (isSuw) {
WizardManagerHelper.copyWizardManagerExtras(activityIntent, intent); WizardManagerHelper.copyWizardManagerExtras(activityIntent, intent);
} }

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.biometrics.fingerprint
import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import com.android.settings.overlay.FeatureFactory.Companion.featureFactory
/**
* Default class for handling fingerprint enrollment, designed to launch a subsequent activity and
* forward the result, then finish itself.
*/
open class FingerprintEnroll: AppCompatActivity() {
/** Inner class representing enrolling fingerprint enrollment in SetupWizard environment */
class SetupActivity : FingerprintEnroll() {
override val nextActivityClass: Class<*>
get() = enrollActivityProvider.setup
}
/** Inner class representing enrolling fingerprint enrollment from FingerprintSettings */
class InternalActivity : FingerprintEnroll() {
override val nextActivityClass: Class<*>
get() = enrollActivityProvider.internal
}
/**
* The class of the next activity to launch. This is open to allow subclasses to provide their
* own behavior. Defaults to the default activity class provided by the
* enrollActivityClassProvider.
*/
open val nextActivityClass: Class<*>
get() = enrollActivityProvider.default
protected val enrollActivityProvider: FingerprintEnrollActivityClassProvider
get() = featureFactory.fingerprintFeatureProvider.enrollActivityClassProvider
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
/**
* Logs the next activity to be launched, creates an intent for that activity,
* adds flags to forward the result, includes any existing extras from the current intent,
* starts the new activity and then finishes the current one
*/
Log.d("FingerprintEnroll", "forward to $nextActivityClass")
val nextIntent = Intent(this, nextActivityClass)
nextIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT)
nextIntent.putExtras(intent)
startActivity(nextIntent)
finish()
}
}

View File

@@ -0,0 +1,34 @@
/*
* 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.biometrics.fingerprint
import android.app.Activity
open class FingerprintEnrollActivityClassProvider {
open val default: Class<out Activity>
get() = FingerprintEnrollIntroduction::class.java
open val setup: Class<out Activity>
get() = SetupFingerprintEnrollIntroduction::class.java
open val internal: Class<out Activity>
get() = FingerprintEnrollIntroductionInternal::class.java
companion object {
@JvmStatic
val instance = FingerprintEnrollActivityClassProvider()
}
}

View File

@@ -33,7 +33,6 @@ public interface FingerprintFeatureProvider {
*/ */
SfpsEnrollmentFeature getSfpsEnrollmentFeature(); SfpsEnrollmentFeature getSfpsEnrollmentFeature();
/** /**
* Gets calibrator for udfps pre-enroll * Gets calibrator for udfps pre-enroll
* @param appContext application context * @param appContext application context
@@ -52,4 +51,13 @@ public interface FingerprintFeatureProvider {
* @return the feature implementation * @return the feature implementation
*/ */
SfpsRestToUnlockFeature getSfpsRestToUnlockFeature(@NonNull Context context); SfpsRestToUnlockFeature getSfpsRestToUnlockFeature(@NonNull Context context);
/**
* Gets the provider for current fingerprint enrollment activity classes
* @return the provider
*/
@NonNull
default FingerprintEnrollActivityClassProvider getEnrollActivityClassProvider() {
return FingerprintEnrollActivityClassProvider.getInstance();
}
} }

View File

@@ -1142,7 +1142,7 @@ public class FingerprintSettings extends SubSettings {
private void addFirstFingerprint(@Nullable Long gkPwHandle) { private void addFirstFingerprint(@Nullable Long gkPwHandle) {
Intent intent = new Intent(); Intent intent = new Intent();
intent.setClassName(SETTINGS_PACKAGE_NAME, intent.setClassName(SETTINGS_PACKAGE_NAME,
FingerprintEnrollIntroductionInternal.class.getName()); FingerprintEnroll.InternalActivity.class.getName());
intent.putExtra(EXTRA_FROM_SETTINGS_SUMMARY, true); intent.putExtra(EXTRA_FROM_SETTINGS_SUMMARY, true);
intent.putExtra(SettingsBaseActivity.EXTRA_PAGE_TRANSITION_TYPE, intent.putExtra(SettingsBaseActivity.EXTRA_PAGE_TRANSITION_TYPE,
SettingsTransitionHelper.TransitionType.TRANSITION_SLIDE); SettingsTransitionHelper.TransitionType.TRANSITION_SLIDE);

View File

@@ -43,7 +43,7 @@ import com.android.settings.biometrics.BiometricEnrollBase.CONFIRM_REQUEST
import com.android.settings.biometrics.BiometricEnrollBase.EXTRA_FROM_SETTINGS_SUMMARY import com.android.settings.biometrics.BiometricEnrollBase.EXTRA_FROM_SETTINGS_SUMMARY
import com.android.settings.biometrics.BiometricEnrollBase.RESULT_FINISHED import com.android.settings.biometrics.BiometricEnrollBase.RESULT_FINISHED
import com.android.settings.biometrics.fingerprint.FingerprintEnrollEnrolling import com.android.settings.biometrics.fingerprint.FingerprintEnrollEnrolling
import com.android.settings.biometrics.fingerprint.FingerprintEnrollIntroductionInternal import com.android.settings.biometrics.fingerprint.FingerprintEnroll.InternalActivity
import com.android.settings.biometrics.fingerprint2.data.repository.FingerprintSensorRepositoryImpl import com.android.settings.biometrics.fingerprint2.data.repository.FingerprintSensorRepositoryImpl
import com.android.settings.biometrics.fingerprint2.domain.interactor.PressToAuthInteractorImpl import com.android.settings.biometrics.fingerprint2.domain.interactor.PressToAuthInteractorImpl
import com.android.settings.biometrics.fingerprint2.lib.model.FingerprintAuthAttemptModel import com.android.settings.biometrics.fingerprint2.lib.model.FingerprintAuthAttemptModel
@@ -514,7 +514,7 @@ class FingerprintSettingsV2Fragment :
val intent = Intent() val intent = Intent()
intent.setClassName( intent.setClassName(
SETTINGS_PACKAGE_NAME, SETTINGS_PACKAGE_NAME,
FingerprintEnrollIntroductionInternal::class.java.name, InternalActivity::class.java.name,
) )
intent.putExtra(EXTRA_FROM_SETTINGS_SUMMARY, true) intent.putExtra(EXTRA_FROM_SETTINGS_SUMMARY, true)
intent.putExtra( intent.putExtra(

View File

@@ -22,4 +22,7 @@ import kotlinx.coroutines.flow.Flow
data class DeviceSettingLayout(val rows: List<DeviceSettingLayoutRow>) data class DeviceSettingLayout(val rows: List<DeviceSettingLayoutRow>)
/** Represent a row in the layout. */ /** Represent a row in the layout. */
data class DeviceSettingLayoutRow(val settingIds: Flow<List<Int>>) data class DeviceSettingLayoutRow(val columns: Flow<List<DeviceSettingLayoutColumn>>)
/** Represent a column in a row. */
data class DeviceSettingLayoutColumn(val settingId: Int, val highlighted: Boolean)

View File

@@ -20,12 +20,23 @@ import android.bluetooth.BluetoothAdapter
import android.content.Context import android.content.Context
import android.media.AudioManager import android.media.AudioManager
import android.os.Bundle import android.os.Bundle
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@@ -43,7 +54,6 @@ import com.android.settings.core.SubSettingLauncher
import com.android.settings.overlay.FeatureFactory.Companion.featureFactory import com.android.settings.overlay.FeatureFactory.Companion.featureFactory
import com.android.settings.spa.preference.ComposePreference import com.android.settings.spa.preference.ComposePreference
import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingIcon import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingIcon
import com.android.settingslib.spa.framework.theme.SettingsDimension import com.android.settingslib.spa.framework.theme.SettingsDimension
@@ -91,10 +101,16 @@ class DeviceDetailsFragmentFormatterImpl(
) : DeviceDetailsFragmentFormatter { ) : DeviceDetailsFragmentFormatter {
private val repository = private val repository =
featureFactory.bluetoothFeatureProvider.getDeviceSettingRepository( featureFactory.bluetoothFeatureProvider.getDeviceSettingRepository(
context, bluetoothAdapter, fragment.lifecycleScope) context,
bluetoothAdapter,
fragment.lifecycleScope,
)
private val spatialAudioInteractor = private val spatialAudioInteractor =
featureFactory.bluetoothFeatureProvider.getSpatialAudioInteractor( featureFactory.bluetoothFeatureProvider.getSpatialAudioInteractor(
context, context.getSystemService(AudioManager::class.java), fragment.lifecycleScope) context,
context.getSystemService(AudioManager::class.java),
fragment.lifecycleScope,
)
private val viewModel: BluetoothDeviceDetailsViewModel = private val viewModel: BluetoothDeviceDetailsViewModel =
ViewModelProvider( ViewModelProvider(
fragment, fragment,
@@ -104,7 +120,8 @@ class DeviceDetailsFragmentFormatterImpl(
spatialAudioInteractor, spatialAudioInteractor,
cachedDevice, cachedDevice,
backgroundCoroutineContext, backgroundCoroutineContext,
)) ),
)
.get(BluetoothDeviceDetailsViewModel::class.java) .get(BluetoothDeviceDetailsViewModel::class.java)
override fun getVisiblePreferenceKeys(fragmentType: FragmentTypeModel): List<String>? = override fun getVisiblePreferenceKeys(fragmentType: FragmentTypeModel): List<String>? =
@@ -120,7 +137,8 @@ class DeviceDetailsFragmentFormatterImpl(
viewModel viewModel
.getItems(fragmentType) .getItems(fragmentType)
?.filterIsInstance<DeviceSettingConfigItemModel.BuiltinItem.BluetoothProfilesItem>() ?.filterIsInstance<DeviceSettingConfigItemModel.BuiltinItem.BluetoothProfilesItem>()
?.first()?.invisibleProfiles ?.first()
?.invisibleProfiles
} }
/** Updates bluetooth device details fragment layout. */ /** Updates bluetooth device details fragment layout. */
@@ -144,7 +162,8 @@ class DeviceDetailsFragmentFormatterImpl(
val settingId = items[row].settingId val settingId = items[row].settingId
if (settingIdToXmlPreferences.containsKey(settingId)) { if (settingIdToXmlPreferences.containsKey(settingId)) {
fragment.preferenceScreen.addPreference( fragment.preferenceScreen.addPreference(
settingIdToXmlPreferences[settingId]!!.apply { order = row }) settingIdToXmlPreferences[settingId]!!.apply { order = row }
)
} else { } else {
val pref = val pref =
ComposePreference(context) ComposePreference(context)
@@ -169,7 +188,8 @@ class DeviceDetailsFragmentFormatterImpl(
emitAll( emitAll(
viewModel.getDeviceSetting(cachedDevice, item.settingId).map { viewModel.getDeviceSetting(cachedDevice, item.settingId).map {
it as? DeviceSettingPreferenceModel.HelpPreference it as? DeviceSettingPreferenceModel.HelpPreference
}) }
)
} ?: emit(null) } ?: emit(null)
} }
@@ -177,22 +197,56 @@ class DeviceDetailsFragmentFormatterImpl(
private fun buildPreference(layout: DeviceSettingLayout, row: Int) { private fun buildPreference(layout: DeviceSettingLayout, row: Int) {
val contents by val contents by
remember(row) { remember(row) {
layout.rows[row].settingIds.flatMapLatest { settingIds -> layout.rows[row].columns.flatMapLatest { columns ->
if (settingIds.isEmpty()) { if (columns.isEmpty()) {
flowOf(emptyList<DeviceSettingPreferenceModel>()) flowOf(emptyList<DeviceSettingPreferenceModel>())
} else { } else {
combine( combine(
settingIds.map { settingId -> columns.map { column ->
viewModel.getDeviceSetting(cachedDevice, settingId) viewModel.getDeviceSetting(cachedDevice, column.settingId)
}) {
it.toList()
} }
) {
it.toList()
}
} }
} }
} }
.collectAsStateWithLifecycle(initialValue = listOf()) .collectAsStateWithLifecycle(initialValue = listOf())
val highlighted by
remember(row) {
layout.rows[row].columns.map { columns -> columns.any { it.highlighted } }
}
.collectAsStateWithLifecycle(initialValue = false)
val settings = contents val settings = contents
AnimatedVisibility(
visible = settings.isNotEmpty(),
enter = expandVertically(expandFrom = Alignment.Top),
exit = shrinkVertically(shrinkTowards = Alignment.Top),
) {
Box {
Box(
modifier =
Modifier.matchParentSize()
.padding(16.dp, 0.dp, 8.dp, 0.dp)
.background(
color =
if (highlighted) {
MaterialTheme.colorScheme.primaryContainer
} else {
Color.Transparent
},
shape = RoundedCornerShape(28.dp),
),
) {}
buildPreferences(settings)
}
}
}
@Composable
fun buildPreferences(settings: List<DeviceSettingPreferenceModel?>) {
when (settings.size) { when (settings.size) {
0 -> {} 0 -> {}
1 -> { 1 -> {
@@ -217,11 +271,18 @@ class DeviceDetailsFragmentFormatterImpl(
} }
} }
else -> { else -> {
if (!settings.all { it is DeviceSettingPreferenceModel.MultiTogglePreference }) { if (
!settings.all {
it is DeviceSettingPreferenceModel.MultiTogglePreference
}
) {
return return
} }
buildMultiTogglePreference( buildMultiTogglePreference(
settings.filterIsInstance<DeviceSettingPreferenceModel.MultiTogglePreference>()) settings.filterIsInstance<
DeviceSettingPreferenceModel.MultiTogglePreference
>()
)
} }
} }
} }
@@ -243,11 +304,19 @@ class DeviceDetailsFragmentFormatterImpl(
override val onCheckedChange = { newChecked: Boolean -> override val onCheckedChange = { newChecked: Boolean ->
model.onCheckedChange(newChecked) model.onCheckedChange(newChecked)
} }
override val icon = @Composable { deviceSettingIcon(model.icon) } override val icon: (@Composable () -> Unit)?
get() {
if (model.icon == null) {
return null
}
return { deviceSettingIcon(model.icon) }
}
} }
if (model.onPrimaryClick != null) { if (model.onPrimaryClick != null) {
TwoTargetSwitchPreference( TwoTargetSwitchPreference(
switchPrefModel, primaryOnClick = model.onPrimaryClick::invoke) switchPrefModel,
primaryOnClick = model.onPrimaryClick::invoke,
)
} else { } else {
SwitchPreference(switchPrefModel) SwitchPreference(switchPrefModel)
} }
@@ -263,8 +332,15 @@ class DeviceDetailsFragmentFormatterImpl(
model.onClick?.invoke() model.onClick?.invoke()
Unit Unit
} }
override val icon = @Composable { deviceSettingIcon(model.icon) } override val icon: (@Composable () -> Unit)?
}) get() {
if (model.icon == null) {
return null
}
return { deviceSettingIcon(model.icon) }
}
}
)
} }
@Composable @Composable
@@ -281,11 +357,13 @@ class DeviceDetailsFragmentFormatterImpl(
.setDestination(DeviceDetailsMoreSettingsFragment::class.java.name) .setDestination(DeviceDetailsMoreSettingsFragment::class.java.name)
.setSourceMetricsCategory(fragment.getMetricsCategory()) .setSourceMetricsCategory(fragment.getMetricsCategory())
.setArguments( .setArguments(
Bundle().apply { putString(KEY_DEVICE_ADDRESS, cachedDevice.address) }) Bundle().apply { putString(KEY_DEVICE_ADDRESS, cachedDevice.address) }
)
.launch() .launch()
} }
override val icon = @Composable { deviceSettingIcon(null) } override val icon = @Composable { deviceSettingIcon(null) }
}) }
)
} }
@Composable @Composable

View File

@@ -24,6 +24,7 @@ import androidx.lifecycle.viewModelScope
import com.android.settings.R import com.android.settings.R
import com.android.settings.bluetooth.domain.interactor.SpatialAudioInteractor import com.android.settings.bluetooth.domain.interactor.SpatialAudioInteractor
import com.android.settings.bluetooth.ui.layout.DeviceSettingLayout import com.android.settings.bluetooth.ui.layout.DeviceSettingLayout
import com.android.settings.bluetooth.ui.layout.DeviceSettingLayoutColumn
import com.android.settings.bluetooth.ui.layout.DeviceSettingLayoutRow import com.android.settings.bluetooth.ui.layout.DeviceSettingLayoutRow
import com.android.settings.bluetooth.ui.model.DeviceSettingPreferenceModel import com.android.settings.bluetooth.ui.model.DeviceSettingPreferenceModel
import com.android.settings.bluetooth.ui.model.FragmentTypeModel import com.android.settings.bluetooth.ui.model.FragmentTypeModel
@@ -36,7 +37,6 @@ import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSetti
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
@@ -51,7 +51,7 @@ class BluetoothDeviceDetailsViewModel(
private val spatialAudioInteractor: SpatialAudioInteractor, private val spatialAudioInteractor: SpatialAudioInteractor,
private val cachedDevice: CachedBluetoothDevice, private val cachedDevice: CachedBluetoothDevice,
backgroundCoroutineContext: CoroutineContext, backgroundCoroutineContext: CoroutineContext,
) : AndroidViewModel(application){ ) : AndroidViewModel(application) {
private val items = private val items =
viewModelScope.async(backgroundCoroutineContext, start = CoroutineStart.LAZY) { viewModelScope.async(backgroundCoroutineContext, start = CoroutineStart.LAZY) {
@@ -74,7 +74,7 @@ class BluetoothDeviceDetailsViewModel(
fun getDeviceSetting( fun getDeviceSetting(
cachedDevice: CachedBluetoothDevice, cachedDevice: CachedBluetoothDevice,
@DeviceSettingId settingId: Int @DeviceSettingId settingId: Int,
): Flow<DeviceSettingPreferenceModel?> { ): Flow<DeviceSettingPreferenceModel?> {
if (settingId == DeviceSettingId.DEVICE_SETTING_ID_MORE_SETTINGS) { if (settingId == DeviceSettingId.DEVICE_SETTING_ID_MORE_SETTINGS) {
return flowOf(DeviceSettingPreferenceModel.MoreSettingsPreference(settingId)) return flowOf(DeviceSettingPreferenceModel.MoreSettingsPreference(settingId))
@@ -98,16 +98,19 @@ class BluetoothDeviceDetailsViewModel(
checked = switchState?.checked ?: false, checked = switchState?.checked ?: false,
onCheckedChange = { newState -> onCheckedChange = { newState ->
updateState?.invoke( updateState?.invoke(
DeviceSettingStateModel.ActionSwitchPreferenceState(newState)) DeviceSettingStateModel.ActionSwitchPreferenceState(newState)
)
}, },
onPrimaryClick = { intent?.let { application.startActivity(it) } }) onPrimaryClick = { intent?.let { application.startActivity(it) } },
)
} else { } else {
DeviceSettingPreferenceModel.PlainPreference( DeviceSettingPreferenceModel.PlainPreference(
id = id, id = id,
title = title, title = title,
summary = summary, summary = summary,
icon = icon, icon = icon,
onClick = { intent?.let { application.startActivity(it) } }) onClick = { intent?.let { application.startActivity(it) } },
)
} }
} }
is DeviceSettingModel.FooterPreference -> is DeviceSettingModel.FooterPreference ->
@@ -116,9 +119,8 @@ class BluetoothDeviceDetailsViewModel(
DeviceSettingPreferenceModel.HelpPreference( DeviceSettingPreferenceModel.HelpPreference(
id = id, id = id,
icon = DeviceSettingIcon.ResourceIcon(R.drawable.ic_help), icon = DeviceSettingIcon.ResourceIcon(R.drawable.ic_help),
onClick = { onClick = { application.startActivity(intent) },
application.startActivity(intent) )
})
is DeviceSettingModel.MultiTogglePreference -> is DeviceSettingModel.MultiTogglePreference ->
DeviceSettingPreferenceModel.MultiTogglePreference( DeviceSettingPreferenceModel.MultiTogglePreference(
id = id, id = id,
@@ -129,7 +131,8 @@ class BluetoothDeviceDetailsViewModel(
isAllowedChangingState = isAllowedChangingState, isAllowedChangingState = isAllowedChangingState,
onSelectedChange = { newState -> onSelectedChange = { newState ->
updateState(DeviceSettingStateModel.MultiTogglePreferenceState(newState)) updateState(DeviceSettingStateModel.MultiTogglePreferenceState(newState))
}) },
)
is DeviceSettingModel.Unknown -> null is DeviceSettingModel.Unknown -> null
} }
} }
@@ -145,8 +148,8 @@ class BluetoothDeviceDetailsViewModel(
configItems.map { idToDeviceSetting[it.settingId] ?: flowOf(null) } configItems.map { idToDeviceSetting[it.settingId] ?: flowOf(null) }
val positionToSettingIds = val positionToSettingIds =
combine(configDeviceSetting) { settings -> combine(configDeviceSetting) { settings ->
val positionMapping = mutableMapOf<Int, List<Int>>() val positionMapping = mutableMapOf<Int, List<DeviceSettingLayoutColumn>>()
var multiToggleSettingIds: MutableList<Int>? = null var multiToggleSettingIds: MutableList<DeviceSettingLayoutColumn>? = null
for (i in settings.indices) { for (i in settings.indices) {
val configItem = configItems[i] val configItem = configItems[i]
val setting = settings[i] val setting = settings[i]
@@ -156,14 +159,31 @@ class BluetoothDeviceDetailsViewModel(
} }
if (setting !is DeviceSettingPreferenceModel.MultiTogglePreference) { if (setting !is DeviceSettingPreferenceModel.MultiTogglePreference) {
multiToggleSettingIds = null multiToggleSettingIds = null
positionMapping[i] = listOf(configItem.settingId) positionMapping[i] =
listOf(
DeviceSettingLayoutColumn(
configItem.settingId,
configItem.highlighted,
)
)
continue continue
} }
if (multiToggleSettingIds != null) { if (multiToggleSettingIds != null) {
multiToggleSettingIds.add(setting.id) multiToggleSettingIds.add(
DeviceSettingLayoutColumn(
configItem.settingId,
configItem.highlighted,
)
)
} else { } else {
multiToggleSettingIds = mutableListOf(setting.id) multiToggleSettingIds =
mutableListOf(
DeviceSettingLayoutColumn(
configItem.settingId,
configItem.highlighted,
)
)
positionMapping[i] = multiToggleSettingIds positionMapping[i] = multiToggleSettingIds
} }
} }
@@ -173,7 +193,8 @@ class BluetoothDeviceDetailsViewModel(
return DeviceSettingLayout( return DeviceSettingLayout(
configItems.indices.map { idx -> configItems.indices.map { idx ->
DeviceSettingLayoutRow(positionToSettingIds.map { it[idx] ?: emptyList() }) DeviceSettingLayoutRow(positionToSettingIds.map { it[idx] ?: emptyList() })
}) }
)
} }
class Factory( class Factory(
@@ -186,9 +207,12 @@ class BluetoothDeviceDetailsViewModel(
override fun <T : ViewModel> create(modelClass: Class<T>): T { override fun <T : ViewModel> create(modelClass: Class<T>): T {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
return BluetoothDeviceDetailsViewModel( return BluetoothDeviceDetailsViewModel(
application, deviceSettingRepository, spatialAudioInteractor, application,
deviceSettingRepository,
spatialAudioInteractor,
cachedDevice, cachedDevice,
backgroundCoroutineContext) backgroundCoroutineContext,
)
as T as T
} }
} }

View File

@@ -16,6 +16,8 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams; package com.android.settings.connecteddevice.audiosharing.audiostreams;
import static com.android.settingslib.flags.Flags.audioSharingHysteresisModeFix;
import android.app.settings.SettingsEnums; import android.app.settings.SettingsEnums;
import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcastAssistant; import android.bluetooth.BluetoothLeBroadcastAssistant;
@@ -41,6 +43,7 @@ import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import com.android.settingslib.utils.ThreadUtils; import com.android.settingslib.utils.ThreadUtils;
import com.android.settingslib.widget.ActionButtonsPreference; import com.android.settingslib.widget.ActionButtonsPreference;
import java.util.List;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
@@ -73,12 +76,18 @@ public class AudioStreamButtonController extends BasePreferenceController
int sourceId, int sourceId,
BluetoothLeBroadcastReceiveState state) { BluetoothLeBroadcastReceiveState state) {
super.onReceiveStateChanged(sink, sourceId, state); super.onReceiveStateChanged(sink, sourceId, state);
if (AudioStreamsHelper.isConnected(state)) { boolean shouldUpdateButton =
audioSharingHysteresisModeFix()
? AudioStreamsHelper.hasSourcePresent(state)
: AudioStreamsHelper.isConnected(state);
if (shouldUpdateButton) {
updateButton(); updateButton();
mMetricsFeatureProvider.action( if (AudioStreamsHelper.isConnected(state)) {
mContext, mMetricsFeatureProvider.action(
SettingsEnums.ACTION_AUDIO_STREAM_JOIN_SUCCEED, mContext,
SOURCE_ORIGIN_REPOSITORY); SettingsEnums.ACTION_AUDIO_STREAM_JOIN_SUCCEED,
SOURCE_ORIGIN_REPOSITORY);
}
} }
} }
@@ -146,8 +155,13 @@ public class AudioStreamButtonController extends BasePreferenceController
Log.w(TAG, "updateButton(): preference is null!"); Log.w(TAG, "updateButton(): preference is null!");
return; return;
} }
List<BluetoothLeBroadcastReceiveState> sources =
audioSharingHysteresisModeFix()
? mAudioStreamsHelper.getAllPresentSources()
: mAudioStreamsHelper.getAllConnectedSources();
boolean isConnected = boolean isConnected =
mAudioStreamsHelper.getAllConnectedSources().stream() sources.stream()
.map(BluetoothLeBroadcastReceiveState::getBroadcastId) .map(BluetoothLeBroadcastReceiveState::getBroadcastId)
.anyMatch(connectedBroadcastId -> connectedBroadcastId == mBroadcastId); .anyMatch(connectedBroadcastId -> connectedBroadcastId == mBroadcastId);

View File

@@ -16,6 +16,10 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams; package com.android.settings.connecteddevice.audiosharing.audiostreams;
import static com.android.settingslib.flags.Flags.audioSharingHysteresisModeFix;
import static java.util.stream.Collectors.toList;
import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcastAssistant; import android.bluetooth.BluetoothLeBroadcastAssistant;
import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.bluetooth.BluetoothLeBroadcastReceiveState;
@@ -48,6 +52,8 @@ public class AudioStreamHeaderController extends BasePreferenceController
static final int AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY = static final int AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY =
R.string.audio_streams_listening_now; R.string.audio_streams_listening_now;
static final int AUDIO_STREAM_HEADER_PRESENT_NOW_SUMMARY = R.string.audio_streams_present_now;
@VisibleForTesting static final String AUDIO_STREAM_HEADER_NOT_LISTENING_SUMMARY = ""; @VisibleForTesting static final String AUDIO_STREAM_HEADER_NOT_LISTENING_SUMMARY = "";
private static final String TAG = "AudioStreamHeaderController"; private static final String TAG = "AudioStreamHeaderController";
private static final String KEY = "audio_stream_header"; private static final String KEY = "audio_stream_header";
@@ -80,6 +86,10 @@ public class AudioStreamHeaderController extends BasePreferenceController
updateSummary(); updateSummary();
mAudioStreamsHelper.startMediaService( mAudioStreamsHelper.startMediaService(
mContext, mBroadcastId, mBroadcastName); mContext, mBroadcastId, mBroadcastName);
} else if (audioSharingHysteresisModeFix()
&& AudioStreamsHelper.hasSourcePresent(state)) {
// if source present but not connected, only update the summary
updateSummary();
} }
} }
}; };
@@ -140,8 +150,27 @@ public class AudioStreamHeaderController extends BasePreferenceController
var unused = var unused =
ThreadUtils.postOnBackgroundThread( ThreadUtils.postOnBackgroundThread(
() -> { () -> {
var connectedSourceList =
mAudioStreamsHelper.getAllPresentSources().stream()
.filter(
state ->
(state.getBroadcastId()
== mBroadcastId))
.collect(toList());
var latestSummary = var latestSummary =
mAudioStreamsHelper.getAllConnectedSources().stream() audioSharingHysteresisModeFix()
? connectedSourceList.isEmpty()
? AUDIO_STREAM_HEADER_NOT_LISTENING_SUMMARY
: (connectedSourceList.stream()
.anyMatch(
AudioStreamsHelper
::isConnected)
? mContext.getString(
AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY)
: mContext.getString(
AUDIO_STREAM_HEADER_PRESENT_NOW_SUMMARY))
: mAudioStreamsHelper.getAllConnectedSources().stream()
.map( .map(
BluetoothLeBroadcastReceiveState BluetoothLeBroadcastReceiveState
::getBroadcastId) ::getBroadcastId)
@@ -149,9 +178,10 @@ public class AudioStreamHeaderController extends BasePreferenceController
connectedBroadcastId -> connectedBroadcastId ->
connectedBroadcastId connectedBroadcastId
== mBroadcastId) == mBroadcastId)
? mContext.getString( ? mContext.getString(
AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY) AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY)
: AUDIO_STREAM_HEADER_NOT_LISTENING_SUMMARY; : AUDIO_STREAM_HEADER_NOT_LISTENING_SUMMARY;
ThreadUtils.postOnMainThread( ThreadUtils.postOnMainThread(
() -> { () -> {
if (mHeaderController != null) { if (mHeaderController != null) {

View File

@@ -18,6 +18,8 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams;
import static android.text.Spanned.SPAN_EXCLUSIVE_INCLUSIVE; import static android.text.Spanned.SPAN_EXCLUSIVE_INCLUSIVE;
import static com.android.settingslib.flags.Flags.audioSharingHysteresisModeFix;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.text.SpannableString; import android.text.SpannableString;
@@ -94,8 +96,12 @@ class AudioStreamStateHandler {
} }
preference.setIsConnected( preference.setIsConnected(
newState newState
== AudioStreamsProgressCategoryController.AudioStreamState == AudioStreamsProgressCategoryController
.SOURCE_ADDED); .AudioStreamState.SOURCE_ADDED
|| (audioSharingHysteresisModeFix()
&& newState
== AudioStreamsProgressCategoryController
.AudioStreamState.SOURCE_PRESENT));
preference.setOnPreferenceClickListener(getOnClickListener(controller)); preference.setOnPreferenceClickListener(getOnClickListener(controller));
}); });
} }

View File

@@ -19,6 +19,7 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamMediaService.BROADCAST_ID; import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamMediaService.BROADCAST_ID;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamMediaService.BROADCAST_TITLE; import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamMediaService.BROADCAST_TITLE;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamMediaService.DEVICES; import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamMediaService.DEVICES;
import static com.android.settingslib.flags.Flags.audioSharingHysteresisModeFix;
import static java.util.Collections.emptyList; import static java.util.Collections.emptyList;
@@ -63,6 +64,12 @@ public class AudioStreamsHelper {
private final @Nullable LocalBluetoothManager mBluetoothManager; private final @Nullable LocalBluetoothManager mBluetoothManager;
private final @Nullable LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant; private final @Nullable LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
// Referring to Broadcast Audio Scan Service 1.0
// Table 3.9: Broadcast Receive State characteristic format
// 0x00000000: 0b0 = Not synchronized to BIS_index[x]
// 0xFFFFFFFF: Failed to sync to BIG
private static final long BIS_SYNC_NOT_SYNC_TO_BIS = 0x00000000L;
private static final long BIS_SYNC_FAILED_SYNC_TO_BIG = 0xFFFFFFFFL;
AudioStreamsHelper(@Nullable LocalBluetoothManager bluetoothManager) { AudioStreamsHelper(@Nullable LocalBluetoothManager bluetoothManager) {
mBluetoothManager = bluetoothManager; mBluetoothManager = bluetoothManager;
@@ -144,6 +151,19 @@ public class AudioStreamsHelper {
.toList(); .toList();
} }
/** Retrieves a list of all LE broadcast receive states from sinks with source present. */
@VisibleForTesting
public List<BluetoothLeBroadcastReceiveState> getAllPresentSources() {
if (mLeBroadcastAssistant == null) {
Log.w(TAG, "getAllPresentSources(): LeBroadcastAssistant is null!");
return emptyList();
}
return getConnectedBluetoothDevices(mBluetoothManager, /* inSharingOnly= */ true).stream()
.flatMap(sink -> mLeBroadcastAssistant.getAllSources(sink).stream())
.filter(AudioStreamsHelper::hasSourcePresent)
.toList();
}
/** Retrieves LocalBluetoothLeBroadcastAssistant. */ /** Retrieves LocalBluetoothLeBroadcastAssistant. */
@VisibleForTesting @VisibleForTesting
@Nullable @Nullable
@@ -153,7 +173,18 @@ public class AudioStreamsHelper {
/** Checks the connectivity status based on the provided broadcast receive state. */ /** Checks the connectivity status based on the provided broadcast receive state. */
public static boolean isConnected(BluetoothLeBroadcastReceiveState state) { public static boolean isConnected(BluetoothLeBroadcastReceiveState state) {
return state.getBisSyncState().stream().anyMatch(bitmap -> bitmap != 0); return state.getBisSyncState().stream()
.anyMatch(
bitmap ->
(bitmap != BIS_SYNC_NOT_SYNC_TO_BIS
&& bitmap != BIS_SYNC_FAILED_SYNC_TO_BIG));
}
/** Checks the connectivity status based on the provided broadcast receive state. */
public static boolean hasSourcePresent(BluetoothLeBroadcastReceiveState state) {
// Referring to Broadcast Audio Scan Service 1.0
// All zero address means no source on the sink device
return !state.getSourceDevice().getAddress().equals("00:00:00:00:00:00");
} }
static boolean isBadCode(BluetoothLeBroadcastReceiveState state) { static boolean isBadCode(BluetoothLeBroadcastReceiveState state) {
@@ -242,7 +273,8 @@ public class AudioStreamsHelper {
List<BluetoothLeBroadcastReceiveState> sourceList = List<BluetoothLeBroadcastReceiveState> sourceList =
assistant.getAllSources(cachedDevice.getDevice()); assistant.getAllSources(cachedDevice.getDevice());
if (!sourceList.isEmpty() if (!sourceList.isEmpty()
&& sourceList.stream().anyMatch(AudioStreamsHelper::isConnected)) { && (audioSharingHysteresisModeFix()
|| sourceList.stream().anyMatch(AudioStreamsHelper::isConnected))) {
Log.d( Log.d(
TAG, TAG,
"Lead device has connected broadcast source, device = " "Lead device has connected broadcast source, device = "
@@ -253,7 +285,9 @@ public class AudioStreamsHelper {
for (CachedBluetoothDevice device : cachedDevice.getMemberDevice()) { for (CachedBluetoothDevice device : cachedDevice.getMemberDevice()) {
List<BluetoothLeBroadcastReceiveState> list = List<BluetoothLeBroadcastReceiveState> list =
assistant.getAllSources(device.getDevice()); assistant.getAllSources(device.getDevice());
if (!list.isEmpty() && list.stream().anyMatch(AudioStreamsHelper::isConnected)) { if (!list.isEmpty()
&& (audioSharingHysteresisModeFix()
|| list.stream().anyMatch(AudioStreamsHelper::isConnected))) {
Log.d( Log.d(
TAG, TAG,
"Member device has connected broadcast source, device = " "Member device has connected broadcast source, device = "

View File

@@ -16,6 +16,8 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams; package com.android.settings.connecteddevice.audiosharing.audiostreams;
import static com.android.settingslib.flags.Flags.audioSharingHysteresisModeFix;
import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcastMetadata; import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.bluetooth.BluetoothLeBroadcastReceiveState;
@@ -39,6 +41,9 @@ public class AudioStreamsProgressCategoryCallback extends AudioStreamsBroadcastA
mCategoryController.handleSourceConnected(state); mCategoryController.handleSourceConnected(state);
} else if (AudioStreamsHelper.isBadCode(state)) { } else if (AudioStreamsHelper.isBadCode(state)) {
mCategoryController.handleSourceConnectBadCode(state); mCategoryController.handleSourceConnectBadCode(state);
} else if (audioSharingHysteresisModeFix() && AudioStreamsHelper.hasSourcePresent(state)) {
// Keep this check as the last, source might also present in above states
mCategoryController.handleSourcePresent(state);
} }
} }

View File

@@ -16,6 +16,8 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams; package com.android.settings.connecteddevice.audiosharing.audiostreams;
import static com.android.settingslib.flags.Flags.audioSharingHysteresisModeFix;
import static java.util.Collections.emptyList; import static java.util.Collections.emptyList;
import android.app.AlertDialog; import android.app.AlertDialog;
@@ -48,6 +50,7 @@ import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.utils.ThreadUtils; import com.android.settingslib.utils.ThreadUtils;
import java.util.Comparator; import java.util.Comparator;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
@@ -95,9 +98,14 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
private final Comparator<AudioStreamPreference> mComparator = private final Comparator<AudioStreamPreference> mComparator =
Comparator.<AudioStreamPreference, Boolean>comparing( Comparator.<AudioStreamPreference, Boolean>comparing(
p -> p ->
p.getAudioStreamState() (p.getAudioStreamState()
== AudioStreamsProgressCategoryController == AudioStreamsProgressCategoryController
.AudioStreamState.SOURCE_ADDED) .AudioStreamState.SOURCE_ADDED
|| (audioSharingHysteresisModeFix()
&& p.getAudioStreamState()
== AudioStreamsProgressCategoryController
.AudioStreamState
.SOURCE_PRESENT)))
.thenComparingInt(AudioStreamPreference::getAudioStreamRssi) .thenComparingInt(AudioStreamPreference::getAudioStreamRssi)
.reversed(); .reversed();
@@ -113,6 +121,8 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
ADD_SOURCE_BAD_CODE, ADD_SOURCE_BAD_CODE,
// When addSource result in other bad state. // When addSource result in other bad state.
ADD_SOURCE_FAILED, ADD_SOURCE_FAILED,
// Source is present on sink.
SOURCE_PRESENT,
// Source is added to active sink. // Source is added to active sink.
SOURCE_ADDED, SOURCE_ADDED,
} }
@@ -243,10 +253,13 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
existingPreference, AudioStreamState.ADD_SOURCE_WAIT_FOR_RESPONSE); existingPreference, AudioStreamState.ADD_SOURCE_WAIT_FOR_RESPONSE);
} else { } else {
// A preference with source founded existed either because it's already // A preference with source founded existed either because it's already
// connected (SOURCE_ADDED). Any other reason is unexpected. We update the // connected (SOURCE_ADDED) or present (SOURCE_PRESENT). Any other reason
// preference with this source and won't change it's state. // is unexpected. We update the preference with this source and won't
// change it's state.
existingPreference.setAudioStreamMetadata(source); existingPreference.setAudioStreamMetadata(source);
if (fromState != AudioStreamState.SOURCE_ADDED) { if (fromState != AudioStreamState.SOURCE_ADDED
&& (!audioSharingHysteresisModeFix()
|| fromState != AudioStreamState.SOURCE_PRESENT)) {
Log.w( Log.w(
TAG, TAG,
"handleSourceFound(): unexpected state : " "handleSourceFound(): unexpected state : "
@@ -346,10 +359,14 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
for (var entry : mBroadcastIdToPreferenceMap.entrySet()) { for (var entry : mBroadcastIdToPreferenceMap.entrySet()) {
var preference = entry.getValue(); var preference = entry.getValue();
// Look for preference has SOURCE_ADDED state, re-check if they are still connected. If // Look for preference has SOURCE_ADDED or SOURCE_PRESENT state, re-check if they are
// still connected. If
// not, means the source is removed from the sink, we move back the preference to SYNCED // not, means the source is removed from the sink, we move back the preference to SYNCED
// state. // state.
if (preference.getAudioStreamState() == AudioStreamState.SOURCE_ADDED if ((preference.getAudioStreamState() == AudioStreamState.SOURCE_ADDED
|| (audioSharingHysteresisModeFix()
&& preference.getAudioStreamState()
== AudioStreamState.SOURCE_PRESENT))
&& mAudioStreamsHelper.getAllConnectedSources().stream() && mAudioStreamsHelper.getAllConnectedSources().stream()
.noneMatch( .noneMatch(
connected -> connected ->
@@ -383,6 +400,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
if (!AudioStreamsHelper.isConnected(receiveState)) { if (!AudioStreamsHelper.isConnected(receiveState)) {
return; return;
} }
var broadcastIdConnected = receiveState.getBroadcastId(); var broadcastIdConnected = receiveState.getBroadcastId();
if (mSourceFromQrCode != null && mSourceFromQrCode.getBroadcastId() == UNSET_BROADCAST_ID) { if (mSourceFromQrCode != null && mSourceFromQrCode.getBroadcastId() == UNSET_BROADCAST_ID) {
// mSourceFromQrCode could have no broadcast Id, we fill in the broadcast Id from the // mSourceFromQrCode could have no broadcast Id, we fill in the broadcast Id from the
@@ -455,6 +473,58 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
}); });
} }
// Find preference by receiveState and decide next state.
// Expect one preference existed, move to SOURCE_PRESENT
void handleSourcePresent(BluetoothLeBroadcastReceiveState receiveState) {
if (DEBUG) {
Log.d(TAG, "handleSourcePresent()");
}
if (!AudioStreamsHelper.hasSourcePresent(receiveState)) {
return;
}
var broadcastIdConnected = receiveState.getBroadcastId();
if (mSourceFromQrCode != null && mSourceFromQrCode.getBroadcastId() == UNSET_BROADCAST_ID) {
// mSourceFromQrCode could have no broadcast Id, we fill in the broadcast Id from the
// connected source receiveState.
if (DEBUG) {
Log.d(
TAG,
"handleSourcePresent() : processing mSourceFromQrCode with broadcastId"
+ " unset");
}
boolean updated =
maybeUpdateId(
AudioStreamsHelper.getBroadcastName(receiveState),
receiveState.getBroadcastId());
if (updated && mBroadcastIdToPreferenceMap.containsKey(UNSET_BROADCAST_ID)) {
var preference = mBroadcastIdToPreferenceMap.remove(UNSET_BROADCAST_ID);
mBroadcastIdToPreferenceMap.put(receiveState.getBroadcastId(), preference);
}
}
mBroadcastIdToPreferenceMap.compute(
broadcastIdConnected,
(k, existingPreference) -> {
if (existingPreference == null) {
// No existing preference for this source even if it's already connected,
// add one and set initial state to SOURCE_PRESENT. This could happen
// because
// we retrieves the connected source during onStart() from
// AudioStreamsHelper#getAllPresentSources() even before the source is
// founded by scanning.
return addNewPreference(receiveState, AudioStreamState.SOURCE_PRESENT);
}
if (existingPreference.getAudioStreamState() == AudioStreamState.WAIT_FOR_SYNC
&& existingPreference.getAudioStreamBroadcastId() == UNSET_BROADCAST_ID
&& mSourceFromQrCode != null) {
existingPreference.setAudioStreamMetadata(mSourceFromQrCode);
}
moveToState(existingPreference, AudioStreamState.SOURCE_PRESENT);
return existingPreference;
});
}
// Find preference by metadata and decide next state. // Find preference by metadata and decide next state.
// Expect one preference existed, move to ADD_SOURCE_WAIT_FOR_RESPONSE // Expect one preference existed, move to ADD_SOURCE_WAIT_FOR_RESPONSE
void handleSourceAddRequest( void handleSourceAddRequest(
@@ -530,9 +600,23 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
// Handle QR code scan, display currently connected streams then start scanning // Handle QR code scan, display currently connected streams then start scanning
// sequentially // sequentially
handleSourceFromQrCodeIfExists(); handleSourceFromQrCodeIfExists();
mAudioStreamsHelper if (audioSharingHysteresisModeFix()) {
.getAllConnectedSources() // With hysteresis mode, we prioritize showing connected sources first.
.forEach(this::handleSourceConnected); // If no connected sources are found, we then show present sources.
List<BluetoothLeBroadcastReceiveState> sources =
mAudioStreamsHelper.getAllConnectedSources();
if (!sources.isEmpty()) {
sources.forEach(this::handleSourceConnected);
} else {
mAudioStreamsHelper
.getAllPresentSources()
.forEach(this::handleSourcePresent);
}
} else {
mAudioStreamsHelper
.getAllConnectedSources()
.forEach(this::handleSourceConnected);
}
mLeBroadcastAssistant.startSearchingForSources(emptyList()); mLeBroadcastAssistant.startSearchingForSources(emptyList());
mMediaControlHelper.start(); mMediaControlHelper.start();
}); });
@@ -581,6 +665,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro
AddSourceWaitForResponseState.getInstance(); AddSourceWaitForResponseState.getInstance();
case ADD_SOURCE_BAD_CODE -> AddSourceBadCodeState.getInstance(); case ADD_SOURCE_BAD_CODE -> AddSourceBadCodeState.getInstance();
case ADD_SOURCE_FAILED -> AddSourceFailedState.getInstance(); case ADD_SOURCE_FAILED -> AddSourceFailedState.getInstance();
case SOURCE_PRESENT -> SourcePresentState.getInstance();
case SOURCE_ADDED -> SourceAddedState.getInstance(); case SOURCE_ADDED -> SourceAddedState.getInstance();
default -> throw new IllegalArgumentException("Unsupported state: " + state); default -> throw new IllegalArgumentException("Unsupported state: " + state);
}; };

View File

@@ -0,0 +1,87 @@
/*
* 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.audiostreams;
import android.app.settings.SettingsEnums;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import com.android.settings.R;
import com.android.settings.core.SubSettingLauncher;
import com.android.settings.dashboard.DashboardFragment;
class SourcePresentState extends AudioStreamStateHandler {
@VisibleForTesting
static final int AUDIO_STREAM_SOURCE_PRESENT_STATE_SUMMARY = R.string.audio_streams_present_now;
@Nullable private static SourcePresentState sInstance = null;
SourcePresentState() {}
static SourcePresentState getInstance() {
if (sInstance == null) {
sInstance = new SourcePresentState();
}
return sInstance;
}
@Override
void performAction(
AudioStreamPreference preference,
AudioStreamsProgressCategoryController controller,
AudioStreamsHelper helper) {
// nothing to do
}
@Override
int getSummary() {
return AUDIO_STREAM_SOURCE_PRESENT_STATE_SUMMARY;
}
@Override
Preference.OnPreferenceClickListener getOnClickListener(
AudioStreamsProgressCategoryController controller) {
return preference -> {
var p = (AudioStreamPreference) preference;
Bundle broadcast = new Bundle();
broadcast.putString(
AudioStreamDetailsFragment.BROADCAST_NAME_ARG, (String) p.getTitle());
broadcast.putInt(
AudioStreamDetailsFragment.BROADCAST_ID_ARG, p.getAudioStreamBroadcastId());
new SubSettingLauncher(p.getContext())
.setTitleRes(R.string.audio_streams_detail_page_title)
.setDestination(AudioStreamDetailsFragment.class.getName())
.setSourceMetricsCategory(
!(controller.getFragment() instanceof DashboardFragment)
? SettingsEnums.PAGE_UNKNOWN
: ((DashboardFragment) controller.getFragment())
.getMetricsCategory())
.setArguments(broadcast)
.launch();
return true;
};
}
@Override
AudioStreamsProgressCategoryController.AudioStreamState getStateEnum() {
return AudioStreamsProgressCategoryController.AudioStreamState.SOURCE_PRESENT;
}
}

View File

@@ -37,7 +37,6 @@ import com.android.settingslib.core.instrumentation.Instrumentable;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import com.android.settingslib.core.instrumentation.SettingsJankMonitor; import com.android.settingslib.core.instrumentation.SettingsJankMonitor;
import com.android.settingslib.core.instrumentation.VisibilityLoggerMixin; import com.android.settingslib.core.instrumentation.VisibilityLoggerMixin;
import com.android.settingslib.core.lifecycle.ObservablePreferenceFragment;
/** /**
* Instrumented fragment that logs visibility state. * Instrumented fragment that logs visibility state.

View File

@@ -0,0 +1,137 @@
/*
* 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.core;
import static androidx.lifecycle.Lifecycle.Event.ON_CREATE;
import static androidx.lifecycle.Lifecycle.Event.ON_DESTROY;
import static androidx.lifecycle.Lifecycle.Event.ON_PAUSE;
import static androidx.lifecycle.Lifecycle.Event.ON_RESUME;
import static androidx.lifecycle.Lifecycle.Event.ON_START;
import static androidx.lifecycle.Lifecycle.Event.ON_STOP;
import android.annotation.CallSuper;
import android.content.Context;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import androidx.lifecycle.LifecycleOwner;
import androidx.preference.PreferenceScreen;
import com.android.settingslib.core.lifecycle.Lifecycle;
import com.android.settingslib.preference.PreferenceFragment;
/**
* Preference fragment that has hooks to observe fragment lifecycle events.
*/
public abstract class ObservablePreferenceFragment extends PreferenceFragment
implements LifecycleOwner {
private final Lifecycle mLifecycle = new Lifecycle(this);
public Lifecycle getSettingsLifecycle() {
return mLifecycle;
}
@CallSuper
@Override
public void onAttach(Context context) {
super.onAttach(context);
mLifecycle.onAttach(context);
}
@CallSuper
@Override
public void onCreate(Bundle savedInstanceState) {
mLifecycle.onCreate(savedInstanceState);
mLifecycle.handleLifecycleEvent(ON_CREATE);
super.onCreate(savedInstanceState);
}
@Override
public void setPreferenceScreen(PreferenceScreen preferenceScreen) {
mLifecycle.setPreferenceScreen(preferenceScreen);
super.setPreferenceScreen(preferenceScreen);
}
@CallSuper
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
mLifecycle.onSaveInstanceState(outState);
}
@CallSuper
@Override
public void onStart() {
mLifecycle.handleLifecycleEvent(ON_START);
super.onStart();
}
@CallSuper
@Override
public void onResume() {
mLifecycle.handleLifecycleEvent(ON_RESUME);
super.onResume();
}
@CallSuper
@Override
public void onPause() {
mLifecycle.handleLifecycleEvent(ON_PAUSE);
super.onPause();
}
@CallSuper
@Override
public void onStop() {
mLifecycle.handleLifecycleEvent(ON_STOP);
super.onStop();
}
@CallSuper
@Override
public void onDestroy() {
mLifecycle.handleLifecycleEvent(ON_DESTROY);
super.onDestroy();
}
@CallSuper
@Override
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
mLifecycle.onCreateOptionsMenu(menu, inflater);
super.onCreateOptionsMenu(menu, inflater);
}
@CallSuper
@Override
public void onPrepareOptionsMenu(final Menu menu) {
mLifecycle.onPrepareOptionsMenu(menu);
super.onPrepareOptionsMenu(menu);
}
@CallSuper
@Override
public boolean onOptionsItemSelected(final MenuItem menuItem) {
boolean lifecycleHandled = mLifecycle.onOptionsItemSelected(menuItem);
if (!lifecycleHandled) {
return super.onOptionsItemSelected(menuItem);
}
return lifecycleHandled;
}
}

View File

@@ -27,13 +27,13 @@ import com.android.settings.R
import com.android.settings.SettingsPreferenceFragment import com.android.settings.SettingsPreferenceFragment
import com.android.settings.dashboard.DashboardFragment import com.android.settings.dashboard.DashboardFragment
import com.android.settings.flags.Flags import com.android.settings.flags.Flags
import com.android.settings.network.telephony.SimRepository
import com.android.settings.network.telephony.euicc.EuiccRepository import com.android.settings.network.telephony.euicc.EuiccRepository
import com.android.settings.search.BaseSearchIndexProvider import com.android.settings.search.BaseSearchIndexProvider
import com.android.settings.spa.SpaActivity.Companion.startSpaActivity import com.android.settings.spa.SpaActivity.Companion.startSpaActivity
import com.android.settings.spa.network.NetworkCellularGroupProvider import com.android.settings.spa.network.NetworkCellularGroupProvider
import com.android.settingslib.search.SearchIndexable import com.android.settingslib.search.SearchIndexable
import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle
import com.android.settingslib.spaprivileged.framework.common.userManager
import com.android.settingslib.spaprivileged.settingsprovider.settingsGlobalBooleanFlow import com.android.settingslib.spaprivileged.settingsprovider.settingsGlobalBooleanFlow
@SearchIndexable(forTarget = SearchIndexable.ALL and SearchIndexable.ARC.inv()) @SearchIndexable(forTarget = SearchIndexable.ALL and SearchIndexable.ARC.inv())
@@ -85,10 +85,11 @@ class MobileNetworkListFragment : DashboardFragment() {
val SEARCH_INDEX_DATA_PROVIDER = SearchIndexProvider() val SEARCH_INDEX_DATA_PROVIDER = SearchIndexProvider()
@VisibleForTesting @VisibleForTesting
class SearchIndexProvider : BaseSearchIndexProvider(R.xml.network_provider_sims_list) { class SearchIndexProvider(
private val simRepositoryFactory: (Context) -> SimRepository = ::SimRepository
) : BaseSearchIndexProvider(R.xml.network_provider_sims_list) {
public override fun isPageSearchEnabled(context: Context): Boolean = public override fun isPageSearchEnabled(context: Context): Boolean =
SubscriptionUtil.isSimHardwareVisible(context) && simRepositoryFactory(context).showMobileNetworkPage()
context.userManager.isAdminUser
} }
} }
} }

View File

@@ -1,162 +0,0 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.network;
import static android.os.UserHandle.myUserId;
import static android.os.UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS;
import static com.android.settings.Utils.SETTINGS_PACKAGE_NAME;
import static androidx.lifecycle.Lifecycle.Event;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.UserManager;
import android.provider.Settings;
import android.telephony.PhoneStateListener;
import android.telephony.ServiceState;
import android.telephony.TelephonyCallback;
import android.telephony.TelephonyManager;
import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.OnLifecycleEvent;
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
import com.android.settings.core.PreferenceControllerMixin;
import com.android.settings.network.telephony.MobileNetworkUtils;
import com.android.settingslib.RestrictedLockUtilsInternal;
import com.android.settingslib.RestrictedPreference;
import com.android.settingslib.Utils;
import com.android.settingslib.core.AbstractPreferenceController;
public class MobileNetworkPreferenceController extends AbstractPreferenceController
implements PreferenceControllerMixin, LifecycleObserver {
@VisibleForTesting
static final String KEY_MOBILE_NETWORK_SETTINGS = "mobile_network_settings";
private final boolean mIsSecondaryUser;
private final TelephonyManager mTelephonyManager;
private final UserManager mUserManager;
private Preference mPreference;
@VisibleForTesting
MobileNetworkTelephonyCallback mTelephonyCallback;
private BroadcastReceiver mAirplanModeChangedReceiver;
public MobileNetworkPreferenceController(Context context) {
super(context);
mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
mIsSecondaryUser = !mUserManager.isAdminUser();
mAirplanModeChangedReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
updateState(mPreference);
}
};
}
@Override
public boolean isAvailable() {
return !isUserRestricted() && !Utils.isWifiOnly(mContext);
}
public boolean isUserRestricted() {
return mIsSecondaryUser ||
RestrictedLockUtilsInternal.hasBaseUserRestriction(
mContext,
DISALLOW_CONFIG_MOBILE_NETWORKS,
myUserId());
}
@Override
public void displayPreference(PreferenceScreen screen) {
super.displayPreference(screen);
mPreference = screen.findPreference(getPreferenceKey());
}
@Override
public String getPreferenceKey() {
return KEY_MOBILE_NETWORK_SETTINGS;
}
class MobileNetworkTelephonyCallback extends TelephonyCallback implements
TelephonyCallback.ServiceStateListener {
@Override
public void onServiceStateChanged(ServiceState serviceState) {
updateState(mPreference);
}
}
@OnLifecycleEvent(Event.ON_START)
public void onStart() {
if (isAvailable()) {
if (mTelephonyCallback == null) {
mTelephonyCallback = new MobileNetworkTelephonyCallback();
}
mTelephonyManager.registerTelephonyCallback(
mContext.getMainExecutor(), mTelephonyCallback);
}
if (mAirplanModeChangedReceiver != null) {
mContext.registerReceiver(mAirplanModeChangedReceiver,
new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED));
}
}
@OnLifecycleEvent(Event.ON_STOP)
public void onStop() {
if (mTelephonyCallback != null) {
mTelephonyManager.unregisterTelephonyCallback(mTelephonyCallback);
}
if (mAirplanModeChangedReceiver != null) {
mContext.unregisterReceiver(mAirplanModeChangedReceiver);
}
}
@Override
public void updateState(Preference preference) {
super.updateState(preference);
if (preference instanceof RestrictedPreference &&
((RestrictedPreference) preference).isDisabledByAdmin()) {
return;
}
preference.setEnabled(Settings.Global.getInt(
mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0) == 0);
}
@Override
public boolean handlePreferenceTreeClick(Preference preference) {
if (KEY_MOBILE_NETWORK_SETTINGS.equals(preference.getKey())) {
final Intent intent = new Intent(Settings.ACTION_NETWORK_OPERATOR_SETTINGS);
intent.setPackage(SETTINGS_PACKAGE_NAME);
mContext.startActivity(intent);
return true;
}
return false;
}
@Override
public CharSequence getSummary() {
return MobileNetworkUtils.getCurrentCarrierNameForDisplay(mContext);
}
}

View File

@@ -21,7 +21,6 @@ import static androidx.lifecycle.Lifecycle.Event.ON_RESUME;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.UserManager;
import android.telephony.SubscriptionManager; import android.telephony.SubscriptionManager;
import android.telephony.euicc.EuiccManager; import android.telephony.euicc.EuiccManager;
@@ -35,10 +34,10 @@ import androidx.preference.PreferenceScreen;
import com.android.settings.R; import com.android.settings.R;
import com.android.settings.core.PreferenceControllerMixin; import com.android.settings.core.PreferenceControllerMixin;
import com.android.settings.dashboard.DashboardFragment; import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.network.telephony.SimRepository;
import com.android.settings.network.telephony.euicc.EuiccRepository; import com.android.settings.network.telephony.euicc.EuiccRepository;
import com.android.settings.overlay.FeatureFactory; import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.RestrictedPreference; import com.android.settingslib.RestrictedPreference;
import com.android.settingslib.Utils;
import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.AbstractPreferenceController;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import com.android.settingslib.mobile.dataservice.MobileNetworkInfoEntity; import com.android.settingslib.mobile.dataservice.MobileNetworkInfoEntity;
@@ -56,7 +55,6 @@ public class MobileNetworkSummaryController extends AbstractPreferenceController
private static final String KEY = "mobile_network_list"; private static final String KEY = "mobile_network_list";
private final MetricsFeatureProvider mMetricsFeatureProvider; private final MetricsFeatureProvider mMetricsFeatureProvider;
private UserManager mUserManager;
private RestrictedPreference mPreference; private RestrictedPreference mPreference;
private MobileNetworkRepository mMobileNetworkRepository; private MobileNetworkRepository mMobileNetworkRepository;
@@ -85,7 +83,6 @@ public class MobileNetworkSummaryController extends AbstractPreferenceController
LifecycleOwner lifecycleOwner) { LifecycleOwner lifecycleOwner) {
super(context); super(context);
mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider(); mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider();
mUserManager = context.getSystemService(UserManager.class);
mLifecycleOwner = lifecycleOwner; mLifecycleOwner = lifecycleOwner;
mMobileNetworkRepository = MobileNetworkRepository.getInstance(context); mMobileNetworkRepository = MobileNetworkRepository.getInstance(context);
mIsAirplaneModeOn = mMobileNetworkRepository.isAirplaneModeOn(); mIsAirplaneModeOn = mMobileNetworkRepository.isAirplaneModeOn();
@@ -185,8 +182,7 @@ public class MobileNetworkSummaryController extends AbstractPreferenceController
@Override @Override
public boolean isAvailable() { public boolean isAvailable() {
return SubscriptionUtil.isSimHardwareVisible(mContext) && return new SimRepository(mContext).showMobileNetworkPage();
!Utils.isWifiOnly(mContext) && mUserManager.isAdminUser();
} }
@Override @Override

View File

@@ -1,55 +0,0 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.network;
import android.content.Context;
import android.text.BidiFormatter;
import com.android.settings.R;
import com.android.settings.Utils;
import com.android.settings.activityembedding.ActivityEmbeddingUtils;
import com.android.settings.core.BasePreferenceController;
public class TopLevelNetworkEntryPreferenceController extends BasePreferenceController {
private final MobileNetworkPreferenceController mMobileNetworkPreferenceController;
public TopLevelNetworkEntryPreferenceController(Context context, String preferenceKey) {
super(context, preferenceKey);
mMobileNetworkPreferenceController = new MobileNetworkPreferenceController(mContext);
}
@Override
public int getAvailabilityStatus() {
// TODO(b/281597506): Update the ActivityEmbeddingUtils.isEmbeddingActivityEnabled
// while getting the new API.
return (Utils.isDemoUser(mContext)
&& !ActivityEmbeddingUtils.isEmbeddingActivityEnabled(mContext))
? UNSUPPORTED_ON_DEVICE : AVAILABLE;
}
@Override
public CharSequence getSummary() {
if (mMobileNetworkPreferenceController.isAvailable()) {
return BidiFormatter.getInstance()
.unicodeWrap(mContext.getString(R.string.network_dashboard_summary_mobile));
} else {
return BidiFormatter.getInstance()
.unicodeWrap(mContext.getString(R.string.network_dashboard_summary_no_mobile));
}
}
}

View File

@@ -0,0 +1,58 @@
/*
* 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.text.BidiFormatter
import com.android.settings.R
import com.android.settings.Utils
import com.android.settings.activityembedding.ActivityEmbeddingUtils
import com.android.settings.core.BasePreferenceController
import com.android.settings.network.telephony.SimRepository
class TopLevelNetworkEntryPreferenceController
@JvmOverloads
constructor(
context: Context,
preferenceKey: String,
private val simRepository: SimRepository = SimRepository(context),
private val isDemoUser: () -> Boolean = { Utils.isDemoUser(context) },
private val isEmbeddingActivityEnabled: () -> Boolean = {
ActivityEmbeddingUtils.isEmbeddingActivityEnabled(context)
},
) : BasePreferenceController(context, preferenceKey) {
override fun getAvailabilityStatus(): Int {
// TODO(b/281597506): Update the ActivityEmbeddingUtils.isEmbeddingActivityEnabled
// while getting the new API.
return if (isDemoUser() && !isEmbeddingActivityEnabled()) {
UNSUPPORTED_ON_DEVICE
} else {
AVAILABLE
}
}
override fun getSummary(): CharSequence {
val summaryResId =
if (simRepository.showMobileNetworkPage()) {
R.string.network_dashboard_summary_mobile
} else {
R.string.network_dashboard_summary_no_mobile
}
return BidiFormatter.getInstance().unicodeWrap(mContext.getString(summaryResId))
}
}

View File

@@ -0,0 +1,30 @@
/*
* 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.telephony
import android.content.Context
import android.content.pm.PackageManager
import com.android.settingslib.spaprivileged.framework.common.userManager
class SimRepository(context: Context) {
private val packageManager = context.packageManager
private val userManager = context.userManager
/** Gets whether we show mobile network settings page to the current user. */
fun showMobileNetworkPage(): Boolean =
packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY) && userManager.isAdminUser
}

View File

@@ -76,7 +76,7 @@ public class ZenModeBehaviorFooterPreferenceController extends AbstractZenModePr
// DND turned on by an automatic rule with deprecated zen mode // DND turned on by an automatic rule with deprecated zen mode
for (ZenModeConfig.ZenRule automaticRule : config.automaticRules.values()) { for (ZenModeConfig.ZenRule automaticRule : config.automaticRules.values()) {
if (automaticRule.isAutomaticActive() && isDeprecatedZenMode( if (automaticRule.isActive() && isDeprecatedZenMode(
automaticRule.zenMode)) { automaticRule.zenMode)) {
ComponentName component = automaticRule.component; ComponentName component = automaticRule.component;
if (component != null) { if (component != null) {

View File

@@ -153,7 +153,7 @@ public class ZenModeSettingsFooterPreferenceController extends AbstractZenModePr
// DND turned on by an automatic rule // DND turned on by an automatic rule
for (ZenModeConfig.ZenRule automaticRule : config.automaticRules.values()) { for (ZenModeConfig.ZenRule automaticRule : config.automaticRules.values()) {
if (automaticRule.isAutomaticActive()) { if (automaticRule.isActive()) {
// set footer if 3rd party rule // set footer if 3rd party rule
if (!mZenModeConfigWrapper.isTimeRule(automaticRule.conditionId)) { if (!mZenModeConfigWrapper.isTimeRule(automaticRule.conditionId)) {
return mContext.getString(R.string.zen_mode_settings_dnd_automatic_rule, return mContext.getString(R.string.zen_mode_settings_dnd_automatic_rule,
@@ -180,7 +180,7 @@ public class ZenModeSettingsFooterPreferenceController extends AbstractZenModePr
} }
for (ZenModeConfig.ZenRule automaticRule : config.automaticRules.values()) { for (ZenModeConfig.ZenRule automaticRule : config.automaticRules.values()) {
if (automaticRule.isAutomaticActive()) { if (automaticRule.isActive()) {
zenRules.add(automaticRule); zenRules.add(automaticRule);
} }
} }

View File

@@ -24,6 +24,7 @@ import androidx.preference.Preference;
import androidx.preference.PreferenceScreen; import androidx.preference.PreferenceScreen;
import com.android.internal.widget.LockPatternUtils; import com.android.internal.widget.LockPatternUtils;
import com.android.settings.core.ObservablePreferenceFragment;
import com.android.settings.core.PreferenceControllerMixin; import com.android.settings.core.PreferenceControllerMixin;
import com.android.settings.users.OwnerInfoSettings; import com.android.settings.users.OwnerInfoSettings;
import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
@@ -31,7 +32,6 @@ import com.android.settingslib.RestrictedLockUtilsInternal;
import com.android.settingslib.RestrictedPreference; import com.android.settingslib.RestrictedPreference;
import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.AbstractPreferenceController;
import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.LifecycleObserver;
import com.android.settingslib.core.lifecycle.ObservablePreferenceFragment;
import com.android.settingslib.core.lifecycle.events.OnResume; import com.android.settingslib.core.lifecycle.events.OnResume;
public class OwnerInfoPreferenceController extends AbstractPreferenceController public class OwnerInfoPreferenceController extends AbstractPreferenceController

View File

@@ -24,6 +24,7 @@ import static com.android.settings.slices.SettingsSliceProvider.EXTRA_SLICE_KEY;
import android.annotation.ColorInt; import android.annotation.ColorInt;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
@@ -47,6 +48,7 @@ import com.android.settings.R;
import com.android.settings.SettingsActivity; import com.android.settings.SettingsActivity;
import com.android.settings.SubSettings; import com.android.settings.SubSettings;
import com.android.settings.Utils; import com.android.settings.Utils;
import com.android.settings.accessibility.AccessibilitySlicePreferenceController;
import com.android.settings.core.BasePreferenceController; import com.android.settings.core.BasePreferenceController;
import com.android.settings.core.SliderPreferenceController; import com.android.settings.core.SliderPreferenceController;
import com.android.settings.core.SubSettingLauncher; import com.android.settings.core.SubSettingLauncher;
@@ -448,7 +450,17 @@ public class SliceBuilderUtils {
iconResource = R.drawable.ic_settings_accent; iconResource = R.drawable.ic_settings_accent;
} }
try { try {
return IconCompat.createWithResource(context, iconResource); // LINT.IfChange(createA11yIcon)
if (AccessibilitySlicePreferenceController.class.getName().equals(
data.getPreferenceController())) {
ComponentName serviceComponent = ComponentName.unflattenFromString(data.getKey());
return IconCompat.createWithResource(
context.createPackageContext(serviceComponent.getPackageName(), 0),
iconResource);
// LINT.ThenChange()
} else {
return IconCompat.createWithResource(context, iconResource);
}
} catch (Exception e) { } catch (Exception e) {
Log.w(TAG, "Falling back to settings icon because there is an error getting slice icon " Log.w(TAG, "Falling back to settings icon because there is an error getting slice icon "
+ data.getUri(), e); + data.getUri(), e);

View File

@@ -274,6 +274,12 @@ class SliceDataConverter {
final ServiceInfo serviceInfo = resolveInfo.serviceInfo; final ServiceInfo serviceInfo = resolveInfo.serviceInfo;
final String packageName = serviceInfo.packageName; final String packageName = serviceInfo.packageName;
final ComponentName componentName = new ComponentName(packageName, serviceInfo.name); final ComponentName componentName = new ComponentName(packageName, serviceInfo.name);
// If we change the flattenedName that is used to be set as a key of the Slice, we
// need to make corresponding change in SliceBuilderUtils, since we rely on the
// the A11y Service Slice's key to be a ComponentName to get the correct package name
// to grab the icon belongs to that package.
// LINT.IfChange
final String flattenedName = componentName.flattenToString(); final String flattenedName = componentName.flattenToString();
if (!a11yServiceNames.contains(flattenedName)) { if (!a11yServiceNames.contains(flattenedName)) {
@@ -287,6 +293,7 @@ class SliceDataConverter {
} }
sliceDataBuilder.setKey(flattenedName) sliceDataBuilder.setKey(flattenedName)
// LINT.ThenChange(SliceBuilderUtils.java:createA11yIcon)
.setTitle(title) .setTitle(title)
.setUri(new Uri.Builder() .setUri(new Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT) .scheme(ContentResolver.SCHEME_CONTENT)

View File

@@ -0,0 +1,45 @@
/*
* 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.sound;
import android.app.Flags;
import android.content.Context;
import androidx.preference.Preference;
import com.android.settings.R;
import com.android.settings.core.BasePreferenceController;
public class TopLevelSoundPreferenceController extends BasePreferenceController {
public TopLevelSoundPreferenceController(Context context, String preferenceKey) {
super(context, preferenceKey);
}
@Override
public void updateState(Preference preference) {
super.updateState(preference);
preference.setSummary(Flags.modesApi() && Flags.modesUi()
? R.string.sound_dashboard_summary
: R.string.sound_dashboard_summary_with_dnd);
}
@Override
public int getAvailabilityStatus() {
return AVAILABLE;
}
}

View File

@@ -48,9 +48,9 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import com.android.settings.R import com.android.settings.R
import com.android.settings.flags.Flags import com.android.settings.flags.Flags
import com.android.settings.network.SubscriptionInfoListViewModel import com.android.settings.network.SubscriptionInfoListViewModel
import com.android.settings.network.SubscriptionUtil
import com.android.settings.network.telephony.DataSubscriptionRepository import com.android.settings.network.telephony.DataSubscriptionRepository
import com.android.settings.network.telephony.MobileDataRepository import com.android.settings.network.telephony.MobileDataRepository
import com.android.settings.network.telephony.SimRepository
import com.android.settings.network.telephony.requireSubscriptionManager import com.android.settings.network.telephony.requireSubscriptionManager
import com.android.settings.spa.network.PrimarySimRepository.PrimarySimInfo import com.android.settings.spa.network.PrimarySimRepository.PrimarySimInfo
import com.android.settings.spa.search.SearchablePage import com.android.settings.spa.search.SearchablePage
@@ -66,7 +66,6 @@ import com.android.settingslib.spa.widget.preference.PreferenceModel
import com.android.settingslib.spa.widget.scaffold.RegularScaffold import com.android.settingslib.spa.widget.scaffold.RegularScaffold
import com.android.settingslib.spa.widget.ui.Category import com.android.settingslib.spa.widget.ui.Category
import com.android.settingslib.spaprivileged.framework.common.broadcastReceiverFlow import com.android.settingslib.spaprivileged.framework.common.broadcastReceiverFlow
import com.android.settingslib.spaprivileged.framework.common.userManager
import com.android.settingslib.spaprivileged.settingsprovider.settingsGlobalBooleanFlow import com.android.settingslib.spaprivileged.settingsprovider.settingsGlobalBooleanFlow
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -213,10 +212,7 @@ open class NetworkCellularGroupProvider : SettingsPageProvider, SearchablePage {
const val fileName = "NetworkCellularGroupProvider" const val fileName = "NetworkCellularGroupProvider"
private fun isPageSearchable(context: Context) = private fun isPageSearchable(context: Context) =
Flags.isDualSimOnboardingEnabled() && Flags.isDualSimOnboardingEnabled() && SimRepository(context).showMobileNetworkPage()
SubscriptionUtil.isSimHardwareVisible(context) &&
!com.android.settingslib.Utils.isWifiOnly(context) &&
context.userManager.isAdminUser
} }
} }

View File

@@ -42,7 +42,6 @@ import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule; import android.platform.test.flag.junit.SetFlagsRule;
import android.provider.Settings; import android.provider.Settings;
import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.Flags;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
@@ -50,6 +49,7 @@ import androidx.test.core.app.ApplicationProvider;
import com.android.internal.accessibility.util.AccessibilityUtils; import com.android.internal.accessibility.util.AccessibilityUtils;
import com.android.settings.R; import com.android.settings.R;
import com.android.settings.SettingsActivity; import com.android.settings.SettingsActivity;
import com.android.settings.testutils.FakeFeatureFactory;
import com.android.settings.testutils.XmlTestUtils; import com.android.settings.testutils.XmlTestUtils;
import com.android.settings.testutils.shadow.ShadowAccessibilityManager; import com.android.settings.testutils.shadow.ShadowAccessibilityManager;
import com.android.settings.testutils.shadow.ShadowApplicationPackageManager; import com.android.settings.testutils.shadow.ShadowApplicationPackageManager;
@@ -78,6 +78,7 @@ import org.robolectric.android.controller.ActivityController;
import org.robolectric.annotation.Config; import org.robolectric.annotation.Config;
import org.robolectric.shadow.api.Shadow; import org.robolectric.shadow.api.Shadow;
import org.robolectric.shadows.ShadowContentResolver; import org.robolectric.shadows.ShadowContentResolver;
import org.robolectric.shadows.ShadowLooper;
import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException; import java.io.IOException;
@@ -155,6 +156,53 @@ public class AccessibilitySettingsTest {
assertThat(indexableRawList).isNull(); assertThat(indexableRawList).isNull();
} }
@DisableFlags(Flags.FLAG_FIX_A11Y_SETTINGS_SEARCH)
@Test
public void getDynamicRawDataToIndex_hasInstalledA11yFeatures_flagOff_returnEmpty() {
mShadowAccessibilityManager.setInstalledAccessibilityServiceList(
List.of(mServiceInfo));
mShadowAccessibilityManager.setInstalledAccessibilityShortcutListAsUser(
List.of(getMockAccessibilityShortcutInfo()));
assertThat(AccessibilitySettings.SEARCH_INDEX_DATA_PROVIDER.getDynamicRawDataToIndex(
mContext, /* enabled= */ true))
.isEmpty();
}
@EnableFlags(Flags.FLAG_FIX_A11Y_SETTINGS_SEARCH)
@Test
public void getDynamicRawDataToIndex_hasInstalledA11yFeatures_flagOn_returnRawDataForInstalledA11yFeatures() {
mShadowAccessibilityManager.setInstalledAccessibilityServiceList(
List.of(mServiceInfo));
mShadowAccessibilityManager.setInstalledAccessibilityShortcutListAsUser(
List.of(getMockAccessibilityShortcutInfo()));
final AccessibilitySearchFeatureProvider featureProvider =
FakeFeatureFactory.setupForTest().getAccessibilitySearchFeatureProvider();
final String synonyms = "fake keyword1, fake keyword2";
when(featureProvider.getSynonymsForComponent(mContext, ACTIVITY_COMPONENT_NAME))
.thenReturn("");
when(featureProvider.getSynonymsForComponent(mContext, SERVICE_COMPONENT_NAME))
.thenReturn(synonyms);
final List<SearchIndexableRaw> indexableRawDataList =
AccessibilitySettings.SEARCH_INDEX_DATA_PROVIDER.getDynamicRawDataToIndex(
mContext, /* enabled= */ true);
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
assertThat(indexableRawDataList).hasSize(2);
SearchIndexableRaw a11yActivityIndexableData = indexableRawDataList.get(0);
assertThat(a11yActivityIndexableData.key).isEqualTo(
ACTIVITY_COMPONENT_NAME.flattenToString());
assertThat(a11yActivityIndexableData.title).isEqualTo(DEFAULT_LABEL);
assertThat(a11yActivityIndexableData.keywords).isEmpty();
SearchIndexableRaw a11yServiceIndexableData = indexableRawDataList.get(1);
assertThat(a11yServiceIndexableData.key).isEqualTo(
SERVICE_COMPONENT_NAME.flattenToString());
assertThat(a11yServiceIndexableData.title).isEqualTo(DEFAULT_LABEL);
assertThat(a11yServiceIndexableData.keywords).isEqualTo(synonyms);
}
@Test @Test
public void getServiceSummary_serviceCrash_showsStopped() { public void getServiceSummary_serviceCrash_showsStopped() {
mServiceInfo.crashed = true; mServiceInfo.crashed = true;
@@ -328,7 +376,7 @@ public class AccessibilitySettingsTest {
} }
@Test @Test
@DisableFlags(Flags.FLAG_A11Y_QS_SHORTCUT) @DisableFlags(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT)
public void onCreate_flagDisabled_haveRegisterToSpecificUrisAndActions() { public void onCreate_flagDisabled_haveRegisterToSpecificUrisAndActions() {
setupFragment(); setupFragment();
@@ -341,7 +389,7 @@ public class AccessibilitySettingsTest {
} }
@Test @Test
@EnableFlags(Flags.FLAG_A11Y_QS_SHORTCUT) @EnableFlags(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT)
public void onCreate_flagEnabled_haveRegisterToSpecificUrisAndActions() { public void onCreate_flagEnabled_haveRegisterToSpecificUrisAndActions() {
setupFragment(); setupFragment();
@@ -415,7 +463,7 @@ public class AccessibilitySettingsTest {
} }
@Test @Test
@EnableFlags(com.android.settings.accessibility.Flags.FLAG_CHECK_PREBUNDLED_IS_PREINSTALLED) @EnableFlags(Flags.FLAG_CHECK_PREBUNDLED_IS_PREINSTALLED)
public void testNonPreinstalledApp_IncludedInDownloadedCategory() { public void testNonPreinstalledApp_IncludedInDownloadedCategory() {
mShadowAccessibilityManager.setInstalledAccessibilityServiceList( mShadowAccessibilityManager.setInstalledAccessibilityServiceList(
List.of(getMockAccessibilityServiceInfo( List.of(getMockAccessibilityServiceInfo(

View File

@@ -34,6 +34,7 @@ import android.util.AttributeSet;
import android.widget.SeekBar; import android.widget.SeekBar;
import com.android.settings.R; import com.android.settings.R;
import com.android.settings.Utils;
import com.android.settings.testutils.shadow.ShadowSystemSettings; import com.android.settings.testutils.shadow.ShadowSystemSettings;
import org.junit.Before; import org.junit.Before;
@@ -162,7 +163,8 @@ public class BalanceSeekBarTest {
mProxySeekBarListener.onProgressChanged(mSeekBar, progress, true); mProxySeekBarListener.onProgressChanged(mSeekBar, progress, true);
assertThat(mSeekBar.getStateDescription()).isEqualTo( assertThat(mSeekBar.getStateDescription()).isEqualTo(
mContext.getString(R.string.audio_seek_bar_state_left_first, 50, 50)); mContext.getString(R.string.audio_seek_bar_state_left_first,
Utils.formatPercentage(50), Utils.formatPercentage(50)));
} }
@Test @Test
@@ -177,7 +179,8 @@ public class BalanceSeekBarTest {
mProxySeekBarListener.onProgressChanged(mSeekBar, progress, true); mProxySeekBarListener.onProgressChanged(mSeekBar, progress, true);
assertThat(mSeekBar.getStateDescription()).isEqualTo( assertThat(mSeekBar.getStateDescription()).isEqualTo(
mContext.getString(R.string.audio_seek_bar_state_right_first, 50, 50)); mContext.getString(R.string.audio_seek_bar_state_right_first,
Utils.formatPercentage(50), Utils.formatPercentage(50)));
} }
@Test @Test
@@ -189,7 +192,8 @@ public class BalanceSeekBarTest {
mProxySeekBarListener.onProgressChanged(mSeekBar, progress, true); mProxySeekBarListener.onProgressChanged(mSeekBar, progress, true);
assertThat(mSeekBar.getStateDescription()).isEqualTo( assertThat(mSeekBar.getStateDescription()).isEqualTo(
mContext.getString(R.string.audio_seek_bar_state_left_first, 75, 25)); mContext.getString(R.string.audio_seek_bar_state_left_first,
Utils.formatPercentage(75), Utils.formatPercentage(25)));
} }
@Test @Test
@@ -201,7 +205,8 @@ public class BalanceSeekBarTest {
mProxySeekBarListener.onProgressChanged(mSeekBar, progress, true); mProxySeekBarListener.onProgressChanged(mSeekBar, progress, true);
assertThat(mSeekBar.getStateDescription()).isEqualTo( assertThat(mSeekBar.getStateDescription()).isEqualTo(
mContext.getString(R.string.audio_seek_bar_state_right_first, 75, 25)); mContext.getString(R.string.audio_seek_bar_state_right_first,
Utils.formatPercentage(75), Utils.formatPercentage(25)));
} }
// method to get the center from BalanceSeekBar for testing setMax(). // method to get the center from BalanceSeekBar for testing setMax().

View File

@@ -0,0 +1,94 @@
/*
* 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.biometrics.fingerprint
import android.app.Activity
import android.content.Intent
import com.android.settings.overlay.FeatureFactory
import com.android.settings.testutils.FakeFeatureFactory
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.`when`
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows
@RunWith(RobolectricTestRunner::class)
class FingerprintEnrollTest {
private lateinit var featureFactory: FeatureFactory
private companion object {
const val INTENT_KEY = "testKey"
const val INTENT_VALUE = "testValue"
val INTENT = Intent().apply {
putExtra(INTENT_KEY, INTENT_VALUE)
}
}
private val activityProvider = FingerprintEnrollActivityClassProvider()
@Before
fun setUp() {
featureFactory = FakeFeatureFactory.setupForTest()
`when`(featureFactory.fingerprintFeatureProvider.enrollActivityClassProvider)
.thenReturn(activityProvider)
}
private fun setupActivity(activityClass: Class<out FingerprintEnroll>): FingerprintEnroll {
return Robolectric.buildActivity(activityClass, INTENT).create().get()
}
@Test
fun testFinishAndLaunchDefaultActivity() {
// Run
val activity = setupActivity(FingerprintEnroll::class.java)
// Verify
verifyLaunchNextActivity(activity, activityProvider.default)
}
@Test
fun testFinishAndLaunchSetupActivity() {
// Run
val activity = setupActivity(FingerprintEnroll.SetupActivity::class.java)
// Verify
verifyLaunchNextActivity(activity, activityProvider.setup)
}
@Test
fun testFinishAndLaunchInternalActivity() {
// Run
val activity = setupActivity(FingerprintEnroll.InternalActivity::class.java)
// Verify
verifyLaunchNextActivity(activity, activityProvider.internal)
}
private fun verifyLaunchNextActivity(
currentActivityInstance : FingerprintEnroll,
nextActivityClass: Class<out Activity>
) {
assertThat(currentActivityInstance.isFinishing).isTrue()
val nextActivityIntent = Shadows.shadowOf(currentActivityInstance).nextStartedActivity
assertThat(nextActivityIntent.component!!.className).isEqualTo(nextActivityClass.name)
assertThat(nextActivityIntent.extras!!.size()).isEqualTo(1)
assertThat(nextActivityIntent.getStringExtra(INTENT_KEY)).isEqualTo(INTENT_VALUE)
}
}

View File

@@ -124,10 +124,11 @@ class DeviceDetailsFragmentFormatterTest {
listOf( listOf(
DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem( DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem(
DeviceSettingId.DEVICE_SETTING_ID_HEADER, DeviceSettingId.DEVICE_SETTING_ID_HEADER,
"bluetooth_device_header" highlighted = false,
preferenceKey = "bluetooth_device_header"
), ),
DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem( DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem(
DeviceSettingId.DEVICE_SETTING_ID_ACTION_BUTTONS, "action_buttons"), DeviceSettingId.DEVICE_SETTING_ID_ACTION_BUTTONS, highlighted = false, preferenceKey = "action_buttons"),
), ),
listOf(), listOf(),
null)) null))
@@ -157,7 +158,7 @@ class DeviceDetailsFragmentFormatterTest {
`when`(repository.getDeviceSettingsConfig(cachedDevice)) `when`(repository.getDeviceSettingsConfig(cachedDevice))
.thenReturn( .thenReturn(
DeviceSettingConfigModel( DeviceSettingConfigModel(
listOf(), listOf(), DeviceSettingConfigItemModel.AppProvidedItem(12345))) listOf(), listOf(), DeviceSettingConfigItemModel.AppProvidedItem(12345, false)))
val intent = Intent().apply { val intent = Intent().apply {
setAction(Intent.ACTION_VIEW) setAction(Intent.ACTION_VIEW)
setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
@@ -206,10 +207,10 @@ class DeviceDetailsFragmentFormatterTest {
listOf( listOf(
DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem( DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem(
DeviceSettingId.DEVICE_SETTING_ID_HEADER, DeviceSettingId.DEVICE_SETTING_ID_HEADER,
"bluetooth_device_header"), highlighted = false, preferenceKey = "bluetooth_device_header"),
DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem( DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem(
DeviceSettingId.DEVICE_SETTING_ID_KEYBOARD_SETTINGS, DeviceSettingId.DEVICE_SETTING_ID_KEYBOARD_SETTINGS,
"keyboard_settings"), highlighted = false, preferenceKey = "keyboard_settings"),
), ),
listOf(), listOf(),
null)) null))
@@ -230,12 +231,14 @@ class DeviceDetailsFragmentFormatterTest {
listOf( listOf(
DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem( DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem(
DeviceSettingId.DEVICE_SETTING_ID_HEADER, DeviceSettingId.DEVICE_SETTING_ID_HEADER,
"bluetooth_device_header"), highlighted = false,
preferenceKey = "bluetooth_device_header"),
DeviceSettingConfigItemModel.AppProvidedItem( DeviceSettingConfigItemModel.AppProvidedItem(
DeviceSettingId.DEVICE_SETTING_ID_ANC), DeviceSettingId.DEVICE_SETTING_ID_ANC, highlighted = false),
DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem( DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem(
DeviceSettingId.DEVICE_SETTING_ID_KEYBOARD_SETTINGS, DeviceSettingId.DEVICE_SETTING_ID_KEYBOARD_SETTINGS,
"keyboard_settings"), highlighted = false,
preferenceKey = "keyboard_settings"),
), ),
listOf(), listOf(),
null)) null))

View File

@@ -246,11 +246,11 @@ class BluetoothDeviceDetailsViewModelTest {
} }
private fun getLatestLayout(layout: DeviceSettingLayout): List<List<Int>> { private fun getLatestLayout(layout: DeviceSettingLayout): List<List<Int>> {
var latestLayout = MutableList(layout.rows.size) { emptyList<Int>() } val latestLayout = MutableList(layout.rows.size) { emptyList<Int>() }
for (i in layout.rows.indices) { for (i in layout.rows.indices) {
layout.rows[i] layout.rows[i]
.settingIds .columns
.onEach { latestLayout[i] = it } .onEach { latestLayout[i] = it.map { c -> c.settingId } }
.launchIn(testScope.backgroundScope) .launchIn(testScope.backgroundScope)
} }
@@ -278,15 +278,15 @@ class BluetoothDeviceDetailsViewModelTest {
DeviceSettingModel.ActionSwitchPreference(cachedDevice, settingId, "title") DeviceSettingModel.ActionSwitchPreference(cachedDevice, settingId, "title")
private fun buildRemoteSettingItem(settingId: Int) = private fun buildRemoteSettingItem(settingId: Int) =
DeviceSettingConfigItemModel.AppProvidedItem(settingId) DeviceSettingConfigItemModel.AppProvidedItem(settingId, false)
private companion object { private companion object {
val BUILTIN_SETTING_ITEM_1 = val BUILTIN_SETTING_ITEM_1 =
DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem( DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem(
DeviceSettingId.DEVICE_SETTING_ID_HEADER, "bluetooth_device_header") DeviceSettingId.DEVICE_SETTING_ID_HEADER, false, "bluetooth_device_header")
val BUILDIN_SETTING_ITEM_2 = val BUILDIN_SETTING_ITEM_2 =
DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem( DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem(
DeviceSettingId.DEVICE_SETTING_ID_ACTION_BUTTONS, "action_buttons") DeviceSettingId.DEVICE_SETTING_ID_ACTION_BUTTONS, false, "action_buttons")
val SETTING_ITEM_HELP = DeviceSettingConfigItemModel.AppProvidedItem(12345) val SETTING_ITEM_HELP = DeviceSettingConfigItemModel.AppProvidedItem(12345, false)
} }
} }

View File

@@ -16,6 +16,8 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams; package com.android.settings.connecteddevice.audiosharing.audiostreams;
import static com.android.settingslib.flags.Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
@@ -34,6 +36,7 @@ import android.bluetooth.BluetoothLeBroadcastAssistant;
import android.bluetooth.BluetoothLeBroadcastMetadata; import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.content.Context; import android.content.Context;
import android.platform.test.flag.junit.SetFlagsRule;
import android.view.View; import android.view.View;
import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LifecycleOwner;
@@ -72,8 +75,8 @@ import java.util.concurrent.Executor;
ShadowAudioStreamsHelper.class, ShadowAudioStreamsHelper.class,
}) })
public class AudioStreamButtonControllerTest { public class AudioStreamButtonControllerTest {
@Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
@Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
private static final String KEY = "audio_stream_button"; private static final String KEY = "audio_stream_button";
private static final int BROADCAST_ID = 1; private static final int BROADCAST_ID = 1;
private final Context mContext = ApplicationProvider.getApplicationContext(); private final Context mContext = ApplicationProvider.getApplicationContext();
@@ -83,6 +86,7 @@ public class AudioStreamButtonControllerTest {
@Mock private LocalBluetoothLeBroadcastAssistant mAssistant; @Mock private LocalBluetoothLeBroadcastAssistant mAssistant;
@Mock private AudioStreamsRepository mRepository; @Mock private AudioStreamsRepository mRepository;
@Mock private ActionButtonsPreference mPreference; @Mock private ActionButtonsPreference mPreference;
@Mock private BluetoothDevice mSourceDevice;
private Lifecycle mLifecycle; private Lifecycle mLifecycle;
private LifecycleOwner mLifecycleOwner; private LifecycleOwner mLifecycleOwner;
private FakeFeatureFactory mFeatureFactory; private FakeFeatureFactory mFeatureFactory;
@@ -90,6 +94,7 @@ public class AudioStreamButtonControllerTest {
@Before @Before
public void setUp() { public void setUp() {
mSetFlagsRule.disableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
ShadowAudioStreamsHelper.setUseMock(mAudioStreamsHelper); ShadowAudioStreamsHelper.setUseMock(mAudioStreamsHelper);
when(mAudioStreamsHelper.getLeBroadcastAssistant()).thenReturn(mAssistant); when(mAudioStreamsHelper.getLeBroadcastAssistant()).thenReturn(mAssistant);
mFeatureFactory = FakeFeatureFactory.setupForTest(); mFeatureFactory = FakeFeatureFactory.setupForTest();
@@ -254,6 +259,33 @@ public class AudioStreamButtonControllerTest {
.setButton1Icon(com.android.settings.R.drawable.ic_settings_close); .setButton1Icon(com.android.settings.R.drawable.ic_settings_close);
} }
@Test
public void testCallback_onReceiveStateChangedWithSourcePresent_updateButton() {
mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
String address = "11:22:33:44:55:66";
BluetoothLeBroadcastReceiveState state = mock(BluetoothLeBroadcastReceiveState.class);
when(state.getBroadcastId()).thenReturn(BROADCAST_ID);
when(state.getSourceDevice()).thenReturn(mSourceDevice);
when(mSourceDevice.getAddress()).thenReturn(address);
List<Long> bisSyncState = new ArrayList<>();
when(state.getBisSyncState()).thenReturn(bisSyncState);
when(mAudioStreamsHelper.getAllPresentSources()).thenReturn(List.of(state));
mController.displayPreference(mScreen);
mController.mBroadcastAssistantCallback.onReceiveStateChanged(
mock(BluetoothDevice.class), /* sourceId= */ 0, state);
verify(mFeatureFactory.metricsFeatureProvider, never())
.action(any(), eq(SettingsEnums.ACTION_AUDIO_STREAM_JOIN_SUCCEED), anyInt());
// Called twice, once in displayPreference, the other one in callback
verify(mPreference, times(2)).setButton1Enabled(true);
verify(mPreference, times(2)).setButton1Text(R.string.audio_streams_disconnect);
verify(mPreference, times(2))
.setButton1Icon(com.android.settings.R.drawable.ic_settings_close);
}
@Test @Test
public void testCallback_onSourceAddFailed_updateButton() { public void testCallback_onSourceAddFailed_updateButton() {
when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(Collections.emptyList()); when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(Collections.emptyList());

View File

@@ -18,6 +18,8 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamHeaderController.AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY; import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamHeaderController.AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamHeaderController.AUDIO_STREAM_HEADER_NOT_LISTENING_SUMMARY; import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamHeaderController.AUDIO_STREAM_HEADER_NOT_LISTENING_SUMMARY;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamHeaderController.AUDIO_STREAM_HEADER_PRESENT_NOW_SUMMARY;
import static com.android.settingslib.flags.Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
@@ -31,6 +33,7 @@ import android.bluetooth.BluetoothLeBroadcastAssistant;
import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.content.Context; import android.content.Context;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.platform.test.flag.junit.SetFlagsRule;
import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LifecycleOwner;
import androidx.preference.PreferenceScreen; import androidx.preference.PreferenceScreen;
@@ -68,8 +71,9 @@ import java.util.concurrent.Executor;
ShadowAudioStreamsHelper.class, ShadowAudioStreamsHelper.class,
}) })
public class AudioStreamHeaderControllerTest { public class AudioStreamHeaderControllerTest {
@Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
@Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
private static final String KEY = "audio_stream_header"; private static final String KEY = "audio_stream_header";
private static final int BROADCAST_ID = 1; private static final int BROADCAST_ID = 1;
private static final String BROADCAST_NAME = "broadcast name"; private static final String BROADCAST_NAME = "broadcast name";
@@ -81,12 +85,15 @@ public class AudioStreamHeaderControllerTest {
@Mock private AudioStreamDetailsFragment mFragment; @Mock private AudioStreamDetailsFragment mFragment;
@Mock private LayoutPreference mPreference; @Mock private LayoutPreference mPreference;
@Mock private EntityHeaderController mHeaderController; @Mock private EntityHeaderController mHeaderController;
@Mock private BluetoothDevice mBluetoothDevice;
private Lifecycle mLifecycle; private Lifecycle mLifecycle;
private LifecycleOwner mLifecycleOwner; private LifecycleOwner mLifecycleOwner;
private AudioStreamHeaderController mController; private AudioStreamHeaderController mController;
@Before @Before
public void setUp() { public void setUp() {
mSetFlagsRule.disableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
ShadowEntityHeaderController.setUseMock(mHeaderController); ShadowEntityHeaderController.setUseMock(mHeaderController);
ShadowAudioStreamsHelper.setUseMock(mAudioStreamsHelper); ShadowAudioStreamsHelper.setUseMock(mAudioStreamsHelper);
when(mAudioStreamsHelper.getLeBroadcastAssistant()).thenReturn(mAssistant); when(mAudioStreamsHelper.getLeBroadcastAssistant()).thenReturn(mAssistant);
@@ -168,6 +175,44 @@ public class AudioStreamHeaderControllerTest {
verify(mScreen).addPreference(any()); verify(mScreen).addPreference(any());
} }
@Test
public void testDisplayPreference_sourcePresent_setSummary() {
mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
String address = "11:22:33:44:55:66";
when(mBroadcastReceiveState.getBroadcastId()).thenReturn(BROADCAST_ID);
when(mBroadcastReceiveState.getSourceDevice()).thenReturn(mBluetoothDevice);
when(mBluetoothDevice.getAddress()).thenReturn(address);
List<Long> bisSyncState = new ArrayList<>();
when(mBroadcastReceiveState.getBisSyncState()).thenReturn(bisSyncState);
when(mAudioStreamsHelper.getAllPresentSources())
.thenReturn(List.of(mBroadcastReceiveState));
mController.displayPreference(mScreen);
verify(mHeaderController).setLabel(BROADCAST_NAME);
verify(mHeaderController).setIcon(any(Drawable.class));
verify(mHeaderController)
.setSummary(mContext.getString(AUDIO_STREAM_HEADER_PRESENT_NOW_SUMMARY));
verify(mHeaderController).done(true);
verify(mScreen).addPreference(any());
}
@Test
public void testDisplayPreference_sourceNotPresent_setSummary() {
mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
when(mAudioStreamsHelper.getAllPresentSources()).thenReturn(Collections.emptyList());
mController.displayPreference(mScreen);
verify(mHeaderController).setLabel(BROADCAST_NAME);
verify(mHeaderController).setIcon(any(Drawable.class));
verify(mHeaderController).setSummary(AUDIO_STREAM_HEADER_NOT_LISTENING_SUMMARY);
verify(mHeaderController).done(true);
verify(mScreen).addPreference(any());
}
@Test @Test
public void testCallback_onSourceRemoved_updateButton() { public void testCallback_onSourceRemoved_updateButton() {
when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(Collections.emptyList()); when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(Collections.emptyList());
@@ -212,4 +257,25 @@ public class AudioStreamHeaderControllerTest {
.setSummary(mContext.getString(AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY)); .setSummary(mContext.getString(AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY));
verify(mHeaderController, times(2)).done(true); verify(mHeaderController, times(2)).done(true);
} }
@Test
public void testCallback_onReceiveStateChangedWithSourcePresent_updateButton() {
mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
String address = "11:22:33:44:55:66";
when(mAudioStreamsHelper.getAllPresentSources())
.thenReturn(List.of(mBroadcastReceiveState));
when(mBroadcastReceiveState.getBroadcastId()).thenReturn(BROADCAST_ID);
when(mBroadcastReceiveState.getSourceDevice()).thenReturn(mBluetoothDevice);
when(mBluetoothDevice.getAddress()).thenReturn(address);
mController.displayPreference(mScreen);
mController.mBroadcastAssistantCallback.onReceiveStateChanged(
mock(BluetoothDevice.class), /* sourceId= */ 0, mBroadcastReceiveState);
// Called twice, once in displayPreference, the other one in callback
verify(mHeaderController, times(2))
.setSummary(mContext.getString(AUDIO_STREAM_HEADER_PRESENT_NOW_SUMMARY));
verify(mHeaderController, times(2)).done(true);
}
} }

View File

@@ -16,6 +16,8 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams; package com.android.settings.connecteddevice.audiosharing.audiostreams;
import static com.android.settingslib.flags.Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
@@ -30,6 +32,7 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import android.content.Context; import android.content.Context;
import android.platform.test.flag.junit.SetFlagsRule;
import android.text.SpannableString; import android.text.SpannableString;
import androidx.preference.Preference; import androidx.preference.Preference;
@@ -48,6 +51,8 @@ import org.robolectric.RobolectricTestRunner;
@RunWith(RobolectricTestRunner.class) @RunWith(RobolectricTestRunner.class)
public class AudioStreamStateHandlerTest { public class AudioStreamStateHandlerTest {
@Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
@Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
private static final int SUMMARY_RES = 1; private static final int SUMMARY_RES = 1;
private static final String SUMMARY = "summary"; private static final String SUMMARY = "summary";
private final Context mContext = spy(ApplicationProvider.getApplicationContext()); private final Context mContext = spy(ApplicationProvider.getApplicationContext());
@@ -58,6 +63,7 @@ public class AudioStreamStateHandlerTest {
@Before @Before
public void setUp() { public void setUp() {
mSetFlagsRule.disableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
mHandler = spy(new AudioStreamStateHandler()); mHandler = spy(new AudioStreamStateHandler());
} }
@@ -101,6 +107,28 @@ public class AudioStreamStateHandlerTest {
verify(mPreference).setOnPreferenceClickListener(eq(null)); verify(mPreference).setOnPreferenceClickListener(eq(null));
} }
@Test
public void testHandleStateChange_setNewState_sourcePresent() {
mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
when(mHandler.getStateEnum())
.thenReturn(AudioStreamsProgressCategoryController.AudioStreamState.SOURCE_PRESENT);
when(mPreference.getAudioStreamState())
.thenReturn(
AudioStreamsProgressCategoryController.AudioStreamState
.ADD_SOURCE_BAD_CODE);
mHandler.handleStateChange(mPreference, mController, mHelper);
verify(mPreference)
.setAudioStreamState(
AudioStreamsProgressCategoryController.AudioStreamState.SOURCE_PRESENT);
verify(mHandler).performAction(any(), any(), any());
verify(mPreference).setIsConnected(eq(true));
verify(mPreference).setSummary(eq(""));
verify(mPreference).setOnPreferenceClickListener(eq(null));
}
@Test @Test
public void testHandleStateChange_setNewState_newSummary_newListener() { public void testHandleStateChange_setNewState_newSummary_newListener() {
Preference.OnPreferenceClickListener listener = Preference.OnPreferenceClickListener listener =

View File

@@ -19,6 +19,8 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams;
import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
import static com.android.settingslib.flags.Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
@@ -37,6 +39,7 @@ import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.content.Context; import android.content.Context;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.content.res.Resources; import android.content.res.Resources;
import android.platform.test.flag.junit.SetFlagsRule;
import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentActivity;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
@@ -74,6 +77,8 @@ import java.util.List;
}) })
public class AudioStreamsHelperTest { public class AudioStreamsHelperTest {
@Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
@Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
private static final int GROUP_ID = 1; private static final int GROUP_ID = 1;
private static final int BROADCAST_ID_1 = 1; private static final int BROADCAST_ID_1 = 1;
private static final int BROADCAST_ID_2 = 2; private static final int BROADCAST_ID_2 = 2;
@@ -86,10 +91,12 @@ public class AudioStreamsHelperTest {
@Mock private BluetoothLeBroadcastMetadata mMetadata; @Mock private BluetoothLeBroadcastMetadata mMetadata;
@Mock private CachedBluetoothDevice mCachedDevice; @Mock private CachedBluetoothDevice mCachedDevice;
@Mock private BluetoothDevice mDevice; @Mock private BluetoothDevice mDevice;
@Mock private BluetoothDevice mSourceDevice;
private AudioStreamsHelper mHelper; private AudioStreamsHelper mHelper;
@Before @Before
public void setUp() { public void setUp() {
mSetFlagsRule.disableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
when(mLocalBluetoothManager.getProfileManager()).thenReturn(mLocalBluetoothProfileManager); when(mLocalBluetoothManager.getProfileManager()).thenReturn(mLocalBluetoothProfileManager);
when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(mDeviceManager); when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(mDeviceManager);
when(mLocalBluetoothProfileManager.getLeAudioBroadcastAssistantProfile()) when(mLocalBluetoothProfileManager.getLeAudioBroadcastAssistantProfile())
@@ -166,6 +173,7 @@ public class AudioStreamsHelperTest {
@Test @Test
public void removeSource_memberHasConnectedSource() { public void removeSource_memberHasConnectedSource() {
String address = "11:22:33:44:55:66";
List<BluetoothDevice> devices = new ArrayList<>(); List<BluetoothDevice> devices = new ArrayList<>();
var memberDevice = mock(BluetoothDevice.class); var memberDevice = mock(BluetoothDevice.class);
devices.add(mDevice); devices.add(mDevice);
@@ -184,6 +192,8 @@ public class AudioStreamsHelperTest {
List<Long> bisSyncState = new ArrayList<>(); List<Long> bisSyncState = new ArrayList<>();
bisSyncState.add(1L); bisSyncState.add(1L);
when(source.getBisSyncState()).thenReturn(bisSyncState); when(source.getBisSyncState()).thenReturn(bisSyncState);
when(source.getSourceDevice()).thenReturn(mSourceDevice);
when(mSourceDevice.getAddress()).thenReturn(address);
mHelper.removeSource(BROADCAST_ID_2); mHelper.removeSource(BROADCAST_ID_2);
@@ -217,6 +227,52 @@ public class AudioStreamsHelperTest {
assertThat(list.get(0)).isEqualTo(source); assertThat(list.get(0)).isEqualTo(source);
} }
@Test
public void getAllPresentSources_noSource() {
mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
List<BluetoothDevice> devices = new ArrayList<>();
devices.add(mDevice);
String address = "00:00:00:00:00:00";
when(mAssistant.getAllConnectedDevices()).thenReturn(devices);
BluetoothLeBroadcastReceiveState source = mock(BluetoothLeBroadcastReceiveState.class);
when(mDeviceManager.findDevice(any())).thenReturn(mCachedDevice);
when(mCachedDevice.getDevice()).thenReturn(mDevice);
when(mCachedDevice.getGroupId()).thenReturn(GROUP_ID);
when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of(source));
when(source.getSourceDevice()).thenReturn(mSourceDevice);
when(mSourceDevice.getAddress()).thenReturn(address);
var list = mHelper.getAllPresentSources();
assertThat(list).isEmpty();
}
@Test
public void getAllPresentSources_returnSource() {
mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
String address = "11:22:33:44:55:66";
List<BluetoothDevice> devices = new ArrayList<>();
devices.add(mDevice);
when(mAssistant.getAllConnectedDevices()).thenReturn(devices);
BluetoothLeBroadcastReceiveState source = mock(BluetoothLeBroadcastReceiveState.class);
when(mDeviceManager.findDevice(any())).thenReturn(mCachedDevice);
when(mCachedDevice.getDevice()).thenReturn(mDevice);
when(mCachedDevice.getGroupId()).thenReturn(GROUP_ID);
when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of(source));
when(source.getSourceDevice()).thenReturn(mSourceDevice);
when(mSourceDevice.getAddress()).thenReturn(address);
List<Long> bisSyncState = new ArrayList<>();
when(source.getBisSyncState()).thenReturn(bisSyncState);
var list = mHelper.getAllPresentSources();
assertThat(list).isNotEmpty();
assertThat(list.get(0)).isEqualTo(source);
}
@Test @Test
public void startMediaService_noDevice_doNothing() { public void startMediaService_noDevice_doNothing() {
mHelper.startMediaService(mContext, BROADCAST_ID_1, BROADCAST_NAME); mHelper.startMediaService(mContext, BROADCAST_ID_1, BROADCAST_NAME);

View File

@@ -16,6 +16,8 @@
package com.android.settings.connecteddevice.audiosharing.audiostreams; package com.android.settings.connecteddevice.audiosharing.audiostreams;
import static com.android.settingslib.flags.Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.anyString;
@@ -25,6 +27,7 @@ import static org.mockito.Mockito.when;
import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcastMetadata; import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.platform.test.flag.junit.SetFlagsRule;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
@@ -41,14 +44,18 @@ import java.util.List;
@RunWith(RobolectricTestRunner.class) @RunWith(RobolectricTestRunner.class)
public class AudioStreamsProgressCategoryCallbackTest { public class AudioStreamsProgressCategoryCallbackTest {
@Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
@Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@Mock private AudioStreamsProgressCategoryController mController; @Mock private AudioStreamsProgressCategoryController mController;
@Mock private BluetoothDevice mDevice; @Mock private BluetoothDevice mDevice;
@Mock private BluetoothLeBroadcastReceiveState mState; @Mock private BluetoothLeBroadcastReceiveState mState;
@Mock private BluetoothLeBroadcastMetadata mMetadata; @Mock private BluetoothLeBroadcastMetadata mMetadata;
@Mock private BluetoothDevice mSourceDevice;
private AudioStreamsProgressCategoryCallback mCallback; private AudioStreamsProgressCategoryCallback mCallback;
@Before @Before
public void setUp() { public void setUp() {
mSetFlagsRule.disableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
mCallback = new AudioStreamsProgressCategoryCallback(mController); mCallback = new AudioStreamsProgressCategoryCallback(mController);
} }
@@ -62,6 +69,20 @@ public class AudioStreamsProgressCategoryCallbackTest {
verify(mController).handleSourceConnected(any()); verify(mController).handleSourceConnected(any());
} }
@Test
public void testOnReceiveStateChanged_sourcePresent() {
mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
String address = "11:22:33:44:55:66";
List<Long> bisSyncState = new ArrayList<>();
when(mState.getBisSyncState()).thenReturn(bisSyncState);
when(mState.getSourceDevice()).thenReturn(mSourceDevice);
when(mSourceDevice.getAddress()).thenReturn(address);
mCallback.onReceiveStateChanged(mDevice, /* sourceId= */ 0, mState);
verify(mController).handleSourcePresent(any());
}
@Test @Test
public void testOnReceiveStateChanged_badCode() { public void testOnReceiveStateChanged_badCode() {
when(mState.getPaSyncState()) when(mState.getPaSyncState())

View File

@@ -20,10 +20,12 @@ import static com.android.settings.connecteddevice.audiosharing.audiostreams.Aud
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.AudioStreamState.ADD_SOURCE_FAILED; import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.AudioStreamState.ADD_SOURCE_FAILED;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.AudioStreamState.ADD_SOURCE_WAIT_FOR_RESPONSE; import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.AudioStreamState.ADD_SOURCE_WAIT_FOR_RESPONSE;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.AudioStreamState.SOURCE_ADDED; import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.AudioStreamState.SOURCE_ADDED;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.AudioStreamState.SOURCE_PRESENT;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.AudioStreamState.SYNCED; import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.AudioStreamState.SYNCED;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.AudioStreamState.WAIT_FOR_SYNC; import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.AudioStreamState.WAIT_FOR_SYNC;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.UNSET_BROADCAST_ID; import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.UNSET_BROADCAST_ID;
import static com.android.settings.core.BasePreferenceController.AVAILABLE; import static com.android.settings.core.BasePreferenceController.AVAILABLE;
import static com.android.settingslib.flags.Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
@@ -41,12 +43,14 @@ import static org.robolectric.Shadows.shadowOf;
import static java.util.Collections.emptyList; import static java.util.Collections.emptyList;
import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeAudioContentMetadata; import android.bluetooth.BluetoothLeAudioContentMetadata;
import android.bluetooth.BluetoothLeBroadcastMetadata; import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothProfile;
import android.content.Context; import android.content.Context;
import android.os.Looper; import android.os.Looper;
import android.platform.test.flag.junit.SetFlagsRule;
import android.view.View; import android.view.View;
import android.widget.Button; import android.widget.Button;
import android.widget.TextView; import android.widget.TextView;
@@ -96,6 +100,8 @@ import java.util.List;
}) })
public class AudioStreamsProgressCategoryControllerTest { public class AudioStreamsProgressCategoryControllerTest {
@Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
@Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
private static final String VALID_METADATA = private static final String VALID_METADATA =
"BLUETOOTH:UUID:184F;BN:VGVzdA==;AT:1;AD:00A1A1A1A1A1;BI:1E240;BC:VGVzdENvZGU=;" "BLUETOOTH:UUID:184F;BN:VGVzdA==;AT:1;AD:00A1A1A1A1A1;BI:1E240;BC:VGVzdENvZGU=;"
+ "MD:BgNwVGVzdA==;AS:1;PI:A0;NS:1;BS:3;NB:2;SM:BQNUZXN0BARlbmc=;;"; + "MD:BgNwVGVzdA==;AS:1;PI:A0;NS:1;BS:3;NB:2;SM:BQNUZXN0BARlbmc=;;";
@@ -115,6 +121,7 @@ public class AudioStreamsProgressCategoryControllerTest {
@Mock private BluetoothLeBroadcastMetadata mMetadata; @Mock private BluetoothLeBroadcastMetadata mMetadata;
@Mock private CachedBluetoothDevice mDevice; @Mock private CachedBluetoothDevice mDevice;
@Mock private AudioStreamsProgressCategoryPreference mPreference; @Mock private AudioStreamsProgressCategoryPreference mPreference;
@Mock private BluetoothDevice mSourceDevice;
private Lifecycle mLifecycle; private Lifecycle mLifecycle;
private LifecycleOwner mLifecycleOwner; private LifecycleOwner mLifecycleOwner;
private Fragment mFragment; private Fragment mFragment;
@@ -125,6 +132,7 @@ public class AudioStreamsProgressCategoryControllerTest {
ShadowAudioStreamsHelper.setUseMock(mAudioStreamsHelper); ShadowAudioStreamsHelper.setUseMock(mAudioStreamsHelper);
when(mAudioStreamsHelper.getLeBroadcastAssistant()).thenReturn(mLeBroadcastAssistant); when(mAudioStreamsHelper.getLeBroadcastAssistant()).thenReturn(mLeBroadcastAssistant);
when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(emptyList()); when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(emptyList());
mSetFlagsRule.disableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBtManager; ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBtManager;
when(mLocalBtManager.getEventManager()).thenReturn(mBluetoothEventManager); when(mLocalBtManager.getEventManager()).thenReturn(mBluetoothEventManager);
@@ -282,6 +290,29 @@ public class AudioStreamsProgressCategoryControllerTest {
verify(mController, never()).moveToState(any(), any()); verify(mController, never()).moveToState(any(), any());
} }
@Test
public void testOnStart_initHasDevice_getPresentSources() {
mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
// Setup a device
ShadowAudioStreamsHelper.setCachedBluetoothDeviceInSharingOrLeConnected(mDevice);
List<BluetoothLeBroadcastReceiveState> connectedList = new ArrayList<>();
// Empty connected device list
when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(connectedList);
mController.onStart(mLifecycleOwner);
shadowOf(Looper.getMainLooper()).idle();
verify(mAudioStreamsHelper).getAllPresentSources();
verify(mLeBroadcastAssistant).startSearchingForSources(any());
var dialog = ShadowAlertDialog.getLatestAlertDialog();
assertThat(dialog).isNull();
verify(mController, never()).moveToState(any(), any());
}
@Test @Test
public void testOnStart_handleSourceFromQrCode() { public void testOnStart_handleSourceFromQrCode() {
// Setup a device // Setup a device
@@ -764,6 +795,58 @@ public class AudioStreamsProgressCategoryControllerTest {
assertThat(states.get(1)).isEqualTo(ADD_SOURCE_FAILED); assertThat(states.get(1)).isEqualTo(ADD_SOURCE_FAILED);
} }
@Test
public void testHandleSourcePresent_updateState() {
mSetFlagsRule.enableFlags(FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX);
String address = "11:22:33:44:55:66";
// Setup a device
ShadowAudioStreamsHelper.setCachedBluetoothDeviceInSharingOrLeConnected(mDevice);
// Setup mPreference so it's not null
mController.displayPreference(mScreen);
// A new source found
when(mMetadata.getBroadcastId()).thenReturn(NEWLY_FOUND_BROADCAST_ID);
mController.handleSourceFound(mMetadata);
shadowOf(Looper.getMainLooper()).idle();
// The connected source is identified as having a bad code
BluetoothLeBroadcastReceiveState receiveState =
mock(BluetoothLeBroadcastReceiveState.class);
when(receiveState.getBroadcastId()).thenReturn(NEWLY_FOUND_BROADCAST_ID);
when(receiveState.getSourceDevice()).thenReturn(mSourceDevice);
when(mSourceDevice.getAddress()).thenReturn(address);
List<Long> bisSyncState = new ArrayList<>();
when(receiveState.getBisSyncState()).thenReturn(bisSyncState);
// The new found source is identified as failed to connect
mController.handleSourcePresent(receiveState);
shadowOf(Looper.getMainLooper()).idle();
ArgumentCaptor<AudioStreamPreference> preference =
ArgumentCaptor.forClass(AudioStreamPreference.class);
ArgumentCaptor<AudioStreamsProgressCategoryController.AudioStreamState> state =
ArgumentCaptor.forClass(
AudioStreamsProgressCategoryController.AudioStreamState.class);
verify(mController, times(2)).moveToState(preference.capture(), state.capture());
List<AudioStreamPreference> preferences = preference.getAllValues();
assertThat(preferences.size()).isEqualTo(2);
List<AudioStreamsProgressCategoryController.AudioStreamState> states = state.getAllValues();
assertThat(states.size()).isEqualTo(2);
// Verify one preference is created with SYNCED
assertThat(preferences.get(0).getAudioStreamBroadcastId())
.isEqualTo(NEWLY_FOUND_BROADCAST_ID);
assertThat(states.get(0)).isEqualTo(SYNCED);
// Verify the preference is updated to state ADD_SOURCE_FAILED
assertThat(preferences.get(1).getAudioStreamBroadcastId())
.isEqualTo(NEWLY_FOUND_BROADCAST_ID);
assertThat(states.get(1)).isEqualTo(SOURCE_PRESENT);
}
private static BluetoothLeBroadcastReceiveState createConnectedMock(int id) { private static BluetoothLeBroadcastReceiveState createConnectedMock(int id) {
var connected = mock(BluetoothLeBroadcastReceiveState.class); var connected = mock(BluetoothLeBroadcastReceiveState.class);
List<Long> bisSyncState = new ArrayList<>(); List<Long> bisSyncState = new ArrayList<>();

View File

@@ -0,0 +1,137 @@
/*
* 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.audiostreams;
import static android.app.settings.SettingsEnums.AUDIO_STREAM_MAIN;
import static com.android.settings.connecteddevice.audiosharing.audiostreams.SourcePresentState.AUDIO_STREAM_SOURCE_PRESENT_STATE_SUMMARY;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import androidx.fragment.app.FragmentActivity;
import androidx.preference.Preference;
import androidx.test.core.app.ApplicationProvider;
import com.android.settings.R;
import com.android.settings.SettingsActivity;
import com.android.settings.testutils.FakeFeatureFactory;
import com.android.settings.testutils.shadow.ShadowFragment;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
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 SourcePresentStateTest {
@Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
private static final int BROADCAST_ID = 1;
private static final String BROADCAST_TITLE = "title";
private final Context mContext = ApplicationProvider.getApplicationContext();
@Mock private AudioStreamPreference mPreference;
@Mock private AudioStreamsProgressCategoryController mController;
@Mock private AudioStreamsHelper mHelper;
@Mock private AudioStreamsRepository mRepository;
@Mock private AudioStreamsDashboardFragment mFragment;
@Mock private FragmentActivity mActivity;
private FakeFeatureFactory mFeatureFactory;
private SourcePresentState mInstance;
@Before
public void setUp() {
when(mFragment.getActivity()).thenReturn(mActivity);
mFeatureFactory = FakeFeatureFactory.setupForTest();
mInstance = new SourcePresentState();
when(mPreference.getAudioStreamBroadcastId()).thenReturn(BROADCAST_ID);
when(mPreference.getTitle()).thenReturn(BROADCAST_TITLE);
}
@Test
public void testGetInstance() {
mInstance = SourcePresentState.getInstance();
assertThat(mInstance).isNotNull();
assertThat(mInstance).isInstanceOf(SourcePresentState.class);
}
@Test
public void testGetSummary() {
int summary = mInstance.getSummary();
assertThat(summary).isEqualTo(AUDIO_STREAM_SOURCE_PRESENT_STATE_SUMMARY);
}
@Test
public void testGetStateEnum() {
AudioStreamsProgressCategoryController.AudioStreamState stateEnum =
mInstance.getStateEnum();
assertThat(stateEnum)
.isEqualTo(AudioStreamsProgressCategoryController.AudioStreamState.SOURCE_PRESENT);
}
@Test
public void testGetOnClickListener_startSubSettings() {
when(mController.getFragment()).thenReturn(mFragment);
when(mFragment.getMetricsCategory()).thenReturn(AUDIO_STREAM_MAIN);
Preference.OnPreferenceClickListener listener = mInstance.getOnClickListener(mController);
assertThat(listener).isNotNull();
// mContext is not an Activity context, calling startActivity() from outside of an Activity
// context requires the FLAG_ACTIVITY_NEW_TASK flag, create a mock to avoid this
// AndroidRuntimeException.
Context activityContext = mock(Context.class);
when(mPreference.getContext()).thenReturn(activityContext);
listener.onPreferenceClick(mPreference);
ArgumentCaptor<Intent> argumentCaptor = ArgumentCaptor.forClass(Intent.class);
verify(activityContext).startActivity(argumentCaptor.capture());
Intent intent = argumentCaptor.getValue();
assertThat(intent.getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT))
.isEqualTo(AudioStreamDetailsFragment.class.getName());
assertThat(intent.getIntExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_TITLE_RESID, 0))
.isEqualTo(R.string.audio_streams_detail_page_title);
assertThat(intent.getIntExtra(MetricsFeatureProvider.EXTRA_SOURCE_METRICS_CATEGORY, 0))
.isEqualTo(AUDIO_STREAM_MAIN);
Bundle bundle = intent.getBundleExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS);
assertThat(bundle).isNotNull();
assertThat(bundle.getString(AudioStreamDetailsFragment.BROADCAST_NAME_ARG))
.isEqualTo(BROADCAST_TITLE);
assertThat(bundle.getInt(AudioStreamDetailsFragment.BROADCAST_ID_ARG))
.isEqualTo(BROADCAST_ID);
}
}

View File

@@ -59,6 +59,11 @@ public class ShadowAudioStreamsHelper {
return sMockHelper.getAllConnectedSources(); return sMockHelper.getAllConnectedSources();
} }
@Implementation
public List<BluetoothLeBroadcastReceiveState> getAllPresentSources() {
return sMockHelper.getAllPresentSources();
}
/** Gets {@link CachedBluetoothDevice} in sharing or le connected */ /** Gets {@link CachedBluetoothDevice} in sharing or le connected */
@Implementation @Implementation
public static Optional<CachedBluetoothDevice> getCachedBluetoothDeviceInSharingOrLeConnected( public static Optional<CachedBluetoothDevice> getCachedBluetoothDeviceInSharingOrLeConnected(

View File

@@ -32,7 +32,6 @@ import static org.mockito.Mockito.when;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.UserManager;
import android.provider.Settings; import android.provider.Settings;
import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager; import android.telephony.SubscriptionManager;
@@ -73,8 +72,6 @@ public class MobileNetworkSummaryControllerTest {
@Mock @Mock
private PreferenceScreen mPreferenceScreen; private PreferenceScreen mPreferenceScreen;
@Mock @Mock
private UserManager mUserManager;
@Mock
private MobileNetworkRepository mMobileNetworkRepository; private MobileNetworkRepository mMobileNetworkRepository;
@Mock @Mock
private MobileNetworkRepository.MobileNetworkCallback mMobileNetworkCallback; private MobileNetworkRepository.MobileNetworkCallback mMobileNetworkCallback;
@@ -92,7 +89,6 @@ public class MobileNetworkSummaryControllerTest {
doReturn(mTelephonyManager).when(mContext).getSystemService(TelephonyManager.class); doReturn(mTelephonyManager).when(mContext).getSystemService(TelephonyManager.class);
doReturn(mSubscriptionManager).when(mContext).getSystemService(SubscriptionManager.class); doReturn(mSubscriptionManager).when(mContext).getSystemService(SubscriptionManager.class);
doReturn(mEuiccManager).when(mContext).getSystemService(EuiccManager.class); doReturn(mEuiccManager).when(mContext).getSystemService(EuiccManager.class);
doReturn(mUserManager).when(mContext).getSystemService(UserManager.class);
mMobileNetworkRepository = MobileNetworkRepository.getInstance(mContext); mMobileNetworkRepository = MobileNetworkRepository.getInstance(mContext);
mLifecycleOwner = () -> mLifecycle; mLifecycleOwner = () -> mLifecycle;
mLifecycle = new Lifecycle(mLifecycleOwner); mLifecycle = new Lifecycle(mLifecycleOwner);
@@ -118,21 +114,6 @@ public class MobileNetworkSummaryControllerTest {
SubscriptionUtil.setAvailableSubscriptionsForTesting(null); SubscriptionUtil.setAvailableSubscriptionsForTesting(null);
} }
@Test
public void isAvailable_wifiOnlyMode_notAvailable() {
when(mTelephonyManager.isDataCapable()).thenReturn(false);
when(mUserManager.isAdminUser()).thenReturn(true);
assertThat(mController.isAvailable()).isFalse();
}
@Test
public void isAvailable_secondaryUser_notAvailable() {
when(mTelephonyManager.isDataCapable()).thenReturn(true);
when(mUserManager.isAdminUser()).thenReturn(false);
assertThat(mController.isAvailable()).isFalse();
}
@Test @Test
public void getSummary_noSubscriptions_returnSummaryCorrectly() { public void getSummary_noSubscriptions_returnSummaryCorrectly() {
mController.displayPreference(mPreferenceScreen); mController.displayPreference(mPreferenceScreen);

View File

@@ -1,102 +0,0 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.network;
import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.os.UserManager;
import android.text.BidiFormatter;
import android.util.FeatureFlagUtils;
import com.android.settings.R;
import com.android.settings.testutils.shadow.ShadowRestrictedLockUtilsInternal;
import com.android.settings.testutils.shadow.ShadowUserManager;
import com.android.settings.testutils.shadow.ShadowUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.util.ReflectionHelpers;
@RunWith(RobolectricTestRunner.class)
@Config(shadows = {
ShadowRestrictedLockUtilsInternal.class,
ShadowUtils.class,
ShadowUserManager.class,
})
public class TopLevelNetworkEntryPreferenceControllerTest {
@Mock
private MobileNetworkPreferenceController mMobileNetworkPreferenceController;;
private Context mContext;
private TopLevelNetworkEntryPreferenceController mController;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mContext = RuntimeEnvironment.application;
final ShadowUserManager um = Shadow.extract(
RuntimeEnvironment.application.getSystemService(UserManager.class));
um.setIsAdminUser(true);
mController = new TopLevelNetworkEntryPreferenceController(mContext, "test_key");
ReflectionHelpers.setField(mController, "mMobileNetworkPreferenceController",
mMobileNetworkPreferenceController);
}
@After
public void tearDown() {
ShadowUtils.reset();
}
@Test
public void getAvailabilityStatus_demoUser_nonLargeScreen_unsupported() {
ShadowUtils.setIsDemoUser(true);
FeatureFlagUtils.setEnabled(mContext, "settings_support_large_screen", false);
assertThat(mController.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE);
}
@Test
public void getSummary_hasMobile_shouldReturnMobileSummary() {
when(mMobileNetworkPreferenceController.isAvailable()).thenReturn(true);
assertThat(mController.getSummary()).isEqualTo(BidiFormatter.getInstance().unicodeWrap(
mContext.getString(R.string.network_dashboard_summary_mobile)));
}
@Test
public void getSummary_noMobile_shouldReturnNoMobileSummary() {
when(mMobileNetworkPreferenceController.isAvailable()).thenReturn(false);
assertThat(mController.getSummary()).isEqualTo(BidiFormatter.getInstance().unicodeWrap(
mContext.getString(R.string.network_dashboard_summary_no_mobile)));
}
}

View File

@@ -43,7 +43,6 @@ import androidx.preference.PreferenceScreen;
import com.android.settings.R; import com.android.settings.R;
import com.android.settings.notification.zen.AbstractZenModePreferenceController.ZenModeConfigWrapper; import com.android.settings.notification.zen.AbstractZenModePreferenceController.ZenModeConfigWrapper;
import com.android.settings.notification.zen.ZenModeBehaviorFooterPreferenceController;
import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.core.lifecycle.Lifecycle;
import org.junit.Before; import org.junit.Before;
@@ -206,7 +205,7 @@ public class ZenModeBehaviorFooterPreferenceControllerTest {
ZenRule injectedRule = spy(new ZenRule()); ZenRule injectedRule = spy(new ZenRule());
injectedRule.zenMode = ZEN_MODE_ALARMS; injectedRule.zenMode = ZEN_MODE_ALARMS;
injectedRule.component = mock(ComponentName.class); injectedRule.component = mock(ComponentName.class);
when(injectedRule.isAutomaticActive()).thenReturn(true); when(injectedRule.isActive()).thenReturn(true);
when(injectedRule.component.getPackageName()).thenReturn(TEST_APP_NAME); when(injectedRule.component.getPackageName()).thenReturn(TEST_APP_NAME);
injectedAutomaticRules.put("testid", injectedRule); injectedAutomaticRules.put("testid", injectedRule);
@@ -226,7 +225,7 @@ public class ZenModeBehaviorFooterPreferenceControllerTest {
ZenRule injectedRule = spy(new ZenRule()); ZenRule injectedRule = spy(new ZenRule());
injectedRule.zenMode = ZEN_MODE_NO_INTERRUPTIONS; injectedRule.zenMode = ZEN_MODE_NO_INTERRUPTIONS;
injectedRule.component = mock(ComponentName.class); injectedRule.component = mock(ComponentName.class);
when(injectedRule.isAutomaticActive()).thenReturn(true); when(injectedRule.isActive()).thenReturn(true);
when(injectedRule.component.getPackageName()).thenReturn(TEST_APP_NAME); when(injectedRule.component.getPackageName()).thenReturn(TEST_APP_NAME);
injectedAutomaticRules.put("testid", injectedRule); injectedAutomaticRules.put("testid", injectedRule);

View File

@@ -44,7 +44,6 @@ import androidx.preference.Preference;
import androidx.preference.PreferenceScreen; import androidx.preference.PreferenceScreen;
import com.android.settings.notification.zen.AbstractZenModePreferenceController.ZenModeConfigWrapper; import com.android.settings.notification.zen.AbstractZenModePreferenceController.ZenModeConfigWrapper;
import com.android.settings.notification.zen.ZenModeSettingsFooterPreferenceController;
import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.core.lifecycle.Lifecycle;
import org.junit.Before; import org.junit.Before;
@@ -289,7 +288,7 @@ public class ZenModeSettingsFooterPreferenceControllerTest {
injectedRule.component = mock(ComponentName.class); injectedRule.component = mock(ComponentName.class);
injectedRule.name = nameAndId; injectedRule.name = nameAndId;
injectedRule.conditionId = new Uri.Builder().authority(nameAndId).build(); // unique uri injectedRule.conditionId = new Uri.Builder().authority(nameAndId).build(); // unique uri
when(injectedRule.isAutomaticActive()).thenReturn(isActive); when(injectedRule.isActive()).thenReturn(isActive);
when(mConfigWrapper.isTimeRule(injectedRule.conditionId)).thenReturn(!isApp); when(mConfigWrapper.isTimeRule(injectedRule.conditionId)).thenReturn(!isApp);
if (isApp) { if (isApp) {
when(injectedRule.component.getPackageName()).thenReturn(TEST_APP_NAME); when(injectedRule.component.getPackageName()).thenReturn(TEST_APP_NAME);

View File

@@ -36,11 +36,11 @@ import androidx.preference.PreferenceManager;
import androidx.preference.PreferenceScreen; import androidx.preference.PreferenceScreen;
import com.android.internal.widget.LockPatternUtils; import com.android.internal.widget.LockPatternUtils;
import com.android.settings.core.ObservablePreferenceFragment;
import com.android.settings.users.OwnerInfoSettings; import com.android.settings.users.OwnerInfoSettings;
import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
import com.android.settingslib.RestrictedPreference; import com.android.settingslib.RestrictedPreference;
import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.core.lifecycle.Lifecycle;
import com.android.settingslib.core.lifecycle.ObservablePreferenceFragment;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;

View File

@@ -17,57 +17,37 @@
package com.android.settings.network package com.android.settings.network
import android.content.Context import android.content.Context
import android.content.res.Resources
import android.os.UserManager
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settings.R import com.android.settings.network.MobileNetworkListFragment.Companion.SearchIndexProvider
import com.android.settingslib.spaprivileged.framework.common.userManager import com.android.settings.network.telephony.SimRepository
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mockito.kotlin.doReturn import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock import org.mockito.kotlin.mock
import org.mockito.kotlin.spy
import org.mockito.kotlin.stub import org.mockito.kotlin.stub
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class MobileNetworkListFragmentTest { class MobileNetworkListFragmentTest {
private val mockUserManager = mock<UserManager>() private val mockSimRepository = mock<SimRepository>()
private val mockResources = mock<Resources>() private val context: Context = ApplicationProvider.getApplicationContext()
private val context: Context = spy(ApplicationProvider.getApplicationContext()) {
on { userManager } doReturn mockUserManager
on { resources } doReturn mockResources
}
@Test @Test
fun isPageSearchEnabled_adminUser_shouldReturnTrue() { fun isPageSearchEnabled_showMobileNetworkPage_returnTrue() {
mockUserManager.stub { mockSimRepository.stub { on { showMobileNetworkPage() } doReturn true }
on { isAdminUser } doReturn true
}
mockResources.stub {
on { getBoolean(R.bool.config_show_sim_info) } doReturn true
}
val isEnabled = val isEnabled = SearchIndexProvider { mockSimRepository }.isPageSearchEnabled(context)
MobileNetworkListFragment.SEARCH_INDEX_DATA_PROVIDER.isPageSearchEnabled(context)
assertThat(isEnabled).isTrue() assertThat(isEnabled).isTrue()
} }
@Test @Test
fun isPageSearchEnabled_nonAdminUser_shouldReturnFalse() { fun isPageSearchEnabled_hideMobileNetworkPage_returnFalse() {
mockUserManager.stub { mockSimRepository.stub { on { showMobileNetworkPage() } doReturn false }
on { isAdminUser } doReturn false
}
mockResources.stub {
on { getBoolean(R.bool.config_show_sim_info) } doReturn true
}
val isEnabled = val isEnabled = SearchIndexProvider { mockSimRepository }.isPageSearchEnabled(context)
MobileNetworkListFragment.SEARCH_INDEX_DATA_PROVIDER.isPageSearchEnabled(context)
assertThat(isEnabled).isFalse() assertThat(isEnabled).isFalse()
} }

View File

@@ -0,0 +1,101 @@
/*
* 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.text.BidiFormatter
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settings.R
import com.android.settings.core.BasePreferenceController
import com.android.settings.network.telephony.SimRepository
import com.google.common.truth.Truth.assertThat
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 TopLevelNetworkEntryPreferenceControllerTest {
private val mockSimRepository = mock<SimRepository>()
private val context: Context = ApplicationProvider.getApplicationContext()
private var isDemoUser = false
private var isEmbeddingActivityEnabled = false
private var controller =
TopLevelNetworkEntryPreferenceController(
context = context,
preferenceKey = TEST_KEY,
simRepository = mockSimRepository,
isDemoUser = { isDemoUser },
isEmbeddingActivityEnabled = { isEmbeddingActivityEnabled },
)
@Test
fun getAvailabilityStatus_demoUser_largeScreen_unsupported() {
isDemoUser = true
isEmbeddingActivityEnabled = true
val availabilityStatus = controller.availabilityStatus
assertThat(availabilityStatus).isEqualTo(BasePreferenceController.AVAILABLE)
}
@Test
fun getAvailabilityStatus_demoUser_nonLargeScreen_unsupported() {
isDemoUser = true
isEmbeddingActivityEnabled = false
val availabilityStatus = controller.availabilityStatus
assertThat(availabilityStatus).isEqualTo(BasePreferenceController.UNSUPPORTED_ON_DEVICE)
}
@Test
fun getSummary_hasMobile_shouldReturnMobileSummary() {
mockSimRepository.stub { on { showMobileNetworkPage() } doReturn true }
val summary = controller.summary
assertThat(summary)
.isEqualTo(
BidiFormatter.getInstance()
.unicodeWrap(context.getString(R.string.network_dashboard_summary_mobile))
)
}
@Test
fun getSummary_noMobile_shouldReturnNoMobileSummary() {
mockSimRepository.stub { on { showMobileNetworkPage() } doReturn false }
val summary = controller.summary
assertThat(summary)
.isEqualTo(
BidiFormatter.getInstance()
.unicodeWrap(context.getString(R.string.network_dashboard_summary_no_mobile))
)
}
private companion object {
const val TEST_KEY = "test_key"
}
}

View File

@@ -0,0 +1,87 @@
/*
* 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.telephony
import android.content.Context
import android.content.pm.PackageManager
import android.os.UserManager
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settingslib.spaprivileged.framework.common.userManager
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.spy
import org.mockito.kotlin.stub
@RunWith(AndroidJUnit4::class)
class SimRepositoryTest {
private val mockUserManager = mock<UserManager>()
private val mockPackageManager = mock<PackageManager>()
private val context: Context = spy(ApplicationProvider.getApplicationContext()) {
on { userManager } doReturn mockUserManager
on { packageManager } doReturn mockPackageManager
}
private val repository = SimRepository(context)
@Test
fun showMobileNetworkPage_adminUserAndHasTelephony_returnTrue() {
mockUserManager.stub {
on { isAdminUser } doReturn true
}
mockPackageManager.stub {
on { hasSystemFeature(PackageManager.FEATURE_TELEPHONY) } doReturn true
}
val showMobileNetworkPage = repository.showMobileNetworkPage()
assertThat(showMobileNetworkPage).isTrue()
}
@Test
fun showMobileNetworkPage_notAdminUser_returnFalse() {
mockUserManager.stub {
on { isAdminUser } doReturn false
}
mockPackageManager.stub {
on { hasSystemFeature(PackageManager.FEATURE_TELEPHONY) } doReturn true
}
val showMobileNetworkPage = repository.showMobileNetworkPage()
assertThat(showMobileNetworkPage).isFalse()
}
@Test fun showMobileNetworkPage_noTelephony_returnFalse() {
mockUserManager.stub {
on { isAdminUser } doReturn true
}
mockPackageManager.stub {
on { hasSystemFeature(PackageManager.FEATURE_TELEPHONY) } doReturn false
}
val showMobileNetworkPage = repository.showMobileNetworkPage()
assertThat(showMobileNetworkPage).isFalse()
}
}

View File

@@ -1,187 +0,0 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.network;
import static androidx.lifecycle.Lifecycle.Event;
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 static org.mockito.Mockito.when;
import android.content.Context;
import android.os.Looper;
import android.os.UserManager;
import android.provider.Settings;
import android.provider.Settings.Global;
import android.telephony.PhoneStateListener;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LifecycleRegistry;
import androidx.preference.Preference;
import androidx.preference.PreferenceManager;
import androidx.preference.PreferenceScreen;
import androidx.test.annotation.UiThreadTest;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
import com.android.settingslib.RestrictedPreference;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@RunWith(AndroidJUnit4.class)
public class MobileNetworkPreferenceControllerTest {
private Context mContext;
@Mock
private TelephonyManager mTelephonyManager;
@Mock
private SubscriptionManager mSubscriptionManager;
@Mock
private UserManager mUserManager;
private PreferenceManager mPreferenceManager;
private PreferenceScreen mScreen;
@Mock
private LifecycleOwner mLifecycleOwner;
private LifecycleRegistry mLifecycleRegistry;
private MobileNetworkPreferenceController mController;
private Preference mPreference;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mContext = spy(ApplicationProvider.getApplicationContext());
when(mContext.getSystemService(Context.TELEPHONY_SERVICE)).thenReturn(mTelephonyManager);
when(mContext.getSystemService(SubscriptionManager.class)).thenReturn(mSubscriptionManager);
when(mSubscriptionManager.createForAllUserProfiles()).thenReturn(mSubscriptionManager);
when(mContext.getSystemService(UserManager.class)).thenReturn(mUserManager);
if (Looper.myLooper() == null) {
Looper.prepare();
}
mPreferenceManager = new PreferenceManager(mContext);
mScreen = mPreferenceManager.createPreferenceScreen(mContext);
mPreference = new Preference(mContext);
mPreference.setKey(MobileNetworkPreferenceController.KEY_MOBILE_NETWORK_SETTINGS);
mLifecycleRegistry = new LifecycleRegistry(mLifecycleOwner);
when(mLifecycleOwner.getLifecycle()).thenReturn(mLifecycleRegistry);
}
@Test
public void secondaryUser_prefIsNotAvailable() {
when(mUserManager.isAdminUser()).thenReturn(false);
when(mTelephonyManager.isDataCapable()).thenReturn(true);
mController = new MobileNetworkPreferenceController(mContext);
assertThat(mController.isAvailable()).isFalse();
}
@Test
public void wifiOnly_prefIsNotAvailable() {
when(mUserManager.isAdminUser()).thenReturn(true);
when(mTelephonyManager.isDataCapable()).thenReturn(false);
mController = new MobileNetworkPreferenceController(mContext);
assertThat(mController.isAvailable()).isFalse();
}
@Test
@UiThreadTest
public void goThroughLifecycle_isAvailable_shouldListenToServiceChange() {
mController = spy(new MobileNetworkPreferenceController(mContext));
mLifecycleRegistry.addObserver(mController);
doReturn(true).when(mController).isAvailable();
mLifecycleRegistry.handleLifecycleEvent(Event.ON_START);
verify(mController).onStart();
verify(mTelephonyManager).registerTelephonyCallback(
mContext.getMainExecutor(), mController.mTelephonyCallback);
mLifecycleRegistry.handleLifecycleEvent(Event.ON_STOP);
verify(mController).onStop();
verify(mTelephonyManager).unregisterTelephonyCallback(mController.mTelephonyCallback);
}
@Test
@UiThreadTest
public void serviceStateChange_shouldUpdatePrefSummary() {
final String testCarrierName = "test";
mController = spy(new MobileNetworkPreferenceController(mContext));
mLifecycleRegistry.addObserver(mController);
doReturn(true).when(mController).isAvailable();
mScreen.addPreference(mPreference);
// Display pref and go through lifecycle to set up listener.
mController.displayPreference(mScreen);
mLifecycleRegistry.handleLifecycleEvent(Event.ON_START);
verify(mController).onStart();
verify(mTelephonyManager).registerTelephonyCallback(
mContext.getMainExecutor(), mController.mTelephonyCallback);
doReturn(testCarrierName).when(mController).getSummary();
mController.mTelephonyCallback.onServiceStateChanged(null);
// Carrier name should be set.
Assert.assertEquals(mPreference.getSummary(), testCarrierName);
}
@Test
public void airplaneModeTurnedOn_shouldDisablePreference() {
Settings.Global.putInt(mContext.getContentResolver(),
Global.AIRPLANE_MODE_ON, 1);
mController = spy(new MobileNetworkPreferenceController(mContext));
final RestrictedPreference mPreference = new RestrictedPreference(mContext);
mController.updateState(mPreference);
assertThat(mPreference.isEnabled()).isFalse();
}
@Test
public void airplaneModeTurnedOffAndNoUserRestriction_shouldEnablePreference() {
Settings.Global.putInt(mContext.getContentResolver(),
Global.AIRPLANE_MODE_ON, 0);
mController = spy(new MobileNetworkPreferenceController(mContext));
final RestrictedPreference mPreference = new RestrictedPreference(mContext);
mPreference.setDisabledByAdmin(null);
mController.updateState(mPreference);
assertThat(mPreference.isEnabled()).isTrue();
}
@Test
public void airplaneModeTurnedOffAndHasUserRestriction_shouldDisablePreference() {
Settings.Global.putInt(mContext.getContentResolver(),
Global.AIRPLANE_MODE_ON, 0);
mController = spy(new MobileNetworkPreferenceController(mContext));
final RestrictedPreference mPreference = new RestrictedPreference(mContext);
mPreference.setDisabledByAdmin(EnforcedAdmin.MULTIPLE_ENFORCED_ADMIN);
mController.updateState(mPreference);
assertThat(mPreference.isEnabled()).isFalse();
}
}