Snap for 13174046 from 305a8e0ea7 to 25Q2-release

Change-Id: Ib56f1d75610e715634a3d0e9dc0697182123e995
This commit is contained in:
Android Build Coastguard Worker
2025-03-06 16:21:26 -08:00
27 changed files with 1055 additions and 356 deletions

View File

@@ -21,9 +21,9 @@
android:padding="?android:attr/dialogPreferredPadding">
<TextView
android:id="@+id/accessibility_dialog_header_text_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/accessibility_magnification_area_settings_message"
android:textSize="16sp"
style="?android:attr/textAppearanceMedium"
android:textColor="?android:attr/textColorAlertDialogListItem"/>

View File

@@ -53,6 +53,8 @@
android:id="@+id/name_layout"
android:hint="@string/vpn_name"
app:endIconMode="clear_text"
app:helperTextEnabled="true"
app:helperText="@string/vpn_required"
app:errorEnabled="true">
<com.google.android.material.textfield.TextInputEditText
style="@style/vpn_value"
@@ -73,6 +75,8 @@
android:id="@+id/server_layout"
android:hint="@string/vpn_server"
app:endIconMode="clear_text"
app:helperTextEnabled="true"
app:helperText="@string/vpn_required"
app:errorEnabled="true">
<com.google.android.material.textfield.TextInputEditText
style="@style/vpn_value"
@@ -90,7 +94,7 @@
android:hint="@string/vpn_ipsec_identifier"
app:endIconMode="clear_text"
app:helperTextEnabled="true"
app:helperText="@string/vpn_not_used"
app:helperText="@string/vpn_required"
app:errorEnabled="true">
<com.google.android.material.textfield.TextInputEditText
style="@style/vpn_value"
@@ -108,6 +112,8 @@
android:id="@+id/ipsec_secret_layout"
android:hint="@string/vpn_ipsec_secret"
app:endIconMode="password_toggle"
app:helperTextEnabled="true"
app:helperText="@string/vpn_required"
app:errorEnabled="true">
<com.google.android.material.textfield.TextInputEditText
style="@style/vpn_value"
@@ -183,7 +189,7 @@
android:hint="@string/proxy_hostname_label"
app:endIconMode="clear_text"
app:helperTextEnabled="true"
app:helperText="@string/proxy_hostname_hint"
app:helperText="@string/vpn_optional"
app:errorEnabled="true">
<com.google.android.material.textfield.TextInputEditText
style="@style/vpn_value"
@@ -197,7 +203,7 @@
android:hint="@string/proxy_port_label"
app:endIconMode="clear_text"
app:helperTextEnabled="true"
app:helperText="@string/proxy_port_hint"
app:helperText="@string/vpn_optional"
app:errorEnabled="true">
<com.google.android.material.textfield.TextInputEditText
style="@style/vpn_value"
@@ -217,6 +223,8 @@
android:id="@+id/username_layout"
android:hint="@string/vpn_username"
app:endIconMode="clear_text"
app:helperTextEnabled="true"
app:helperText="@string/vpn_optional"
app:errorEnabled="true">
<com.google.android.material.textfield.TextInputEditText
style="@style/vpn_value"
@@ -228,6 +236,8 @@
android:id="@+id/password_layout"
android:hint="@string/vpn_password"
app:endIconMode="password_toggle"
app:helperTextEnabled="true"
app:helperText="@string/vpn_optional"
app:errorEnabled="true">
<com.google.android.material.textfield.TextInputEditText
style="@style/vpn_value"

View File

@@ -5220,6 +5220,16 @@
<string name="accessibility_screen_magnification_title">Magnification</string>
<!-- Title for accessibility shortcut preference for magnification. [CHAR LIMIT=60] -->
<string name="accessibility_screen_magnification_shortcut_title">Magnification shortcut</string>
<!-- Title of cursor following mode preference for magnification. [CHAR LIMIT=60] -->
<string name="accessibility_magnification_cursor_following_title">Cursor following</string>
<!-- Header message of cursor following mode dialog for magnification. [CHAR LIMIT=none] -->
<string name="accessibility_magnification_cursor_following_header">Choose how Magnification follows your cursor.</string>
<!-- Option title of cursor following continuous mode in the mode selection dialog. [CHAR LIMIT=none] -->
<string name="accessibility_magnification_cursor_following_continuous">Move screen continuously as mouse moves</string>
<!-- Option title of cursor following center mode in the mode selection dialog. [CHAR LIMIT=none] -->
<string name="accessibility_magnification_cursor_following_center">Move screen keeping mouse at center of screen</string>
<!-- Option title of cursor following edge mode in the mode selection dialog. [CHAR LIMIT=none] -->
<string name="accessibility_magnification_cursor_following_edge">Move screen when mouse touches edges of screen</string>
<!-- Title for accessibility follow typing preference for magnification. [CHAR LIMIT=35] -->
<string name="accessibility_screen_magnification_follow_typing_title">Magnify typing</string>
<!-- Summary for accessibility follow typing preference for magnification. [CHAR LIMIT=none] -->
@@ -6601,8 +6611,12 @@
<string name="battery_usage_timestamps_content_description"><xliff:g id="from_timestamp">%1$s</xliff:g> to <xliff:g id="to_timestamp">%2$s</xliff:g></string>
<!-- [CHAR_LIMIT=NONE] The first slot is a week day (e.g. "Monday"); the second slot is a hourly time span (e.g. "6 AM - 8 AM"). -->
<string name="battery_usage_day_and_hour"><xliff:g id="day">%1$s</xliff:g> <xliff:g id="hour">%2$s</xliff:g></string>
<!-- [CHAR_LIMIT=NONE] Accessibility content description for each slot in battery chart view. -->
<string name="battery_usage_time_info_and_battery_level"><xliff:g id="time_info" example="Battery usage for Monday 6 AM - 8 AM">%1$s</xliff:g> <xliff:g id="battery_level" example="Battery level percentage from 83% to 59%">%2$s</xliff:g></string>
<!-- [CHAR_LIMIT=NONE] Accessibility content description for each slot in battery chart view. Please reuse the words in tc/6732629268310936155 -->
<string name="battery_usage_status_time_info_and_battery_level"><xliff:g id="selected_status" example="Selected">%1$s</xliff:g>, <xliff:g id="time_info" example="Battery usage for Monday 6 AM - 8 AM">%2$s</xliff:g> <xliff:g id="battery_level" example="Battery level percentage from 83% to 59%">%3$s</xliff:g></string>
<!-- [CHAR_LIMIT=NONE] Accessibility content description for the battery usage slot is selected. -->
<string name="battery_chart_slot_status_selected">Selected</string>
<!-- [CHAR_LIMIT=NONE] Accessibility content description for the battery usage slot is not selected -->
<string name="battery_chart_slot_status_unselected">Unselected</string>
<!-- [CHAR_LIMIT=NONE] Accessibility content description for battery chart view. -->
<string name="battery_usage_chart">Battery usage chart</string>
<!-- [CHAR_LIMIT=NONE] Accessibility content description for daily battery chart view. -->
@@ -7276,6 +7290,12 @@ Data usage charges may apply.</string>
generic error. [CHAR LIMIT=120] -->
<string name="vpn_always_on_invalid_reason_other">The information entered doesn\'t support
always-on VPN</string>
<!-- Hint for an optional field in a VPN profile. [CHAR LIMIT=40] -->
<string name="vpn_optional">(optional)</string>
<!-- Hint for a required field in a VPN profile. [CHAR LIMIT=40] -->
<string name="vpn_required">(required)</string>
<!-- Error message displayed below the VPN EditText when the filed is required. [CHAR LIMIT=NONE] -->
<string name="vpn_field_required">The field is required</string>
<!-- Button label to cancel changing a VPN profile. [CHAR LIMIT=40] -->
<string name="vpn_cancel">Cancel</string>
@@ -12924,12 +12944,12 @@ Data usage charges may apply.</string>
<!-- Title for a toggle that enables freeform windows. Freeform windows enables users to freely arrange and resize overlapping apps. [CHAR LIMIT=50] -->
<string name="enable_desktop_mode">Enable freeform windows</string>
<!-- Title for a toggle that enables desktop experience features. This includes desktop view and connected displays. [CHAR LIMIT=50] -->
<!-- Title for a toggle that enables desktop experience features. This includes desktop windowing and connected displays. [CHAR LIMIT=50] -->
<string name="enable_desktop_experience_features">Enable desktop experience features</string>
<!-- Summary for a toggle that enables desktop experience features when the device itself can show the desktop (but it is not available without the developer option). [CHAR LIMIT=NONE] -->
<string name="enable_desktop_experience_features_summary_with_desktop">Enable Desktop View on the device and on secondary displays.</string>
<!-- Summary for a toggle that enables desktop experience features when desktop views don't need to be enable. [CHAR LIMIT=NONE] -->
<string name="enable_desktop_experience_features_summary_without_desktop">Enable Desktop View on secondary displays.</string>
<string name="enable_desktop_experience_features_summary_with_desktop">Enable desktop windowing on the device and on secondary displays.</string>
<!-- Summary for a toggle that enables desktop experience features when desktop windowing doesn't need to be enabled. [CHAR LIMIT=NONE] -->
<string name="enable_desktop_experience_features_summary_without_desktop">Enable desktop windowing on secondary displays.</string>
<!-- Title for a toggle that enables freeform windows on secondary display. Freeform windows enables users to freely arrange and resize overlapping apps. [CHAR LIMIT=50] -->
<string name="enable_desktop_mode_on_secondary_display">Enable freeform windows on secondary display</string>
@@ -14283,4 +14303,6 @@ Data usage charges may apply.</string>
<string name="supervision_add_forgot_pin_preference_title">Forgot PIN</string>
<!-- Title for web content filters entry [CHAR LIMIT=60] -->
<string name="supervision_web_content_filters_title">Web content filters</string>
<!-- Generic content description that is attached to the preview illustration at the top of an Accessibility feature toggle page. [CHAR LIMIT=NONE] -->
<string name="accessibility_illustration_content_description"><xliff:g id="feature" example="Select to Speak">%1$s</xliff:g> animation</string>
</resources>

View File

@@ -104,6 +104,11 @@ public class AccessibilityDialogUtils {
* screen / Switch between full and partial screen > Save.
*/
int DIALOG_MAGNIFICATION_TRIPLE_TAP_WARNING = 1011;
/**
* OPEN: Settings > Accessibility > Magnification > Cursor following.
*/
int DIALOG_MAGNIFICATION_CURSOR_FOLLOWING_MODE = 1012;
}
/**

View File

@@ -0,0 +1,221 @@
/*
* Copyright (C) 2025 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.accessibility;
import android.app.Dialog;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.content.DialogInterface;
import android.provider.Settings;
import android.provider.Settings.Secure.AccessibilityMagnificationCursorFollowingMode;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.core.util.Preconditions;
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
import com.android.settings.DialogCreatable;
import com.android.settings.R;
import com.android.settings.accessibility.AccessibilityDialogUtils.DialogEnums;
import com.android.settings.core.BasePreferenceController;
import java.util.ArrayList;
import java.util.List;
/**
* Controller that shows the magnification cursor following mode and the preference click behavior.
*/
public class MagnificationCursorFollowingModePreferenceController extends
BasePreferenceController implements DialogCreatable {
static final String PREF_KEY =
Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE;
private static final String TAG =
MagnificationCursorFollowingModePreferenceController.class.getSimpleName();
private final List<ModeInfo> mModeList = new ArrayList<>();
@Nullable
private DialogHelper mDialogHelper;
@VisibleForTesting
@Nullable
ListView mModeListView;
@Nullable
private Preference mModePreference;
public MagnificationCursorFollowingModePreferenceController(@NonNull Context context,
@NonNull String preferenceKey) {
super(context, preferenceKey);
initModeList();
}
public void setDialogHelper(@NonNull DialogHelper dialogHelper) {
mDialogHelper = dialogHelper;
}
private void initModeList() {
mModeList.add(new ModeInfo(mContext.getString(
R.string.accessibility_magnification_cursor_following_continuous),
Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_CONTINUOUS));
mModeList.add(new ModeInfo(
mContext.getString(R.string.accessibility_magnification_cursor_following_center),
Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_CENTER));
mModeList.add(new ModeInfo(
mContext.getString(R.string.accessibility_magnification_cursor_following_edge),
Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_EDGE));
}
@Override
public int getAvailabilityStatus() {
return AVAILABLE;
}
@NonNull
@Override
public CharSequence getSummary() {
return getCursorFollowingModeSummary(getCurrentMagnificationCursorFollowingMode());
}
@Override
public void displayPreference(@NonNull PreferenceScreen screen) {
super.displayPreference(screen);
mModePreference = screen.findPreference(getPreferenceKey());
}
@Override
public boolean handlePreferenceTreeClick(@NonNull Preference preference) {
if (!TextUtils.equals(preference.getKey(), getPreferenceKey()) || mModePreference == null) {
return super.handlePreferenceTreeClick(preference);
}
Preconditions.checkNotNull(mDialogHelper).showDialog(
DialogEnums.DIALOG_MAGNIFICATION_CURSOR_FOLLOWING_MODE);
return true;
}
@NonNull
@Override
public Dialog onCreateDialog(int dialogId) {
Preconditions.checkArgument(
dialogId == DialogEnums.DIALOG_MAGNIFICATION_CURSOR_FOLLOWING_MODE,
"This only handles cursor following mode dialog");
return createMagnificationCursorFollowingModeDialog();
}
@Override
public int getDialogMetricsCategory(int dialogId) {
Preconditions.checkArgument(
dialogId == DialogEnums.DIALOG_MAGNIFICATION_CURSOR_FOLLOWING_MODE,
"This only handles cursor following mode dialog");
return SettingsEnums.DIALOG_MAGNIFICATION_CURSOR_FOLLOWING;
}
@NonNull
private Dialog createMagnificationCursorFollowingModeDialog() {
mModeListView = AccessibilityDialogUtils.createSingleChoiceListView(mContext, mModeList,
/* itemListener= */null);
final View headerView = LayoutInflater.from(mContext).inflate(
R.layout.accessibility_dialog_header, mModeListView,
/* attachToRoot= */false);
final TextView textView = Preconditions.checkNotNull(headerView.findViewById(
R.id.accessibility_dialog_header_text_view));
textView.setText(
mContext.getString(R.string.accessibility_magnification_cursor_following_header));
textView.setVisibility(View.VISIBLE);
mModeListView.addHeaderView(headerView, /* data= */null, /* isSelectable= */false);
final int selectionIndex = computeSelectionIndex();
if (selectionIndex != AdapterView.INVALID_POSITION) {
mModeListView.setItemChecked(selectionIndex, /* value= */true);
}
final CharSequence title = mContext.getString(
R.string.accessibility_magnification_cursor_following_title);
final CharSequence positiveBtnText = mContext.getString(R.string.save);
final CharSequence negativeBtnText = mContext.getString(R.string.cancel);
return AccessibilityDialogUtils.createCustomDialog(mContext, title, mModeListView,
positiveBtnText,
this::onMagnificationCursorFollowingModeDialogPositiveButtonClicked,
negativeBtnText, /* negativeListener= */null);
}
void onMagnificationCursorFollowingModeDialogPositiveButtonClicked(
DialogInterface dialogInterface, int which) {
ListView listView = Preconditions.checkNotNull(mModeListView);
final int selectionIndex = listView.getCheckedItemPosition();
if (selectionIndex == AdapterView.INVALID_POSITION) {
Log.w(TAG, "Selected positive button with INVALID_POSITION index");
return;
}
ModeInfo cursorFollowingMode = (ModeInfo) listView.getItemAtPosition(selectionIndex);
if (cursorFollowingMode != null) {
Preconditions.checkNotNull(mModePreference).setSummary(
getCursorFollowingModeSummary(cursorFollowingMode.mMode));
Settings.Secure.putInt(mContext.getContentResolver(), PREF_KEY,
cursorFollowingMode.mMode);
}
}
private int computeSelectionIndex() {
ListView listView = Preconditions.checkNotNull(mModeListView);
@AccessibilityMagnificationCursorFollowingMode
final int currentMode = getCurrentMagnificationCursorFollowingMode();
for (int i = 0; i < listView.getCount(); i++) {
final ModeInfo mode = (ModeInfo) listView.getItemAtPosition(i);
if (mode != null && mode.mMode == currentMode) {
return i;
}
}
return AdapterView.INVALID_POSITION;
}
@NonNull
private CharSequence getCursorFollowingModeSummary(
@AccessibilityMagnificationCursorFollowingMode int cursorFollowingMode) {
int stringId = switch (cursorFollowingMode) {
case Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_CENTER ->
R.string.accessibility_magnification_cursor_following_center;
case Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_EDGE ->
R.string.accessibility_magnification_cursor_following_edge;
default ->
R.string.accessibility_magnification_cursor_following_continuous;
};
return mContext.getString(stringId);
}
private @AccessibilityMagnificationCursorFollowingMode int
getCurrentMagnificationCursorFollowingMode() {
return Settings.Secure.getInt(mContext.getContentResolver(), PREF_KEY,
Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_CONTINUOUS);
}
static class ModeInfo extends ItemInfoArrayAdapter.ItemInfo {
@AccessibilityMagnificationCursorFollowingMode
public final int mMode;
ModeInfo(@NonNull CharSequence title,
@AccessibilityMagnificationCursorFollowingMode int mode) {
super(title, /* summary= */null, /* drawableId= */null);
mMode = mode;
}
}
}

View File

@@ -176,8 +176,12 @@ public class MagnificationModePreferenceController extends BasePreferenceControl
mContext, mModeInfos, this::onMagnificationModeSelected);
final View headerView = LayoutInflater.from(mContext).inflate(
R.layout.accessibility_magnification_mode_header,
getMagnificationModesListView(), /* attachToRoot= */false);
R.layout.accessibility_dialog_header, getMagnificationModesListView(),
/* attachToRoot= */false);
final TextView textView = Preconditions.checkNotNull(headerView.findViewById(
R.id.accessibility_dialog_header_text_view));
textView.setText(
mContext.getString(R.string.accessibility_magnification_area_settings_message));
getMagnificationModesListView().addHeaderView(headerView, /* data= */null,
/* isSelectable= */false);

View File

@@ -68,6 +68,7 @@ import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.flags.Flags;
import com.android.settings.widget.SettingsMainSwitchBar;
import com.android.settings.widget.SettingsMainSwitchPreference;
import com.android.settingslib.utils.ThreadUtils;
import com.android.settingslib.widget.IllustrationPreference;
import com.android.settingslib.widget.TopIntroPreference;
@@ -311,6 +312,11 @@ public abstract class ToggleFeaturePreferenceFragment extends DashboardFragment
return getString(R.string.accessibility_shortcut_title, mFeatureName);
}
@VisibleForTesting
CharSequence getContentDescriptionForAnimatedIllustration() {
return getString(R.string.accessibility_illustration_content_description, mFeatureName);
}
protected void onPreferenceToggled(String preferenceKey, boolean enabled) {
}
@@ -427,22 +433,38 @@ public abstract class ToggleFeaturePreferenceFragment extends DashboardFragment
return drawable;
}
private void initAnimatedImagePreference() {
if (mImageUri == null) {
initAnimatedImagePreference(mImageUri, new IllustrationPreference(getPrefContext()));
}
@VisibleForTesting
void initAnimatedImagePreference(
@Nullable Uri imageUri,
@NonNull IllustrationPreference preference) {
if (imageUri == null) {
return;
}
final int displayHalfHeight =
AccessibilityUtil.getDisplayBounds(getPrefContext()).height() / 2;
final IllustrationPreference illustrationPreference =
new IllustrationPreference(getPrefContext());
illustrationPreference.setImageUri(mImageUri);
illustrationPreference.setSelectable(false);
illustrationPreference.setMaxHeight(displayHalfHeight);
illustrationPreference.setKey(KEY_ANIMATED_IMAGE);
getPreferenceScreen().addPreference(illustrationPreference);
preference.setImageUri(imageUri);
preference.setSelectable(false);
preference.setMaxHeight(displayHalfHeight);
preference.setKey(KEY_ANIMATED_IMAGE);
preference.setOnBindListener(view -> {
// isAnimatable is decided in
// {@link IllustrationPreference#onBindViewHolder(PreferenceViewHolder)}. Therefore, we
// wait until the view is bond to set the content description for it.
// The content description is added for an animation illustration only. Since the static
// images are decorative.
ThreadUtils.getUiThreadHandler().post(() -> {
if (preference.isAnimatable()) {
preference.setContentDescription(
getContentDescriptionForAnimatedIllustration());
}
});
});
getPreferenceScreen().addPreference(preference);
}
@VisibleForTesting

View File

@@ -93,6 +93,8 @@ public class ToggleScreenMagnificationPreferenceFragment extends
private TouchExplorationStateChangeListener mTouchExplorationStateChangeListener;
@Nullable
private DialogCreatable mMagnificationModeDialogDelegate;
@Nullable
private DialogCreatable mMagnificationCursorFollowingModeDialogDelegate;
@Nullable
MagnificationOneFingerPanningPreferenceController mOneFingerPanningPreferenceController;
@@ -104,6 +106,12 @@ public class ToggleScreenMagnificationPreferenceFragment extends
mMagnificationModeDialogDelegate = delegate;
}
@VisibleForTesting
public void setMagnificationCursorFollowingModeDialogDelegate(
@NonNull DialogCreatable delegate) {
mMagnificationCursorFollowingModeDialogDelegate = delegate;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -186,6 +194,9 @@ public class ToggleScreenMagnificationPreferenceFragment extends
case DialogEnums.DIALOG_MAGNIFICATION_TRIPLE_TAP_WARNING:
return Preconditions.checkNotNull(mMagnificationModeDialogDelegate)
.onCreateDialog(dialogId);
case DialogEnums.DIALOG_MAGNIFICATION_CURSOR_FOLLOWING_MODE:
return Preconditions.checkNotNull(mMagnificationCursorFollowingModeDialogDelegate)
.onCreateDialog(dialogId);
case DialogEnums.GESTURE_NAVIGATION_TUTORIAL:
return AccessibilityShortcutsTutorial
.showAccessibilityGestureTutorialDialog(getPrefContext());
@@ -201,6 +212,11 @@ public class ToggleScreenMagnificationPreferenceFragment extends
PackageManager.FEATURE_WINDOW_MAGNIFICATION);
}
private static boolean isMagnificationCursorFollowingModeDialogSupported() {
// TODO(b/398066000): Hide the setting when no pointer device exists for most form factors.
return com.android.settings.accessibility.Flags.enableMagnificationCursorFollowingDialog();
}
@Override
protected void initSettingsPreference() {
final PreferenceCategory generalCategory = findPreference(KEY_GENERAL_CATEGORY);
@@ -213,6 +229,7 @@ public class ToggleScreenMagnificationPreferenceFragment extends
addJoystickSetting(generalCategory);
// LINT.ThenChange(:search_data)
}
addCursorFollowingSetting(generalCategory);
addFeedbackSetting(generalCategory);
}
@@ -286,6 +303,31 @@ public class ToggleScreenMagnificationPreferenceFragment extends
addPreferenceController(magnificationModePreferenceController);
}
private static Preference createCursorFollowingPreference(Context context) {
final Preference pref = new Preference(context);
pref.setTitle(R.string.accessibility_magnification_cursor_following_title);
pref.setKey(MagnificationCursorFollowingModePreferenceController.PREF_KEY);
pref.setPersistent(false);
return pref;
}
private void addCursorFollowingSetting(PreferenceCategory generalCategory) {
if (!isMagnificationCursorFollowingModeDialogSupported()) {
return;
}
generalCategory.addPreference(createCursorFollowingPreference(getPrefContext()));
final MagnificationCursorFollowingModePreferenceController controller =
new MagnificationCursorFollowingModePreferenceController(
getContext(),
MagnificationCursorFollowingModePreferenceController.PREF_KEY);
controller.setDialogHelper(/* dialogHelper= */this);
mMagnificationCursorFollowingModeDialogDelegate = controller;
controller.displayPreference(getPreferenceScreen());
addPreferenceController(controller);
}
private static Preference createFollowTypingPreference(Context context) {
final Preference pref = new SwitchPreferenceCompat(context);
pref.setTitle(R.string.accessibility_screen_magnification_follow_typing_title);
@@ -510,6 +552,9 @@ public class ToggleScreenMagnificationPreferenceFragment extends
case DialogEnums.DIALOG_MAGNIFICATION_TRIPLE_TAP_WARNING:
return Preconditions.checkNotNull(mMagnificationModeDialogDelegate)
.getDialogMetricsCategory(dialogId);
case DialogEnums.DIALOG_MAGNIFICATION_CURSOR_FOLLOWING_MODE:
return Preconditions.checkNotNull(mMagnificationCursorFollowingModeDialogDelegate)
.getDialogMetricsCategory(dialogId);
case DialogEnums.GESTURE_NAVIGATION_TUTORIAL:
return SettingsEnums.DIALOG_TOGGLE_SCREEN_MAGNIFICATION_GESTURE_NAVIGATION;
case DialogEnums.ACCESSIBILITY_BUTTON_TUTORIAL:
@@ -667,6 +712,11 @@ public class ToggleScreenMagnificationPreferenceFragment extends
return rawData;
}
// Add all preferences to search raw data so that they are included in
// indexing, which happens infrequently. Irrelevant preferences should be
// hidden from the live returned search results by `getNonIndexableKeys`,
// which is called every time a search occurs. This allows for dynamic search
// entries that hide or show depending on current device state.
rawData.add(createShortcutPreferenceSearchData(context));
Stream.of(
createMagnificationModePreference(context),
@@ -674,6 +724,7 @@ public class ToggleScreenMagnificationPreferenceFragment extends
createOneFingerPanningPreference(context),
createAlwaysOnPreference(context),
createJoystickPreference(context),
createCursorFollowingPreference(context),
createFeedbackPreference(context)
)
.forEach(pref ->
@@ -714,6 +765,10 @@ public class ToggleScreenMagnificationPreferenceFragment extends
}
}
if (!isMagnificationCursorFollowingModeDialogSupported()) {
niks.add(MagnificationCursorFollowingModePreferenceController.PREF_KEY);
}
if (!Flags.enableLowVisionHats()) {
niks.add(MagnificationFeedbackPreferenceController.PREF_KEY);
}

View File

@@ -34,7 +34,10 @@ import android.media.MediaMetadata;
import android.media.session.MediaSession;
import android.media.session.PlaybackState;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Process;
import android.util.Log;
import android.view.KeyEvent;
@@ -51,24 +54,21 @@ import com.android.settingslib.bluetooth.BluetoothUtils;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.bluetooth.VolumeControlProfile;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import com.android.settingslib.utils.ThreadUtils;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.HashMap;
import java.util.Map;
public class AudioStreamMediaService extends Service {
static final String BROADCAST_ID = "audio_stream_media_service_broadcast_id";
static final String BROADCAST_TITLE = "audio_stream_media_service_broadcast_title";
static final String DEVICES = "audio_stream_media_service_devices";
private static final String TAG = "AudioStreamMediaService";
private static final int NOTIFICATION_ID = 1;
private static final int NOTIFICATION_ID = R.string.audio_streams_title;
private static final int BROADCAST_LISTENING_NOW_TEXT = R.string.audio_streams_listening_now;
private static final int BROADCAST_STREAM_PAUSED_TEXT = R.string.audio_streams_present_now;
@VisibleForTesting static final String LEAVE_BROADCAST_ACTION = "leave_broadcast_action";
@@ -113,17 +113,16 @@ public class AudioStreamMediaService extends Service {
private final MetricsFeatureProvider mMetricsFeatureProvider =
FeatureFactory.getFeatureFactory().getMetricsFeatureProvider();
private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
private final AtomicBoolean mIsMuted = new AtomicBoolean(false);
private final AtomicBoolean mIsHysteresis = new AtomicBoolean(false);
private final HandlerThread mHandlerThread = new HandlerThread(TAG,
Process.THREAD_PRIORITY_BACKGROUND);
private boolean mIsMuted = false;
// Set 25 as default as the volume range from `VolumeControlProfile` is from 0 to 255.
// If the initial volume from `onDeviceVolumeChanged` is larger than zero (not muted), we will
// override this value. Otherwise, we raise the volume to 25 when the play button is clicked.
private final AtomicInteger mLatestPositiveVolume = new AtomicInteger(25);
private final Object mLocalSessionLock = new Object();
private int mLatestPositiveVolume = 25;
private boolean mHysteresisModeFixAvailable;
private int mBroadcastId;
@Nullable private List<BluetoothDevice> mDevices;
@Nullable private Map<BluetoothDevice, LocalBluetoothLeBroadcastSourceState> mStateByDevice;
@Nullable private LocalBluetoothManager mLocalBtManager;
@Nullable private AudioStreamsHelper mAudioStreamsHelper;
@Nullable private LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
@@ -154,7 +153,6 @@ public class AudioStreamMediaService extends Service {
Log.w(TAG, "onCreate() : mLeBroadcastAssistant is null!");
return;
}
mHysteresisModeFixAvailable = BluetoothUtils.isAudioSharingHysteresisModeFixAvailable(this);
mNotificationManager = getSystemService(NotificationManager.class);
if (mNotificationManager == null) {
@@ -162,7 +160,8 @@ public class AudioStreamMediaService extends Service {
return;
}
mExecutor.execute(
mHandlerThread.start();
getHandler().post(
() -> {
if (mLocalBtManager == null
|| mLeBroadcastAssistant == null
@@ -184,45 +183,49 @@ public class AudioStreamMediaService extends Service {
mVolumeControl = mLocalBtManager.getProfileManager().getVolumeControlProfile();
if (mVolumeControl != null) {
mVolumeControlCallback = new VolumeControlCallback();
mVolumeControl.registerCallback(mExecutor, mVolumeControlCallback);
mVolumeControl.registerCallback(getHandler()::post, mVolumeControlCallback);
}
mBroadcastAssistantCallback = new AssistantCallback();
mLeBroadcastAssistant.registerServiceCallBack(
mExecutor, mBroadcastAssistantCallback);
getHandler()::post, mBroadcastAssistantCallback);
mHysteresisModeFixAvailable =
BluetoothUtils.isAudioSharingHysteresisModeFixAvailable(this);
});
}
@VisibleForTesting
Handler getHandler() {
return mHandlerThread.getThreadHandler();
}
@Override
public void onDestroy() {
Log.d(TAG, "onDestroy()");
super.onDestroy();
if (BluetoothUtils.isAudioSharingUIAvailable(this)) {
if (mDevices != null) {
mDevices.clear();
mDevices = null;
}
synchronized (mLocalSessionLock) {
if (mLocalSession != null) {
mLocalSession.release();
mLocalSession = null;
}
}
mExecutor.execute(
() -> {
if (mLocalBtManager != null) {
mLocalBtManager.getEventManager().unregisterCallback(
mBluetoothCallback);
}
if (mLeBroadcastAssistant != null && mBroadcastAssistantCallback != null) {
mLeBroadcastAssistant.unregisterServiceCallBack(
mBroadcastAssistantCallback);
}
if (mVolumeControl != null && mVolumeControlCallback != null) {
mVolumeControl.unregisterCallback(mVolumeControlCallback);
}
});
}
getHandler().post(
() -> {
if (mStateByDevice != null) {
mStateByDevice.clear();
mStateByDevice = null;
}
if (mLocalSession != null) {
mLocalSession.release();
mLocalSession = null;
}
if (mLocalBtManager != null) {
mLocalBtManager.getEventManager().unregisterCallback(
mBluetoothCallback);
}
if (mLeBroadcastAssistant != null && mBroadcastAssistantCallback != null) {
mLeBroadcastAssistant.unregisterServiceCallBack(
mBroadcastAssistantCallback);
}
if (mVolumeControl != null && mVolumeControlCallback != null) {
mVolumeControl.unregisterCallback(mVolumeControlCallback);
}
});
mHandlerThread.quitSafely();
}
@Override
@@ -233,53 +236,59 @@ public class AudioStreamMediaService extends Service {
stopSelf();
return START_NOT_STICKY;
}
mBroadcastId = intent.getIntExtra(BROADCAST_ID, -1);
if (mBroadcastId == -1) {
Log.w(TAG, "Invalid broadcast ID. Service will not start.");
stopSelf();
return START_NOT_STICKY;
}
var extra = intent.getParcelableArrayListExtra(DEVICES, BluetoothDevice.class);
if (extra == null || extra.isEmpty()) {
Log.w(TAG, "No device. Service will not start.");
stopSelf();
return START_NOT_STICKY;
}
mDevices = Collections.synchronizedList(extra);
MediaSession.Token token =
getOrCreateLocalMediaSession(intent.getStringExtra(BROADCAST_TITLE));
startForeground(NOTIFICATION_ID, buildNotification(token));
getHandler().post(() -> {
mBroadcastId = intent.getIntExtra(BROADCAST_ID, -1);
if (mBroadcastId == -1) {
Log.w(TAG, "Invalid broadcast ID. Service will not start.");
stopSelf();
return;
}
var devices = intent.getParcelableArrayListExtra(DEVICES, BluetoothDevice.class);
if (devices == null || devices.isEmpty()) {
Log.w(TAG, "No device. Service will not start.");
stopSelf();
} else {
mStateByDevice = new HashMap<>();
devices.forEach(d -> mStateByDevice.put(d, STREAMING));
MediaSession.Token token =
getOrCreateLocalMediaSession(intent.getStringExtra(BROADCAST_TITLE));
startForeground(NOTIFICATION_ID, buildNotification(token));
}
});
return START_NOT_STICKY;
}
private MediaSession.Token getOrCreateLocalMediaSession(String title) {
synchronized (mLocalSessionLock) {
if (mLocalSession != null) {
return mLocalSession.getSessionToken();
}
mLocalSession = new MediaSession(this, TAG);
mLocalSession.setMetadata(
new MediaMetadata.Builder()
.putString(MediaMetadata.METADATA_KEY_TITLE, title)
.putLong(MediaMetadata.METADATA_KEY_DURATION, STATIC_PLAYBACK_DURATION)
.build());
mLocalSession.setActive(true);
mLocalSession.setPlaybackState(getPlaybackState());
mMediaSessionCallback = new MediaSessionCallback();
mLocalSession.setCallback(mMediaSessionCallback);
if (mLocalSession != null) {
return mLocalSession.getSessionToken();
}
mLocalSession = new MediaSession(this, TAG);
mLocalSession.setMetadata(
new MediaMetadata.Builder()
.putString(MediaMetadata.METADATA_KEY_TITLE, title)
.putLong(MediaMetadata.METADATA_KEY_DURATION, STATIC_PLAYBACK_DURATION)
.build());
mLocalSession.setActive(true);
mLocalSession.setPlaybackState(getPlaybackState());
mMediaSessionCallback = new MediaSessionCallback();
mLocalSession.setCallback(mMediaSessionCallback, getHandler());
return mLocalSession.getSessionToken();
}
private PlaybackState getPlaybackState() {
if (mIsHysteresis.get()) {
if (isAllDeviceHysteresis()) {
return mPlayStateHysteresisBuilder.build();
}
return mIsMuted.get() ? mPlayStatePausingBuilder.build() : mPlayStatePlayingBuilder.build();
return mIsMuted ? mPlayStatePausingBuilder.build() : mPlayStatePlayingBuilder.build();
}
private boolean isAllDeviceHysteresis() {
return mHysteresisModeFixAvailable && mStateByDevice != null
&& mStateByDevice.values().stream().allMatch(v -> v == PAUSED);
}
private String getDeviceName() {
if (mDevices == null || mDevices.isEmpty() || mLocalBtManager == null) {
if (mStateByDevice == null || mStateByDevice.isEmpty() || mLocalBtManager == null) {
return DEFAULT_DEVICE_NAME;
}
@@ -288,7 +297,8 @@ public class AudioStreamMediaService extends Service {
return DEFAULT_DEVICE_NAME;
}
CachedBluetoothDevice device = manager.findDevice(mDevices.get(0));
CachedBluetoothDevice device = manager.findDevice(
mStateByDevice.keySet().iterator().next());
return device != null ? device.getName() : DEFAULT_DEVICE_NAME;
}
@@ -304,7 +314,7 @@ public class AudioStreamMediaService extends Service {
.setSmallIcon(com.android.settingslib.R.drawable.ic_bt_le_audio_sharing)
.setStyle(mediaStyle)
.setContentText(getString(
mIsHysteresis.get() ? BROADCAST_STREAM_PAUSED_TEXT :
isAllDeviceHysteresis() ? BROADCAST_STREAM_PAUSED_TEXT :
BROADCAST_LISTENING_NOW_TEXT))
.setSilent(true);
return notificationBuilder.build();
@@ -333,7 +343,8 @@ public class AudioStreamMediaService extends Service {
public void onReceiveStateChanged(
BluetoothDevice sink, int sourceId, BluetoothLeBroadcastReceiveState state) {
super.onReceiveStateChanged(sink, sourceId, state);
if (!mHysteresisModeFixAvailable || mDevices == null || !mDevices.contains(sink)) {
if (!mHysteresisModeFixAvailable || mStateByDevice == null
|| !mStateByDevice.containsKey(sink)) {
return;
}
var sourceState = LocalBluetoothLeBroadcastAssistant.getLocalSourceState(state);
@@ -343,12 +354,10 @@ public class AudioStreamMediaService extends Service {
if (!streaming && !paused) {
return;
}
// Atomically update mIsHysteresis if its current value is not the current paused state
if (mIsHysteresis.compareAndSet(!paused, paused)) {
synchronized (mLocalSessionLock) {
if (mLocalSession == null) {
return;
}
boolean shouldUpdate = mStateByDevice.get(sink) != sourceState;
if (shouldUpdate) {
mStateByDevice.put(sink, sourceState);
if (mLocalSession != null) {
mLocalSession.setPlaybackState(getPlaybackState());
if (mNotificationManager != null) {
mNotificationManager.notify(
@@ -356,7 +365,7 @@ public class AudioStreamMediaService extends Service {
buildNotification(mLocalSession.getSessionToken())
);
}
Log.d(TAG, "updating hysteresis mode to : " + paused);
Log.d(TAG, "updating source state to : " + sourceState);
}
}
}
@@ -374,24 +383,22 @@ public class AudioStreamMediaService extends Service {
@Override
public void onDeviceVolumeChanged(
@NonNull BluetoothDevice device, @IntRange(from = -255, to = 255) int volume) {
if (mDevices == null || mDevices.isEmpty()) {
if (mStateByDevice == null || mStateByDevice.isEmpty()) {
Log.w(TAG, "active device or device has source is null!");
return;
}
Log.d(
TAG,
"onDeviceVolumeChanged() bluetoothDevice : " + device + " volume: " + volume);
if (mDevices.contains(device)) {
if (mStateByDevice.containsKey(device)) {
if (volume == 0) {
mIsMuted.set(true);
mIsMuted = true;
} else {
mIsMuted.set(false);
mLatestPositiveVolume.set(volume);
mIsMuted = false;
mLatestPositiveVolume = volume;
}
synchronized (mLocalSessionLock) {
if (mLocalSession != null) {
mLocalSession.setPlaybackState(getPlaybackState());
}
if (mLocalSession != null) {
mLocalSession.setPlaybackState(getPlaybackState());
}
}
}
@@ -400,10 +407,12 @@ public class AudioStreamMediaService extends Service {
private class BtCallback implements BluetoothCallback {
@Override
public void onBluetoothStateChanged(int bluetoothState) {
if (BluetoothAdapter.STATE_OFF == bluetoothState) {
Log.d(TAG, "onBluetoothStateChanged() : stopSelf");
stopSelf();
}
getHandler().post(() -> {
if (BluetoothAdapter.STATE_OFF == bluetoothState) {
Log.d(TAG, "onBluetoothStateChanged() : stopSelf");
stopSelf();
}
});
}
@Override
@@ -411,24 +420,17 @@ public class AudioStreamMediaService extends Service {
@NonNull CachedBluetoothDevice cachedDevice,
@ConnectionState int state,
int bluetoothProfile) {
if (state == BluetoothAdapter.STATE_DISCONNECTED
&& bluetoothProfile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT
&& mDevices != null) {
mDevices.remove(cachedDevice.getDevice());
cachedDevice
.getMemberDevice()
.forEach(
m -> {
// Check nullability to pass NullAway check
if (mDevices != null) {
mDevices.remove(m.getDevice());
}
});
}
if (mDevices == null || mDevices.isEmpty()) {
Log.d(TAG, "onProfileConnectionStateChanged() : stopSelf");
stopSelf();
}
getHandler().post(() -> {
if (state == BluetoothAdapter.STATE_DISCONNECTED
&& bluetoothProfile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT
&& mStateByDevice != null) {
mStateByDevice.remove(cachedDevice.getDevice());
}
if (mStateByDevice == null || mStateByDevice.isEmpty()) {
Log.d(TAG, "onProfileConnectionStateChanged() : stopSelf");
stopSelf();
}
});
}
}
@@ -454,10 +456,8 @@ public class AudioStreamMediaService extends Service {
@Override
public void onSeekTo(long pos) {
Log.d(TAG, "onSeekTo: " + pos);
synchronized (mLocalSessionLock) {
if (mLocalSession != null) {
mLocalSession.setPlaybackState(getPlaybackState());
}
if (mLocalSession != null) {
mLocalSession.setPlaybackState(getPlaybackState());
}
}
@@ -484,28 +484,26 @@ public class AudioStreamMediaService extends Service {
}
private void handleOnPlay() {
if (mDevices == null || mDevices.isEmpty()) {
if (mStateByDevice == null || mStateByDevice.isEmpty()) {
Log.w(TAG, "active device or device has source is null!");
return;
}
Log.d(
TAG,
"onPlay() setting volume for device : "
+ mDevices.getFirst()
+ " volume: "
+ mLatestPositiveVolume.get());
setDeviceVolume(mDevices.getFirst(), mLatestPositiveVolume.get());
mStateByDevice.keySet().forEach(device -> {
Log.d(TAG, "onPlay() setting volume for device : " + device + " volume: "
+ mLatestPositiveVolume);
setDeviceVolume(device, mLatestPositiveVolume);
});
}
private void handleOnPause() {
if (mDevices == null || mDevices.isEmpty()) {
if (mStateByDevice == null || mStateByDevice.isEmpty()) {
Log.w(TAG, "active device or device has source is null!");
return;
}
Log.d(
TAG,
"onPause() setting volume for device : " + mDevices.getFirst() + " volume: " + 0);
setDeviceVolume(mDevices.getFirst(), /* volume= */ 0);
mStateByDevice.keySet().forEach(device -> {
Log.d(TAG, "onPause() setting volume for device : " + device + " volume: " + 0);
setDeviceVolume(device, /* volume= */ 0);
});
}
private void setDeviceVolume(BluetoothDevice device, int volume) {
@@ -514,7 +512,7 @@ public class AudioStreamMediaService extends Service {
ThreadUtils.postOnBackgroundThread(
() -> {
if (mVolumeControl != null) {
mVolumeControl.setDeviceVolume(device, volume, true);
mVolumeControl.setDeviceVolume(device, volume, false);
mMetricsFeatureProvider.action(
getApplicationContext(), event, volume == 0 ? 1 : 0);
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright (C) 2025 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.display
import android.window.DesktopExperienceFlags.DesktopExperienceFlag
import com.android.settings.flags.FeatureFlags
/** Class handling Settings flags, but using the Desktop Experience developer option overrides. */
class DesktopExperienceFlags(private val featureFlagsImpl: FeatureFlags) : FeatureFlags by featureFlagsImpl {
private val displayTopologyPaneInDisplayListFlag =
DesktopExperienceFlag(
featureFlagsImpl::displayTopologyPaneInDisplayList,
/* shouldOverrideByDevOption= */ true,
)
override fun displayTopologyPaneInDisplayList(): Boolean =
displayTopologyPaneInDisplayListFlag.isTrue
private val displaySizeConnectedDisplaySettingFlag =
DesktopExperienceFlag(
featureFlagsImpl::displaySizeConnectedDisplaySetting,
/* shouldOverrideByDevOption= */ true,
)
override fun displaySizeConnectedDisplaySetting(): Boolean =
displaySizeConnectedDisplaySettingFlag.isTrue
}

View File

@@ -109,7 +109,8 @@ public class ExternalDisplaySettingsConfiguration {
private final Handler mHandler;
Injector(@Nullable Context context) {
this(context, new FeatureFlagsImpl(), new Handler(Looper.getMainLooper()));
this(context, new DesktopExperienceFlags(new FeatureFlagsImpl()),
new Handler(Looper.getMainLooper()));
}
Injector(@Nullable Context context, @NonNull FeatureFlags flags, @NonNull Handler handler) {

View File

@@ -16,6 +16,9 @@
package com.android.settings.fuelgauge.batteryusage;
import static com.android.settings.fuelgauge.batteryusage.BatteryChartViewModel.SELECTED_INDEX_ALL;
import static com.android.settings.fuelgauge.batteryusage.BatteryChartViewModel.SELECTED_INDEX_INVALID;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.app.settings.SettingsEnums;
@@ -82,10 +85,10 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
@VisibleForTesting TextView mChartSummaryTextView;
@VisibleForTesting BatteryChartView mDailyChartView;
@VisibleForTesting BatteryChartView mHourlyChartView;
@VisibleForTesting int mDailyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL;
@VisibleForTesting int mHourlyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL;
@VisibleForTesting int mDailyHighlightSlotIndex = BatteryChartViewModel.SELECTED_INDEX_INVALID;
@VisibleForTesting int mHourlyHighlightSlotIndex = BatteryChartViewModel.SELECTED_INDEX_INVALID;
@VisibleForTesting int mDailyChartIndex = SELECTED_INDEX_ALL;
@VisibleForTesting int mHourlyChartIndex = SELECTED_INDEX_ALL;
@VisibleForTesting int mDailyHighlightSlotIndex = SELECTED_INDEX_INVALID;
@VisibleForTesting int mHourlyHighlightSlotIndex = SELECTED_INDEX_INVALID;
private boolean mIs24HourFormat;
private View mBatteryChartViewGroup;
@@ -198,8 +201,8 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
getTotalHours(batteryLevelData));
if (batteryLevelData == null) {
mDailyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL;
mHourlyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL;
mDailyChartIndex = SELECTED_INDEX_ALL;
mHourlyChartIndex = SELECTED_INDEX_ALL;
mDailyViewModel = null;
mHourlyViewModels = null;
refreshUi();
@@ -226,9 +229,9 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
}
boolean isHighlightSlotFocused() {
return (mDailyHighlightSlotIndex != BatteryChartViewModel.SELECTED_INDEX_INVALID
return (mDailyHighlightSlotIndex != SELECTED_INDEX_INVALID
&& mDailyHighlightSlotIndex == mDailyChartIndex
&& mHourlyHighlightSlotIndex != BatteryChartViewModel.SELECTED_INDEX_INVALID
&& mHourlyHighlightSlotIndex != SELECTED_INDEX_INVALID
&& mHourlyHighlightSlotIndex == mHourlyChartIndex);
}
@@ -242,8 +245,8 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
}
void selectHighlightSlotIndex() {
if (mDailyHighlightSlotIndex == BatteryChartViewModel.SELECTED_INDEX_INVALID
|| mHourlyHighlightSlotIndex == BatteryChartViewModel.SELECTED_INDEX_INVALID) {
if (mDailyHighlightSlotIndex == SELECTED_INDEX_INVALID
|| mHourlyHighlightSlotIndex == SELECTED_INDEX_INVALID) {
return;
}
if (mDailyHighlightSlotIndex == mDailyChartIndex
@@ -258,8 +261,11 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
"onDailyChartSelect:%d, onHourlyChartSelect:%d",
mDailyChartIndex, mHourlyChartIndex));
refreshUi();
// The highlight slot must be selected.
mHandler.post(
() -> mDailyChartView.setAccessibilityPaneTitle(getAccessibilityAnnounceMessage()));
() ->
mDailyChartView.setAccessibilityPaneTitle(
getAccessibilityAnnounceMessage(/* isSlotSelected= */ true)));
if (mOnSelectedIndexUpdatedListener != null) {
mOnSelectedIndexUpdatedListener.onSelectedIndexUpdated();
}
@@ -295,15 +301,16 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
}
Log.d(TAG, "onDailyChartSelect:" + trapezoidIndex);
mDailyChartIndex = trapezoidIndex;
mHourlyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL;
mHourlyChartIndex = SELECTED_INDEX_ALL;
refreshUi();
mHandler.post(
() ->
mDailyChartView.setAccessibilityPaneTitle(
getAccessibilityAnnounceMessage()));
getAccessibilityAnnounceMessage(
mDailyChartIndex != SELECTED_INDEX_ALL)));
mMetricsFeatureProvider.action(
mPrefContext,
trapezoidIndex == BatteryChartViewModel.SELECTED_INDEX_ALL
trapezoidIndex == SELECTED_INDEX_ALL
? SettingsEnums.ACTION_BATTERY_USAGE_DAILY_SHOW_ALL
: SettingsEnums.ACTION_BATTERY_USAGE_DAILY_TIME_SLOT,
mDailyChartIndex);
@@ -314,7 +321,7 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
mHourlyChartView = hourlyChartView;
mHourlyChartView.setOnSelectListener(
trapezoidIndex -> {
if (mDailyChartIndex == BatteryChartViewModel.SELECTED_INDEX_ALL) {
if (mDailyChartIndex == SELECTED_INDEX_ALL) {
// This will happen when a daily slot and an hour slot are clicked together.
return;
}
@@ -327,10 +334,11 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
mHandler.post(
() ->
mHourlyChartView.setAccessibilityPaneTitle(
getAccessibilityAnnounceMessage()));
getAccessibilityAnnounceMessage(
mHourlyChartIndex != SELECTED_INDEX_ALL)));
mMetricsFeatureProvider.action(
mPrefContext,
trapezoidIndex == BatteryChartViewModel.SELECTED_INDEX_ALL
trapezoidIndex == SELECTED_INDEX_ALL
? SettingsEnums.ACTION_BATTERY_USAGE_SHOW_ALL
: SettingsEnums.ACTION_BATTERY_USAGE_TIME_SLOT,
mHourlyChartIndex);
@@ -378,27 +386,27 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
} else {
mDailyChartView.setVisibility(View.VISIBLE);
if (mDailyChartIndex >= mDailyViewModel.size()) {
mDailyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL;
mDailyChartIndex = SELECTED_INDEX_ALL;
}
mDailyViewModel.setSelectedIndex(mDailyChartIndex);
mDailyViewModel.setHighlightSlotIndex(mDailyHighlightSlotIndex);
mDailyChartView.setViewModel(mDailyViewModel);
}
if (mDailyChartIndex == BatteryChartViewModel.SELECTED_INDEX_ALL) {
if (mDailyChartIndex == SELECTED_INDEX_ALL) {
// Multiple days are selected, hide the hourly chart view.
animateBatteryHourlyChartView(/* visible= */ false);
} else {
animateBatteryHourlyChartView(/* visible= */ true);
final BatteryChartViewModel hourlyViewModel = mHourlyViewModels.get(mDailyChartIndex);
if (mHourlyChartIndex >= hourlyViewModel.size()) {
mHourlyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL;
mHourlyChartIndex = SELECTED_INDEX_ALL;
}
hourlyViewModel.setSelectedIndex(mHourlyChartIndex);
hourlyViewModel.setHighlightSlotIndex(
(mDailyChartIndex == mDailyHighlightSlotIndex)
? mHourlyHighlightSlotIndex
: BatteryChartViewModel.SELECTED_INDEX_INVALID);
: SELECTED_INDEX_INVALID);
mHourlyChartView.setViewModel(hourlyViewModel);
}
}
@@ -416,7 +424,7 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
isAccessibilityText
? mDailyViewModel.getContentDescription(mDailyChartIndex)
: mDailyViewModel.getFullText(mDailyChartIndex);
if (mHourlyChartIndex == BatteryChartViewModel.SELECTED_INDEX_ALL) {
if (mHourlyChartIndex == SELECTED_INDEX_ALL) {
return selectedDayText;
}
@@ -441,15 +449,19 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
return "";
}
if (mDailyChartIndex == BatteryChartViewModel.SELECTED_INDEX_ALL
|| mHourlyChartIndex == BatteryChartViewModel.SELECTED_INDEX_ALL) {
if (mDailyChartIndex == SELECTED_INDEX_ALL || mHourlyChartIndex == SELECTED_INDEX_ALL) {
return mDailyViewModel.getSlotBatteryLevelText(mDailyChartIndex);
}
return mHourlyViewModels.get(mDailyChartIndex).getSlotBatteryLevelText(mHourlyChartIndex);
}
private String getAccessibilityAnnounceMessage() {
private String getAccessibilityAnnounceMessage(final boolean isSlotSelected) {
final String selectedInformation =
mPrefContext.getString(
isSlotSelected
? R.string.battery_chart_slot_status_selected
: R.string.battery_chart_slot_status_unselected);
final String slotInformation = getSlotInformation(/* isAccessibilityText= */ true);
final String slotInformationMessage =
slotInformation == null
@@ -460,7 +472,8 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
final String batteryLevelPercentageMessage = getBatteryLevelPercentageInfo();
return mPrefContext.getString(
R.string.battery_usage_time_info_and_battery_level,
R.string.battery_usage_status_time_info_and_battery_level,
selectedInformation,
slotInformationMessage,
batteryLevelPercentageMessage);
}
@@ -533,9 +546,8 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
}
private boolean isAllSelected() {
return (isBatteryLevelDataInOneDay()
|| mDailyChartIndex == BatteryChartViewModel.SELECTED_INDEX_ALL)
&& mHourlyChartIndex == BatteryChartViewModel.SELECTED_INDEX_ALL;
return (isBatteryLevelDataInOneDay() || mDailyChartIndex == SELECTED_INDEX_ALL)
&& mHourlyChartIndex == SELECTED_INDEX_ALL;
}
@VisibleForTesting
@@ -571,9 +583,7 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
return null;
}
BatteryDiffData allBatteryDiffData =
batteryUsageData
.get(BatteryChartViewModel.SELECTED_INDEX_ALL)
.get(BatteryChartViewModel.SELECTED_INDEX_ALL);
batteryUsageData.get(SELECTED_INDEX_ALL).get(SELECTED_INDEX_ALL);
return allBatteryDiffData == null ? null : allBatteryDiffData.getAppDiffEntryList();
}
@@ -613,12 +623,9 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
@Override
public String generateSlotBatteryLevelText(List<Integer> levels, int index) {
final int fromBatteryLevelIndex =
index == BatteryChartViewModel.SELECTED_INDEX_ALL ? 0 : index;
final int fromBatteryLevelIndex = index == SELECTED_INDEX_ALL ? 0 : index;
final int toBatteryLevelIndex =
index == BatteryChartViewModel.SELECTED_INDEX_ALL
? levels.size() - 1
: index + 1;
index == SELECTED_INDEX_ALL ? levels.size() - 1 : index + 1;
return mPrefContext.getString(
R.string.battery_level_percentage,
generateBatteryLevelText(levels.get(fromBatteryLevelIndex)),
@@ -687,9 +694,9 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
return index == timestamps.size() - 1
? generateText(timestamps, index)
: mContext.getString(
R.string.battery_usage_timestamps_content_description,
generateText(timestamps, index),
generateText(timestamps, index + 1));
R.string.battery_usage_timestamps_content_description,
generateText(timestamps, index),
generateText(timestamps, index + 1));
}
HourlyChartLabelTextGenerator updateSpecialCaseContext(

View File

@@ -17,6 +17,8 @@ package com.android.settings.fuelgauge.batteryusage;
import static com.android.settings.Utils.formatPercentage;
import static com.android.settings.fuelgauge.batteryusage.BatteryChartViewModel.AxisLabelPosition.BETWEEN_TRAPEZOIDS;
import static com.android.settings.fuelgauge.batteryusage.BatteryChartViewModel.SELECTED_INDEX_ALL;
import static com.android.settings.fuelgauge.batteryusage.BatteryChartViewModel.SELECTED_INDEX_INVALID;
import static com.android.settingslib.fuelgauge.BatteryStatus.BATTERY_LEVEL_UNKNOWN;
import static java.lang.Math.abs;
@@ -81,7 +83,7 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
getContext().getResources().getConfiguration().getLayoutDirection();
private BatteryChartViewModel mViewModel;
private int mHoveredIndex = BatteryChartViewModel.SELECTED_INDEX_INVALID;
private int mHoveredIndex = SELECTED_INDEX_INVALID;
private int mDividerWidth;
private int mDividerHeight;
private float mTrapezoidVOffset;
@@ -245,9 +247,9 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
// sent here.
return true;
case MotionEvent.ACTION_HOVER_EXIT:
if (mHoveredIndex != BatteryChartViewModel.SELECTED_INDEX_INVALID) {
if (mHoveredIndex != SELECTED_INDEX_INVALID) {
sendAccessibilityEventForHover(AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
mHoveredIndex = BatteryChartViewModel.SELECTED_INDEX_INVALID; // reset
mHoveredIndex = SELECTED_INDEX_INVALID; // reset
invalidate();
}
// Ignore the super.onHoverEvent() because the hovered trapezoid has already been
@@ -262,7 +264,7 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
public void onHoverChanged(boolean hovered) {
super.onHoverChanged(hovered);
if (!hovered) {
mHoveredIndex = BatteryChartViewModel.SELECTED_INDEX_INVALID; // reset
mHoveredIndex = SELECTED_INDEX_INVALID; // reset
invalidate();
}
}
@@ -295,9 +297,7 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
if (mOnSelectListener != null) {
// Selects all if users click the same trapezoid item two times.
mOnSelectListener.onSelect(
index == mViewModel.selectedIndex()
? BatteryChartViewModel.SELECTED_INDEX_ALL
: index);
index == mViewModel.selectedIndex() ? SELECTED_INDEX_ALL : index);
}
view.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK);
}
@@ -332,8 +332,8 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
setBackgroundColor(Color.TRANSPARENT);
mTrapezoidSolidColor = Utils.getColorAccentDefaultColor(context);
mTrapezoidColor = Utils.getDisabled(context, mTrapezoidSolidColor);
mTrapezoidHoverColor = context.getColor(
com.android.internal.R.color.materialColorSecondaryContainer);
mTrapezoidHoverColor =
context.getColor(com.android.internal.R.color.materialColorSecondaryContainer);
// Initializes the divider line paint.
final Resources resources = getContext().getResources();
mDividerWidth = resources.getDimensionPixelSize(R.dimen.chartview_divider_width);
@@ -623,8 +623,7 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
// Configures the trapezoid paint color.
final int trapezoidColor =
(mViewModel.selectedIndex() == index
|| mViewModel.selectedIndex()
== BatteryChartViewModel.SELECTED_INDEX_ALL)
|| mViewModel.selectedIndex() == SELECTED_INDEX_ALL)
? mTrapezoidSolidColor
: mTrapezoidColor;
final boolean isHoverState =
@@ -659,9 +658,7 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
}
private boolean isHighlightSlotValid() {
return mViewModel != null
&& mViewModel.getHighlightSlotIndex()
!= BatteryChartViewModel.SELECTED_INDEX_INVALID;
return mViewModel != null && mViewModel.getHighlightSlotIndex() != SELECTED_INDEX_INVALID;
}
private void drawTransomLine(Canvas canvas) {
@@ -715,7 +712,7 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
// Searches the corresponding trapezoid index from x location.
private int getTrapezoidIndex(float x) {
if (mTrapezoidSlots == null) {
return BatteryChartViewModel.SELECTED_INDEX_INVALID;
return SELECTED_INDEX_INVALID;
}
for (int index = 0; index < mTrapezoidSlots.length; index++) {
final TrapezoidSlot slot = mTrapezoidSlots[index];
@@ -723,7 +720,7 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
return index;
}
}
return BatteryChartViewModel.SELECTED_INDEX_INVALID;
return SELECTED_INDEX_INVALID;
}
private void initializeAxisLabelsBounds() {
@@ -796,7 +793,11 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
childInfo.setText(slotTimeInfo);
childInfo.setContentDescription(
mContext.getString(
R.string.battery_usage_time_info_and_battery_level,
R.string.battery_usage_status_time_info_and_battery_level,
mContext.getString(
mViewModel.selectedIndex() == virtualViewId
? R.string.battery_chart_slot_status_selected
: R.string.battery_chart_slot_status_unselected),
slotTimeInfo,
batteryLevelInfo));
childInfo.setAccessibilityFocused(virtualViewId == mAccessibilityFocusNodeViewId);

View File

@@ -20,6 +20,7 @@ import android.content.Context;
import com.android.settings.core.PreferenceControllerMixin;
import com.android.settings.network.SubscriptionUtil;
import com.android.settingslib.Utils;
import com.android.settingslib.core.AbstractPreferenceController;
public class NetworkResetPreferenceController extends AbstractPreferenceController
@@ -34,8 +35,9 @@ public class NetworkResetPreferenceController extends AbstractPreferenceControll
@Override
public boolean isAvailable() {
return (SubscriptionUtil.isSimHardwareVisible(mContext) &&
(!mRestrictionChecker.hasUserRestriction()));
return (SubscriptionUtil.isSimHardwareVisible(mContext)
&& !Utils.isWifiOnly(mContext)
&& !mRestrictionChecker.hasUserRestriction());
}
@Override

View File

@@ -40,6 +40,7 @@ import com.android.internal.net.VpnProfile;
import com.android.net.module.util.ProxyUtils;
import com.android.settings.R;
import com.android.settings.utils.AndroidKeystoreAliasLoader;
import com.android.settings.wifi.utils.TextInputGroup;
import java.util.Collection;
import java.util.List;
@@ -70,16 +71,17 @@ class ConfigDialog extends AlertDialog implements TextWatcher,
private View mView;
private TextView mName;
private TextInputGroup mNameInput;
private Spinner mType;
private TextView mServer;
private TextView mUsername;
private TextInputGroup mServerInput;
private TextInputGroup mUsernameInput;
private TextInputGroup mPasswordInput;
private TextView mPassword;
private Spinner mProxySettings;
private TextView mProxyHost;
private TextView mProxyPort;
private TextView mIpsecIdentifier;
private TextView mIpsecSecret;
private TextInputGroup mIpsecIdentifierInput;
private TextInputGroup mIpsecSecretInput;
private Spinner mIpsecUserCert;
private Spinner mIpsecCaCert;
private Spinner mIpsecServerCert;
@@ -106,16 +108,22 @@ class ConfigDialog extends AlertDialog implements TextWatcher,
Context context = getContext();
// First, find out all the fields.
mName = (TextView) mView.findViewById(R.id.name);
mNameInput = new TextInputGroup(mView, R.id.name_layout, R.id.name,
R.string.vpn_field_required);
mType = (Spinner) mView.findViewById(R.id.type);
mServer = (TextView) mView.findViewById(R.id.server);
mUsername = (TextView) mView.findViewById(R.id.username);
mPassword = (TextView) mView.findViewById(R.id.password);
mServerInput = new TextInputGroup(mView, R.id.server_layout, R.id.server,
R.string.vpn_field_required);
mUsernameInput = new TextInputGroup(mView, R.id.username_layout, R.id.username,
R.string.vpn_field_required);
mPasswordInput = new TextInputGroup(mView, R.id.password_layout, R.id.password,
R.string.vpn_field_required);
mProxySettings = (Spinner) mView.findViewById(R.id.vpn_proxy_settings);
mProxyHost = (TextView) mView.findViewById(R.id.vpn_proxy_host);
mProxyPort = (TextView) mView.findViewById(R.id.vpn_proxy_port);
mIpsecIdentifier = (TextView) mView.findViewById(R.id.ipsec_identifier);
mIpsecSecret = (TextView) mView.findViewById(R.id.ipsec_secret);
mIpsecIdentifierInput = new TextInputGroup(mView, R.id.ipsec_identifier_layout,
R.id.ipsec_identifier, R.string.vpn_field_required);
mIpsecSecretInput = new TextInputGroup(mView, R.id.ipsec_secret_layout, R.id.ipsec_secret,
R.string.vpn_field_required);
mIpsecUserCert = (Spinner) mView.findViewById(R.id.ipsec_user_cert);
mIpsecCaCert = (Spinner) mView.findViewById(R.id.ipsec_ca_cert);
mIpsecServerCert = (Spinner) mView.findViewById(R.id.ipsec_server_cert);
@@ -125,21 +133,21 @@ class ConfigDialog extends AlertDialog implements TextWatcher,
mAlwaysOnInvalidReason = (TextView) mView.findViewById(R.id.always_on_invalid_reason);
// Second, copy values from the profile.
mName.setText(mProfile.name);
mNameInput.setText(mProfile.name);
setTypesByFeature(mType);
mType.setSelection(convertVpnProfileConstantToTypeIndex(mProfile.type));
mServer.setText(mProfile.server);
mServerInput.setText(mProfile.server);
if (mProfile.saveLogin) {
mUsername.setText(mProfile.username);
mPassword.setText(mProfile.password);
mUsernameInput.setText(mProfile.username);
mPasswordInput.setText(mProfile.password);
}
if (mProfile.proxy != null) {
mProxyHost.setText(mProfile.proxy.getHost());
int port = mProfile.proxy.getPort();
mProxyPort.setText(port == 0 ? "" : Integer.toString(port));
}
mIpsecIdentifier.setText(mProfile.ipsecIdentifier);
mIpsecSecret.setText(mProfile.ipsecSecret);
mIpsecIdentifierInput.setText(mProfile.ipsecIdentifier);
mIpsecSecretInput.setText(mProfile.ipsecSecret);
final AndroidKeystoreAliasLoader androidKeystoreAliasLoader =
new AndroidKeystoreAliasLoader(null);
loadCertificates(mIpsecUserCert, androidKeystoreAliasLoader.getKeyCertAliases(), 0,
@@ -150,7 +158,8 @@ class ConfigDialog extends AlertDialog implements TextWatcher,
R.string.vpn_no_server_cert, mProfile.ipsecServerCert);
mSaveLogin.setChecked(mProfile.saveLogin);
mAlwaysOnVpn.setChecked(mProfile.key.equals(VpnUtils.getLockdownVpn()));
mPassword.setTextAppearance(android.R.style.TextAppearance_DeviceDefault_Medium);
mPasswordInput.getEditText()
.setTextAppearance(android.R.style.TextAppearance_DeviceDefault_Medium);
// Hide lockdown VPN on devices that require IMS authentication
if (SystemProperties.getBoolean("persist.radio.imsregrequired", false)) {
@@ -158,16 +167,16 @@ class ConfigDialog extends AlertDialog implements TextWatcher,
}
// Third, add listeners to required fields.
mName.addTextChangedListener(this);
mNameInput.addTextChangedListener(this);
mType.setOnItemSelectedListener(this);
mServer.addTextChangedListener(this);
mUsername.addTextChangedListener(this);
mPassword.addTextChangedListener(this);
mServerInput.addTextChangedListener(this);
mUsernameInput.addTextChangedListener(this);
mPasswordInput.addTextChangedListener(this);
mProxySettings.setOnItemSelectedListener(this);
mProxyHost.addTextChangedListener(this);
mProxyPort.addTextChangedListener(this);
mIpsecIdentifier.addTextChangedListener(this);
mIpsecSecret.addTextChangedListener(this);
mIpsecIdentifierInput.addTextChangedListener(this);
mIpsecSecretInput.addTextChangedListener(this);
mIpsecUserCert.setOnItemSelectedListener(this);
mShowOptions.setOnClickListener(this);
mAlwaysOnVpn.setOnCheckedChangeListener(this);
@@ -202,6 +211,8 @@ class ConfigDialog extends AlertDialog implements TextWatcher,
setTitle(context.getString(R.string.vpn_connect_to, mProfile.name));
setUsernamePasswordVisibility(mProfile.type);
mUsernameInput.setHelperText(context.getString(R.string.vpn_required));
mPasswordInput.setHelperText(context.getString(R.string.vpn_required));
// Create a button to connect the network.
setButton(DialogInterface.BUTTON_POSITIVE,
@@ -260,6 +271,10 @@ class ConfigDialog extends AlertDialog implements TextWatcher,
updateProxyFieldsVisibility(position);
}
updateUiControls();
mNameInput.setError("");
mServerInput.setError("");
mIpsecIdentifierInput.setError("");
mIpsecSecretInput.setError("");
}
@Override
@@ -375,30 +390,16 @@ class ConfigDialog extends AlertDialog implements TextWatcher,
return false;
}
final int position = mType.getSelectedItemPosition();
final int type = VPN_TYPES.get(position);
if (!editing && requiresUsernamePassword(type)) {
return mUsername.getText().length() != 0 && mPassword.getText().length() != 0;
}
if (mName.getText().length() == 0 || mServer.getText().length() == 0) {
return false;
}
// All IKEv2 methods require an identifier
if (mIpsecIdentifier.getText().length() == 0) {
return false;
}
if (!validateProxy()) {
return false;
}
switch (type) {
switch (getVpnType()) {
case VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS:
return true;
case VpnProfile.TYPE_IKEV2_IPSEC_PSK:
return mIpsecSecret.getText().length() != 0;
return true;
case VpnProfile.TYPE_IKEV2_IPSEC_RSA:
return mIpsecUserCert.getSelectedItemPosition() != 0;
@@ -406,6 +407,29 @@ class ConfigDialog extends AlertDialog implements TextWatcher,
return false;
}
public boolean validate() {
boolean isValidate = true;
int type = getVpnType();
if (!mEditing && requiresUsernamePassword(type)) {
if (!mUsernameInput.validate()) isValidate = false;
if (!mPasswordInput.validate()) isValidate = false;
return isValidate;
}
if (!mNameInput.validate()) isValidate = false;
if (!mServerInput.validate()) isValidate = false;
if (!mIpsecIdentifierInput.validate()) isValidate = false;
if (type == VpnProfile.TYPE_IKEV2_IPSEC_PSK && !mIpsecSecretInput.validate()) {
isValidate = false;
}
if (!isValidate) Log.w(TAG, "Failed to validate VPN profile!");
return isValidate;
}
private int getVpnType() {
return VPN_TYPES.get(mType.getSelectedItemPosition());
}
private void setTypesByFeature(Spinner typeSpinner) {
String[] types = getContext().getResources().getStringArray(R.array.vpn_types);
if (types.length != VPN_TYPES.size()) {
@@ -487,15 +511,14 @@ class ConfigDialog extends AlertDialog implements TextWatcher,
VpnProfile getProfile() {
// First, save common fields.
VpnProfile profile = new VpnProfile(mProfile.key);
profile.name = mName.getText().toString();
final int position = mType.getSelectedItemPosition();
profile.type = VPN_TYPES.get(position);
profile.server = mServer.getText().toString().trim();
profile.username = mUsername.getText().toString();
profile.password = mPassword.getText().toString();
profile.name = mNameInput.getText();
profile.type = getVpnType();
profile.server = mServerInput.getText().trim();
profile.username = mUsernameInput.getText();
profile.password = mPasswordInput.getText();
// Save fields based on VPN type.
profile.ipsecIdentifier = mIpsecIdentifier.getText().toString();
profile.ipsecIdentifier = mIpsecIdentifierInput.getText();
if (hasProxy()) {
String proxyHost = mProxyHost.getText().toString().trim();
@@ -517,7 +540,7 @@ class ConfigDialog extends AlertDialog implements TextWatcher,
// Then, save type-specific fields.
switch (profile.type) {
case VpnProfile.TYPE_IKEV2_IPSEC_PSK:
profile.ipsecSecret = mIpsecSecret.getText().toString();
profile.ipsecSecret = mIpsecSecretInput.getText();
break;
case VpnProfile.TYPE_IKEV2_IPSEC_RSA:

View File

@@ -124,6 +124,7 @@ public class ConfigDialogFragment extends InstrumentedDialogFragment implements
VpnProfile profile = dialog.getProfile();
if (button == DialogInterface.BUTTON_POSITIVE) {
if (!dialog.validate()) return;
// Possibly throw up a dialog to explain lockdown VPN.
final boolean shouldLockdown = dialog.isVpnAlwaysOn();
final boolean shouldConnect = shouldLockdown || !dialog.isEditing();

View File

@@ -77,7 +77,7 @@ import com.android.settings.utils.AndroidKeystoreAliasLoader;
import com.android.settings.wifi.details2.WifiPrivacyPreferenceController;
import com.android.settings.wifi.details2.WifiPrivacyPreferenceController2;
import com.android.settings.wifi.dpp.WifiDppUtils;
import com.android.settings.wifi.utils.SsidInputGroup;
import com.android.settings.wifi.utils.TextInputGroup;
import com.android.settingslib.Utils;
import com.android.settingslib.utils.ThreadUtils;
import com.android.wifi.flags.Flags;
@@ -229,7 +229,7 @@ public class WifiConfigController2 implements TextWatcher,
private final boolean mHideMeteredAndPrivacy;
private final WifiManager mWifiManager;
private final AndroidKeystoreAliasLoader mAndroidKeystoreAliasLoader;
private SsidInputGroup mSsidInputGroup;
private TextInputGroup mSsidInputGroup;
private final Context mContext;
@@ -299,7 +299,8 @@ public class WifiConfigController2 implements TextWatcher,
wepWarningLayout.setVisibility(View.VISIBLE);
}
mSsidInputGroup = new SsidInputGroup(mContext, mView, R.id.ssid_layout, R.id.ssid);
mSsidInputGroup = new TextInputGroup(mView, R.id.ssid_layout, R.id.ssid,
R.string.wifi_ssid_hint);
mSsidScanButton = (ImageButton) mView.findViewById(R.id.ssid_scanner_button);
mIpSettingsSpinner = (Spinner) mView.findViewById(R.id.ip_settings);
mIpSettingsSpinner.setOnItemSelectedListener(this);

View File

@@ -28,7 +28,7 @@ import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
import com.android.settings.R;
import com.android.settings.wifi.utils.SsidInputGroup;
import com.android.settings.wifi.utils.TextInputGroup;
import com.android.settings.wifi.utils.WifiDialogHelper;
import com.android.settingslib.RestrictedLockUtils;
import com.android.settingslib.RestrictedLockUtilsInternal;
@@ -120,7 +120,8 @@ public class WifiDialog extends AlertDialog implements WifiConfigUiBase,
}
mDialogHelper = new WifiDialogHelper(this,
new SsidInputGroup(getContext(), mView, R.id.ssid_layout, R.id.ssid));
new TextInputGroup(mView, R.id.ssid_layout, R.id.ssid,
R.string.vpn_field_required));
}
@SuppressWarnings("MissingSuperCall") // TODO: Fix me

View File

@@ -1,34 +0,0 @@
/*
* Copyright (C) 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.wifi.utils
import android.content.Context
import android.view.View
import com.android.settings.R
/** TextInputGroup for Wi-Fi SSID. */
class SsidInputGroup(private val context: Context, view: View, layoutId: Int, editTextId: Int) :
TextInputGroup(view, layoutId, editTextId) {
fun validate(): Boolean {
if (getText().isEmpty()) {
setError(context.getString(R.string.wifi_ssid_hint))
return false
}
return true
}
}

View File

@@ -18,6 +18,7 @@ package com.android.settings.wifi.utils
import android.text.Editable
import android.text.TextWatcher
import android.util.Log
import android.view.View
import android.widget.EditText
import com.google.android.material.textfield.TextInputLayout
@@ -27,13 +28,17 @@ open class TextInputGroup(
private val view: View,
private val layoutId: Int,
private val editTextId: Int,
private val errorMessageId: Int,
) {
private val View.layout: TextInputLayout?
get() = findViewById(layoutId)
val layout: TextInputLayout
get() = view.requireViewById(layoutId)
private val View.editText: EditText?
get() = findViewById(editTextId)
val editText: EditText
get() = view.requireViewById(editTextId)
val errorMessage: String
get() = view.context.getString(errorMessageId)
private val textWatcher =
object : TextWatcher {
@@ -42,7 +47,7 @@ open class TextInputGroup(
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
view.layout?.isErrorEnabled = false
layout.isErrorEnabled = false
}
}
@@ -51,18 +56,37 @@ open class TextInputGroup(
}
fun addTextChangedListener(watcher: TextWatcher) {
view.editText?.addTextChangedListener(watcher)
editText.addTextChangedListener(watcher)
}
fun getText(): String {
return view.editText?.text?.toString() ?: ""
var text: String
get() = editText.text?.toString() ?: ""
set(value) {
editText.setText(value)
}
var helperText: String
get() = layout.helperText?.toString() ?: ""
set(value) {
layout.setHelperText(value)
}
var error: String
get() = layout.error?.toString() ?: ""
set(value) {
layout.setError(value)
}
open fun validate(): Boolean {
val isValid = text.isNotEmpty()
if (!isValid) {
Log.w(TAG, "validate failed in ${layout.hint ?: "unknown"}")
error = errorMessage.toString()
}
return isValid
}
fun setText(text: String) {
view.editText?.setText(text)
}
fun setError(errorMessage: String?) {
view.layout?.apply { error = errorMessage }
companion object {
const val TAG = "TextInputGroup"
}
}

View File

@@ -21,7 +21,7 @@ import androidx.appcompat.app.AlertDialog
class WifiDialogHelper(
alertDialog: AlertDialog,
private val ssidInputGroup: SsidInputGroup? = null,
private val ssidInputGroup: TextInputGroup? = null,
) : AlertDialogHelper(alertDialog) {
override fun canDismiss(): Boolean {

View File

@@ -0,0 +1,200 @@
/*
* Copyright (C) 2025 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.accessibility;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.provider.Settings;
import android.provider.Settings.Secure.AccessibilityMagnificationCursorFollowingMode;
import android.text.TextUtils;
import android.widget.AdapterView;
import android.widget.ListView;
import androidx.annotation.NonNull;
import androidx.preference.Preference;
import androidx.preference.PreferenceManager;
import androidx.preference.PreferenceScreen;
import androidx.test.core.app.ApplicationProvider;
import com.android.settings.DialogCreatable;
import com.android.settings.R;
import com.android.settings.accessibility.MagnificationCursorFollowingModePreferenceController.ModeInfo;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.RobolectricTestRunner;
/** Tests for {@link MagnificationCursorFollowingModePreferenceController}. */
@RunWith(RobolectricTestRunner.class)
public class MagnificationCursorFollowingModePreferenceControllerTest {
private static final String PREF_KEY =
Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE;
@Rule
public MockitoRule mocks = MockitoJUnit.rule();
@Spy
private TestDialogHelper mDialogHelper = new TestDialogHelper();
private PreferenceScreen mScreen;
private Context mContext;
private MagnificationCursorFollowingModePreferenceController mController;
private Preference mModePreference;
@Before
public void setUp() {
mContext = ApplicationProvider.getApplicationContext();
mContext.setTheme(androidx.appcompat.R.style.Theme_AppCompat);
final PreferenceManager preferenceManager = new PreferenceManager(mContext);
mScreen = preferenceManager.createPreferenceScreen(mContext);
mModePreference = new Preference(mContext);
mModePreference.setKey(PREF_KEY);
mScreen.addPreference(mModePreference);
mController = new MagnificationCursorFollowingModePreferenceController(mContext, PREF_KEY);
mController.setDialogHelper(mDialogHelper);
mDialogHelper.setDialogDelegate(mController);
showPreferenceOnTheScreen();
}
private void showPreferenceOnTheScreen() {
mController.displayPreference(mScreen);
}
@AccessibilityMagnificationCursorFollowingMode
private int getCheckedModeFromDialog() {
final ListView listView = mController.mModeListView;
assertThat(listView).isNotNull();
final int checkedPosition = listView.getCheckedItemPosition();
assertWithMessage("No mode is checked").that(checkedPosition)
.isNotEqualTo(AdapterView.INVALID_POSITION);
final ModeInfo modeInfo = (ModeInfo) listView.getAdapter().getItem(checkedPosition);
return modeInfo.mMode;
}
private void performItemClickWith(@AccessibilityMagnificationCursorFollowingMode int mode) {
final ListView listView = mController.mModeListView;
assertThat(listView).isNotNull();
int modeIndex = AdapterView.NO_ID;
for (int i = 0; i < listView.getAdapter().getCount(); i++) {
final ModeInfo modeInfo = (ModeInfo) listView.getAdapter().getItem(i);
if (modeInfo != null && modeInfo.mMode == mode) {
modeIndex = i;
break;
}
}
assertWithMessage("The mode could not be found").that(modeIndex)
.isNotEqualTo(AdapterView.NO_ID);
listView.performItemClick(listView.getChildAt(modeIndex), modeIndex, modeIndex);
}
@Test
public void clickPreference_defaultMode_selectionIsDefault() {
mController.handlePreferenceTreeClick(mModePreference);
assertThat(getCheckedModeFromDialog()).isEqualTo(
Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_CONTINUOUS);
}
@Test
public void clickPreference_nonDefaultMode_selectionIsExpected() {
Settings.Secure.putInt(mContext.getContentResolver(), PREF_KEY,
Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_CENTER);
mController.handlePreferenceTreeClick(mModePreference);
assertThat(getCheckedModeFromDialog()).isEqualTo(
Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_CENTER);
}
@Test
public void selectItemInDialog_selectionIsExpected() {
mController.handlePreferenceTreeClick(mModePreference);
performItemClickWith(
Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_EDGE);
assertThat(getCheckedModeFromDialog()).isEqualTo(
Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_EDGE);
}
@Test
public void selectItemInDialog_dismissWithoutSave_selectionNotPersists() {
mController.handlePreferenceTreeClick(mModePreference);
performItemClickWith(
Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_EDGE);
showPreferenceOnTheScreen();
mController.handlePreferenceTreeClick(mModePreference);
assertThat(getCheckedModeFromDialog()).isEqualTo(
Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_CONTINUOUS);
assertThat(TextUtils.equals(mController.getSummary(), mContext.getString(
R.string.accessibility_magnification_cursor_following_continuous))).isTrue();
}
@Test
public void selectItemInDialog_saveAndDismiss_selectionPersists() {
mController.handlePreferenceTreeClick(mModePreference);
performItemClickWith(
Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_EDGE);
mController.onMagnificationCursorFollowingModeDialogPositiveButtonClicked(
mDialogHelper.getDialog(), DialogInterface.BUTTON_POSITIVE);
showPreferenceOnTheScreen();
mController.handlePreferenceTreeClick(mModePreference);
assertThat(getCheckedModeFromDialog()).isEqualTo(
Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_EDGE);
assertThat(TextUtils.equals(mController.getSummary(), mContext.getString(
R.string.accessibility_magnification_cursor_following_edge))).isTrue();
}
private static class TestDialogHelper implements DialogHelper {
private DialogCreatable mDialogDelegate;
private Dialog mDialog;
@Override
public void showDialog(int dialogId) {
mDialog = mDialogDelegate.onCreateDialog(dialogId);
}
public void setDialogDelegate(@NonNull DialogCreatable delegate) {
mDialogDelegate = delegate;
}
public Dialog getDialog() {
return mDialog;
}
}
}

View File

@@ -39,6 +39,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.icu.text.CaseMap;
import android.net.Uri;
import android.os.Bundle;
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
@@ -56,12 +57,14 @@ import androidx.fragment.app.FragmentActivity;
import androidx.preference.Preference;
import androidx.preference.PreferenceManager;
import androidx.preference.PreferenceScreen;
import androidx.preference.PreferenceViewHolder;
import androidx.test.core.app.ApplicationProvider;
import com.android.settings.R;
import com.android.settings.flags.Flags;
import com.android.settings.testutils.shadow.ShadowAccessibilityManager;
import com.android.settings.testutils.shadow.ShadowFragment;
import com.android.settingslib.widget.IllustrationPreference;
import com.android.settingslib.widget.TopIntroPreference;
import com.google.android.setupcompat.util.WizardManagerHelper;
@@ -79,6 +82,7 @@ import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.shadows.ShadowApplication;
import org.robolectric.shadows.ShadowLooper;
import java.util.List;
import java.util.Locale;
@@ -315,6 +319,45 @@ public class ToggleFeaturePreferenceFragmentTest {
assertThat(mFragment.getPreferenceScreen().getPreferenceCount()).isEqualTo(0);
}
@Test
public void initAnimatedImagePreference_isAnimatable_setContentDescription() {
mFragment.mFeatureName = "Test Feature";
final View view =
LayoutInflater.from(mContext).inflate(
com.android.settingslib.widget.preference.illustration
.R.layout.illustration_preference,
null);
IllustrationPreference preference = spy(new IllustrationPreference(mFragment.getContext()));
when(preference.isAnimatable()).thenReturn(true);
mFragment.initAnimatedImagePreference(mock(Uri.class), preference);
preference.onBindViewHolder(PreferenceViewHolder.createInstanceForTests(view));
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
String expectedContentDescription = mFragment.getString(
R.string.accessibility_illustration_content_description, mFragment.mFeatureName);
assertThat(preference.getContentDescription().toString())
.isEqualTo(expectedContentDescription);
}
@Test
public void initAnimatedImagePreference_isNotAnimatable_notSetContentDescription() {
mFragment.mFeatureName = "Test Feature";
final View view =
LayoutInflater.from(mContext).inflate(
com.android.settingslib.widget.preference.illustration
.R.layout.illustration_preference,
null);
IllustrationPreference preference = spy(new IllustrationPreference(mFragment.getContext()));
when(preference.isAnimatable()).thenReturn(false);
mFragment.initAnimatedImagePreference(mock(Uri.class), preference);
preference.onBindViewHolder(PreferenceViewHolder.createInstanceForTests(view));
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
verify(preference, never()).setContentDescription(any());
}
@Test
@EnableFlags(Flags.FLAG_ACCESSIBILITY_SHOW_APP_INFO_BUTTON)
public void createAppInfoPreference_withValidComponentName() {

View File

@@ -613,6 +613,24 @@ public class ToggleScreenMagnificationPreferenceFragmentTest {
verify(dialogDelegate).getDialogMetricsCategory(dialogId);
}
@Test
@EnableFlags(com.android.settings.accessibility.Flags
.FLAG_ENABLE_MAGNIFICATION_CURSOR_FOLLOWING_DIALOG)
public void onCreateDialog_setCursorFollowingModeDialogDelegate_invokeDialogDelegate() {
ToggleScreenMagnificationPreferenceFragment fragment =
mFragController.create(
R.id.main_content, /* bundle= */ null).start().resume().get();
final DialogCreatable dialogDelegate = mock(DialogCreatable.class, RETURNS_DEEP_STUBS);
final int dialogId = DialogEnums.DIALOG_MAGNIFICATION_CURSOR_FOLLOWING_MODE;
when(dialogDelegate.getDialogMetricsCategory(anyInt())).thenReturn(dialogId);
fragment.setMagnificationCursorFollowingModeDialogDelegate(dialogDelegate);
fragment.onCreateDialog(dialogId);
fragment.getDialogMetricsCategory(dialogId);
verify(dialogDelegate).onCreateDialog(dialogId);
verify(dialogDelegate).getDialogMetricsCategory(dialogId);
}
@Test
public void getMetricsCategory_returnsCorrectCategory() {
ToggleScreenMagnificationPreferenceFragment fragment =
@@ -826,6 +844,7 @@ public class ToggleScreenMagnificationPreferenceFragmentTest {
MagnificationOneFingerPanningPreferenceController.PREF_KEY,
MagnificationAlwaysOnPreferenceController.PREF_KEY,
MagnificationJoystickPreferenceController.PREF_KEY,
MagnificationCursorFollowingModePreferenceController.PREF_KEY,
MagnificationFeedbackPreferenceController.PREF_KEY);
final List<SearchIndexableRaw> rawData = ToggleScreenMagnificationPreferenceFragment
@@ -881,7 +900,9 @@ public class ToggleScreenMagnificationPreferenceFragmentTest {
@EnableFlags({
com.android.settings.accessibility.Flags.FLAG_FIX_A11Y_SETTINGS_SEARCH,
Flags.FLAG_ENABLE_MAGNIFICATION_ONE_FINGER_PANNING_GESTURE,
Flags.FLAG_ENABLE_LOW_VISION_HATS})
Flags.FLAG_ENABLE_LOW_VISION_HATS,
com.android.settings.accessibility.Flags
.FLAG_ENABLE_MAGNIFICATION_CURSOR_FOLLOWING_DIALOG})
public void getNonIndexableKeys_hasShortcutAndAllFeaturesEnabled_allItemsSearchable() {
mShadowAccessibilityManager.setAccessibilityShortcutTargets(
TRIPLETAP, List.of(MAGNIFICATION_CONTROLLER_NAME));

View File

@@ -34,6 +34,7 @@ import android.content.Context;
import android.graphics.drawable.Drawable;
import android.os.Looper;
import android.os.UserManager;
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import android.util.Pair;
@@ -175,6 +176,7 @@ public class BluetoothDevicePreferenceTest {
}
@Test
@DisableFlags(Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI)
public void onClicked_deviceNotBonded_shouldLogBluetoothPairEvent() {
when(mCachedBluetoothDevice.isConnected()).thenReturn(false);
when(mCachedBluetoothDevice.getBondState()).thenReturn(BluetoothDevice.BOND_NONE);
@@ -192,6 +194,7 @@ public class BluetoothDevicePreferenceTest {
}
@Test
@DisableFlags(Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI)
public void onClicked_deviceNotBonded_shouldLogBluetoothPairEventAndPairWithoutNameEvent() {
when(mCachedBluetoothDevice.isConnected()).thenReturn(false);
when(mCachedBluetoothDevice.getBondState()).thenReturn(BluetoothDevice.BOND_NONE);

View File

@@ -28,7 +28,6 @@ import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
@@ -52,7 +51,9 @@ import android.media.session.ISession;
import android.media.session.ISessionController;
import android.media.session.MediaSessionManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.platform.test.flag.junit.SetFlagsRule;
import android.util.DisplayMetrics;
@@ -81,14 +82,12 @@ import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.android.util.concurrent.InlineExecutorService;
import org.robolectric.annotation.Config;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.util.ReflectionHelpers;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
@RunWith(RobolectricTestRunner.class)
@Config(
@@ -122,6 +121,7 @@ public class AudioStreamMediaServiceTest {
@Mock private PackageManager mPackageManager;
@Mock private DisplayMetrics mDisplayMetrics;
@Mock private Context mContext;
@Mock private Handler mHandler;
private FakeFeatureFactory mFeatureFactory;
private AudioStreamMediaService mAudioStreamMediaService;
@@ -145,11 +145,18 @@ public class AudioStreamMediaServiceTest {
when(mCachedBluetoothDevice.getName()).thenReturn(DEVICE_NAME);
when(mLocalBluetoothProfileManager.getVolumeControlProfile())
.thenReturn(mVolumeControlProfile);
mAudioStreamMediaService = spy(new AudioStreamMediaService());
when(mHandler.post(any(Runnable.class))).thenAnswer(invocation -> {
((Runnable) invocation.getArgument(0)).run();
return null;
});
when(mHandler.getLooper()).thenReturn(Looper.getMainLooper());
mAudioStreamMediaService = spy(new AudioStreamMediaService() {
@Override
Handler getHandler() {
return mHandler;
}
});
ReflectionHelpers.setField(mAudioStreamMediaService, "mBase", mContext);
ReflectionHelpers.setField(
mAudioStreamMediaService, "mExecutor", new InlineExecutorService());
when(mAudioStreamMediaService.getSystemService(anyString()))
.thenReturn(mMediaSessionManager);
when(mMediaSessionManager.createSession(any(), anyString(), any())).thenReturn(mISession);
@@ -391,31 +398,6 @@ public class AudioStreamMediaServiceTest {
verify(mAudioStreamMediaService).stopSelf();
}
@Test
public void bluetoothCallback_onMemberDeviceDisconnect_stopSelf() {
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
when(mCachedBluetoothDevice.getDevice()).thenReturn(mock(BluetoothDevice.class));
CachedBluetoothDevice member = mock(CachedBluetoothDevice.class);
when(mCachedBluetoothDevice.getMemberDevice()).thenReturn(Set.of(member));
when(member.getDevice()).thenReturn(mDevice);
var devices = new ArrayList<BluetoothDevice>();
devices.add(mDevice);
Intent intent = new Intent();
intent.putExtra(BROADCAST_ID, 1);
intent.putParcelableArrayListExtra(DEVICES, devices);
mAudioStreamMediaService.onCreate();
assertThat(mAudioStreamMediaService.mBluetoothCallback).isNotNull();
mAudioStreamMediaService.onStartCommand(intent, /* flags= */ 0, /* startId= */ 0);
mAudioStreamMediaService.mBluetoothCallback.onProfileConnectionStateChanged(
mCachedBluetoothDevice,
BluetoothAdapter.STATE_DISCONNECTED,
BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
verify(mAudioStreamMediaService).stopSelf();
}
@Test
public void mediaSessionCallback_onPause_setVolume() {
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);

View File

@@ -19,9 +19,16 @@ package com.android.settings.network;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.content.res.Resources;
import android.telephony.TelephonyManager;
import com.android.settings.R;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -34,28 +41,66 @@ import org.robolectric.util.ReflectionHelpers;
@RunWith(RobolectricTestRunner.class)
public class NetworkResetPreferenceControllerTest {
@Mock
private TelephonyManager mTelephonyManager;
@Mock
private NetworkResetRestrictionChecker mRestrictionChecker;
private NetworkResetPreferenceController mController;
private Context mContext;
private Resources mResources;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mController = new NetworkResetPreferenceController(RuntimeEnvironment.application);
mContext = spy(RuntimeEnvironment.application);
mResources = spy(mContext.getResources());
when(mContext.getResources()).thenReturn(mResources);
when(mContext.getSystemService(Context.TELEPHONY_SERVICE)).thenReturn(mTelephonyManager);
mController = new NetworkResetPreferenceController(mContext);
ReflectionHelpers.setField(mController, "mRestrictionChecker", mRestrictionChecker);
// Availability defaults
when(mTelephonyManager.isDataCapable()).thenReturn(true);
when(mResources.getBoolean(R.bool.config_show_sim_info)).thenReturn(true);
when(mRestrictionChecker.isRestrictionEnforcedByAdmin()).thenReturn(false);
}
@Test
public void testIsAvailable_shouldReturnTrueWhenNoUserRestriction() {
when(mRestrictionChecker.isRestrictionEnforcedByAdmin()).thenReturn(true);
public void testIsAvailable_showSimInfo_notWifiOnly() {
assertThat(mController.isAvailable()).isTrue();
}
@Test
public void testIsAvailable_hideSimInfo_notWifiOnly() {
when(mResources.getBoolean(R.bool.config_show_sim_info)).thenReturn(false);
assertThat(mController.isAvailable()).isFalse();
}
@Test
public void testIsAvailable_showSimInfo_wifiOnly() {
when(mTelephonyManager.isDataCapable()).thenReturn(false);
assertThat(mController.isAvailable()).isFalse();
}
@Test
public void testIsAvailable_userRestriction() {
when(mRestrictionChecker.isRestrictionEnforcedByAdmin()).thenReturn(true);
when(mRestrictionChecker.hasUserRestriction()).thenReturn(true);
assertThat(mController.isAvailable()).isFalse();
verify(mRestrictionChecker, never()).isRestrictionEnforcedByAdmin();
}
@Test
public void testIsAvailable_noUserRestriction() {
when(mRestrictionChecker.isRestrictionEnforcedByAdmin()).thenReturn(true);
when(mRestrictionChecker.hasUserRestriction()).thenReturn(false);
assertThat(mController.isAvailable()).isTrue();
verify(mRestrictionChecker, never()).isRestrictionEnforcedByAdmin();
}
}