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"> android:padding="?android:attr/dialogPreferredPadding">
<TextView <TextView
android:id="@+id/accessibility_dialog_header_text_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/accessibility_magnification_area_settings_message"
android:textSize="16sp" android:textSize="16sp"
style="?android:attr/textAppearanceMedium" style="?android:attr/textAppearanceMedium"
android:textColor="?android:attr/textColorAlertDialogListItem"/> android:textColor="?android:attr/textColorAlertDialogListItem"/>

View File

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

View File

@@ -5220,6 +5220,16 @@
<string name="accessibility_screen_magnification_title">Magnification</string> <string name="accessibility_screen_magnification_title">Magnification</string>
<!-- Title for accessibility shortcut preference for magnification. [CHAR LIMIT=60] --> <!-- Title for accessibility shortcut preference for magnification. [CHAR LIMIT=60] -->
<string name="accessibility_screen_magnification_shortcut_title">Magnification shortcut</string> <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] --> <!-- Title for accessibility follow typing preference for magnification. [CHAR LIMIT=35] -->
<string name="accessibility_screen_magnification_follow_typing_title">Magnify typing</string> <string name="accessibility_screen_magnification_follow_typing_title">Magnify typing</string>
<!-- Summary for accessibility follow typing preference for magnification. [CHAR LIMIT=none] --> <!-- 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> <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"). --> <!-- [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> <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. --> <!-- [CHAR_LIMIT=NONE] Accessibility content description for each slot in battery chart view. Please reuse the words in tc/6732629268310936155 -->
<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> <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. --> <!-- [CHAR_LIMIT=NONE] Accessibility content description for battery chart view. -->
<string name="battery_usage_chart">Battery usage chart</string> <string name="battery_usage_chart">Battery usage chart</string>
<!-- [CHAR_LIMIT=NONE] Accessibility content description for daily battery chart view. --> <!-- [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] --> generic error. [CHAR LIMIT=120] -->
<string name="vpn_always_on_invalid_reason_other">The information entered doesn\'t support <string name="vpn_always_on_invalid_reason_other">The information entered doesn\'t support
always-on VPN</string> 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] --> <!-- Button label to cancel changing a VPN profile. [CHAR LIMIT=40] -->
<string name="vpn_cancel">Cancel</string> <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] --> <!-- 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> <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> <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] --> <!-- 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> <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 views don't need to be enable. [CHAR LIMIT=NONE] --> <!-- 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 View on secondary displays.</string> <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] --> <!-- 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> <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> <string name="supervision_add_forgot_pin_preference_title">Forgot PIN</string>
<!-- Title for web content filters entry [CHAR LIMIT=60] --> <!-- Title for web content filters entry [CHAR LIMIT=60] -->
<string name="supervision_web_content_filters_title">Web content filters</string> <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> </resources>

View File

@@ -104,6 +104,11 @@ public class AccessibilityDialogUtils {
* screen / Switch between full and partial screen > Save. * screen / Switch between full and partial screen > Save.
*/ */
int DIALOG_MAGNIFICATION_TRIPLE_TAP_WARNING = 1011; 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); mContext, mModeInfos, this::onMagnificationModeSelected);
final View headerView = LayoutInflater.from(mContext).inflate( final View headerView = LayoutInflater.from(mContext).inflate(
R.layout.accessibility_magnification_mode_header, R.layout.accessibility_dialog_header, getMagnificationModesListView(),
getMagnificationModesListView(), /* attachToRoot= */false); /* 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, getMagnificationModesListView().addHeaderView(headerView, /* data= */null,
/* isSelectable= */false); /* isSelectable= */false);

View File

@@ -68,6 +68,7 @@ import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.flags.Flags; import com.android.settings.flags.Flags;
import com.android.settings.widget.SettingsMainSwitchBar; import com.android.settings.widget.SettingsMainSwitchBar;
import com.android.settings.widget.SettingsMainSwitchPreference; import com.android.settings.widget.SettingsMainSwitchPreference;
import com.android.settingslib.utils.ThreadUtils;
import com.android.settingslib.widget.IllustrationPreference; import com.android.settingslib.widget.IllustrationPreference;
import com.android.settingslib.widget.TopIntroPreference; import com.android.settingslib.widget.TopIntroPreference;
@@ -311,6 +312,11 @@ public abstract class ToggleFeaturePreferenceFragment extends DashboardFragment
return getString(R.string.accessibility_shortcut_title, mFeatureName); 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) { protected void onPreferenceToggled(String preferenceKey, boolean enabled) {
} }
@@ -427,22 +433,38 @@ public abstract class ToggleFeaturePreferenceFragment extends DashboardFragment
return drawable; return drawable;
} }
private void initAnimatedImagePreference() { private void initAnimatedImagePreference() {
if (mImageUri == null) { initAnimatedImagePreference(mImageUri, new IllustrationPreference(getPrefContext()));
}
@VisibleForTesting
void initAnimatedImagePreference(
@Nullable Uri imageUri,
@NonNull IllustrationPreference preference) {
if (imageUri == null) {
return; return;
} }
final int displayHalfHeight = final int displayHalfHeight =
AccessibilityUtil.getDisplayBounds(getPrefContext()).height() / 2; AccessibilityUtil.getDisplayBounds(getPrefContext()).height() / 2;
final IllustrationPreference illustrationPreference = preference.setImageUri(imageUri);
new IllustrationPreference(getPrefContext()); preference.setSelectable(false);
illustrationPreference.setImageUri(mImageUri); preference.setMaxHeight(displayHalfHeight);
illustrationPreference.setSelectable(false); preference.setKey(KEY_ANIMATED_IMAGE);
illustrationPreference.setMaxHeight(displayHalfHeight); preference.setOnBindListener(view -> {
illustrationPreference.setKey(KEY_ANIMATED_IMAGE); // isAnimatable is decided in
// {@link IllustrationPreference#onBindViewHolder(PreferenceViewHolder)}. Therefore, we
getPreferenceScreen().addPreference(illustrationPreference); // 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 @VisibleForTesting

View File

@@ -93,6 +93,8 @@ public class ToggleScreenMagnificationPreferenceFragment extends
private TouchExplorationStateChangeListener mTouchExplorationStateChangeListener; private TouchExplorationStateChangeListener mTouchExplorationStateChangeListener;
@Nullable @Nullable
private DialogCreatable mMagnificationModeDialogDelegate; private DialogCreatable mMagnificationModeDialogDelegate;
@Nullable
private DialogCreatable mMagnificationCursorFollowingModeDialogDelegate;
@Nullable @Nullable
MagnificationOneFingerPanningPreferenceController mOneFingerPanningPreferenceController; MagnificationOneFingerPanningPreferenceController mOneFingerPanningPreferenceController;
@@ -104,6 +106,12 @@ public class ToggleScreenMagnificationPreferenceFragment extends
mMagnificationModeDialogDelegate = delegate; mMagnificationModeDialogDelegate = delegate;
} }
@VisibleForTesting
public void setMagnificationCursorFollowingModeDialogDelegate(
@NonNull DialogCreatable delegate) {
mMagnificationCursorFollowingModeDialogDelegate = delegate;
}
@Override @Override
public void onCreate(@Nullable Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@@ -186,6 +194,9 @@ public class ToggleScreenMagnificationPreferenceFragment extends
case DialogEnums.DIALOG_MAGNIFICATION_TRIPLE_TAP_WARNING: case DialogEnums.DIALOG_MAGNIFICATION_TRIPLE_TAP_WARNING:
return Preconditions.checkNotNull(mMagnificationModeDialogDelegate) return Preconditions.checkNotNull(mMagnificationModeDialogDelegate)
.onCreateDialog(dialogId); .onCreateDialog(dialogId);
case DialogEnums.DIALOG_MAGNIFICATION_CURSOR_FOLLOWING_MODE:
return Preconditions.checkNotNull(mMagnificationCursorFollowingModeDialogDelegate)
.onCreateDialog(dialogId);
case DialogEnums.GESTURE_NAVIGATION_TUTORIAL: case DialogEnums.GESTURE_NAVIGATION_TUTORIAL:
return AccessibilityShortcutsTutorial return AccessibilityShortcutsTutorial
.showAccessibilityGestureTutorialDialog(getPrefContext()); .showAccessibilityGestureTutorialDialog(getPrefContext());
@@ -201,6 +212,11 @@ public class ToggleScreenMagnificationPreferenceFragment extends
PackageManager.FEATURE_WINDOW_MAGNIFICATION); 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 @Override
protected void initSettingsPreference() { protected void initSettingsPreference() {
final PreferenceCategory generalCategory = findPreference(KEY_GENERAL_CATEGORY); final PreferenceCategory generalCategory = findPreference(KEY_GENERAL_CATEGORY);
@@ -213,6 +229,7 @@ public class ToggleScreenMagnificationPreferenceFragment extends
addJoystickSetting(generalCategory); addJoystickSetting(generalCategory);
// LINT.ThenChange(:search_data) // LINT.ThenChange(:search_data)
} }
addCursorFollowingSetting(generalCategory);
addFeedbackSetting(generalCategory); addFeedbackSetting(generalCategory);
} }
@@ -286,6 +303,31 @@ public class ToggleScreenMagnificationPreferenceFragment extends
addPreferenceController(magnificationModePreferenceController); 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) { private static Preference createFollowTypingPreference(Context context) {
final Preference pref = new SwitchPreferenceCompat(context); final Preference pref = new SwitchPreferenceCompat(context);
pref.setTitle(R.string.accessibility_screen_magnification_follow_typing_title); 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: case DialogEnums.DIALOG_MAGNIFICATION_TRIPLE_TAP_WARNING:
return Preconditions.checkNotNull(mMagnificationModeDialogDelegate) return Preconditions.checkNotNull(mMagnificationModeDialogDelegate)
.getDialogMetricsCategory(dialogId); .getDialogMetricsCategory(dialogId);
case DialogEnums.DIALOG_MAGNIFICATION_CURSOR_FOLLOWING_MODE:
return Preconditions.checkNotNull(mMagnificationCursorFollowingModeDialogDelegate)
.getDialogMetricsCategory(dialogId);
case DialogEnums.GESTURE_NAVIGATION_TUTORIAL: case DialogEnums.GESTURE_NAVIGATION_TUTORIAL:
return SettingsEnums.DIALOG_TOGGLE_SCREEN_MAGNIFICATION_GESTURE_NAVIGATION; return SettingsEnums.DIALOG_TOGGLE_SCREEN_MAGNIFICATION_GESTURE_NAVIGATION;
case DialogEnums.ACCESSIBILITY_BUTTON_TUTORIAL: case DialogEnums.ACCESSIBILITY_BUTTON_TUTORIAL:
@@ -667,6 +712,11 @@ public class ToggleScreenMagnificationPreferenceFragment extends
return rawData; 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)); rawData.add(createShortcutPreferenceSearchData(context));
Stream.of( Stream.of(
createMagnificationModePreference(context), createMagnificationModePreference(context),
@@ -674,6 +724,7 @@ public class ToggleScreenMagnificationPreferenceFragment extends
createOneFingerPanningPreference(context), createOneFingerPanningPreference(context),
createAlwaysOnPreference(context), createAlwaysOnPreference(context),
createJoystickPreference(context), createJoystickPreference(context),
createCursorFollowingPreference(context),
createFeedbackPreference(context) createFeedbackPreference(context)
) )
.forEach(pref -> .forEach(pref ->
@@ -714,6 +765,10 @@ public class ToggleScreenMagnificationPreferenceFragment extends
} }
} }
if (!isMagnificationCursorFollowingModeDialogSupported()) {
niks.add(MagnificationCursorFollowingModePreferenceController.PREF_KEY);
}
if (!Flags.enableLowVisionHats()) { if (!Flags.enableLowVisionHats()) {
niks.add(MagnificationFeedbackPreferenceController.PREF_KEY); niks.add(MagnificationFeedbackPreferenceController.PREF_KEY);
} }

View File

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

View File

@@ -16,6 +16,9 @@
package com.android.settings.fuelgauge.batteryusage; 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.Animator;
import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorListenerAdapter;
import android.app.settings.SettingsEnums; import android.app.settings.SettingsEnums;
@@ -82,10 +85,10 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
@VisibleForTesting TextView mChartSummaryTextView; @VisibleForTesting TextView mChartSummaryTextView;
@VisibleForTesting BatteryChartView mDailyChartView; @VisibleForTesting BatteryChartView mDailyChartView;
@VisibleForTesting BatteryChartView mHourlyChartView; @VisibleForTesting BatteryChartView mHourlyChartView;
@VisibleForTesting int mDailyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL; @VisibleForTesting int mDailyChartIndex = SELECTED_INDEX_ALL;
@VisibleForTesting int mHourlyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL; @VisibleForTesting int mHourlyChartIndex = SELECTED_INDEX_ALL;
@VisibleForTesting int mDailyHighlightSlotIndex = BatteryChartViewModel.SELECTED_INDEX_INVALID; @VisibleForTesting int mDailyHighlightSlotIndex = SELECTED_INDEX_INVALID;
@VisibleForTesting int mHourlyHighlightSlotIndex = BatteryChartViewModel.SELECTED_INDEX_INVALID; @VisibleForTesting int mHourlyHighlightSlotIndex = SELECTED_INDEX_INVALID;
private boolean mIs24HourFormat; private boolean mIs24HourFormat;
private View mBatteryChartViewGroup; private View mBatteryChartViewGroup;
@@ -198,8 +201,8 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
getTotalHours(batteryLevelData)); getTotalHours(batteryLevelData));
if (batteryLevelData == null) { if (batteryLevelData == null) {
mDailyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL; mDailyChartIndex = SELECTED_INDEX_ALL;
mHourlyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL; mHourlyChartIndex = SELECTED_INDEX_ALL;
mDailyViewModel = null; mDailyViewModel = null;
mHourlyViewModels = null; mHourlyViewModels = null;
refreshUi(); refreshUi();
@@ -226,9 +229,9 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
} }
boolean isHighlightSlotFocused() { boolean isHighlightSlotFocused() {
return (mDailyHighlightSlotIndex != BatteryChartViewModel.SELECTED_INDEX_INVALID return (mDailyHighlightSlotIndex != SELECTED_INDEX_INVALID
&& mDailyHighlightSlotIndex == mDailyChartIndex && mDailyHighlightSlotIndex == mDailyChartIndex
&& mHourlyHighlightSlotIndex != BatteryChartViewModel.SELECTED_INDEX_INVALID && mHourlyHighlightSlotIndex != SELECTED_INDEX_INVALID
&& mHourlyHighlightSlotIndex == mHourlyChartIndex); && mHourlyHighlightSlotIndex == mHourlyChartIndex);
} }
@@ -242,8 +245,8 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
} }
void selectHighlightSlotIndex() { void selectHighlightSlotIndex() {
if (mDailyHighlightSlotIndex == BatteryChartViewModel.SELECTED_INDEX_INVALID if (mDailyHighlightSlotIndex == SELECTED_INDEX_INVALID
|| mHourlyHighlightSlotIndex == BatteryChartViewModel.SELECTED_INDEX_INVALID) { || mHourlyHighlightSlotIndex == SELECTED_INDEX_INVALID) {
return; return;
} }
if (mDailyHighlightSlotIndex == mDailyChartIndex if (mDailyHighlightSlotIndex == mDailyChartIndex
@@ -258,8 +261,11 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
"onDailyChartSelect:%d, onHourlyChartSelect:%d", "onDailyChartSelect:%d, onHourlyChartSelect:%d",
mDailyChartIndex, mHourlyChartIndex)); mDailyChartIndex, mHourlyChartIndex));
refreshUi(); refreshUi();
// The highlight slot must be selected.
mHandler.post( mHandler.post(
() -> mDailyChartView.setAccessibilityPaneTitle(getAccessibilityAnnounceMessage())); () ->
mDailyChartView.setAccessibilityPaneTitle(
getAccessibilityAnnounceMessage(/* isSlotSelected= */ true)));
if (mOnSelectedIndexUpdatedListener != null) { if (mOnSelectedIndexUpdatedListener != null) {
mOnSelectedIndexUpdatedListener.onSelectedIndexUpdated(); mOnSelectedIndexUpdatedListener.onSelectedIndexUpdated();
} }
@@ -295,15 +301,16 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
} }
Log.d(TAG, "onDailyChartSelect:" + trapezoidIndex); Log.d(TAG, "onDailyChartSelect:" + trapezoidIndex);
mDailyChartIndex = trapezoidIndex; mDailyChartIndex = trapezoidIndex;
mHourlyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL; mHourlyChartIndex = SELECTED_INDEX_ALL;
refreshUi(); refreshUi();
mHandler.post( mHandler.post(
() -> () ->
mDailyChartView.setAccessibilityPaneTitle( mDailyChartView.setAccessibilityPaneTitle(
getAccessibilityAnnounceMessage())); getAccessibilityAnnounceMessage(
mDailyChartIndex != SELECTED_INDEX_ALL)));
mMetricsFeatureProvider.action( mMetricsFeatureProvider.action(
mPrefContext, mPrefContext,
trapezoidIndex == BatteryChartViewModel.SELECTED_INDEX_ALL trapezoidIndex == SELECTED_INDEX_ALL
? SettingsEnums.ACTION_BATTERY_USAGE_DAILY_SHOW_ALL ? SettingsEnums.ACTION_BATTERY_USAGE_DAILY_SHOW_ALL
: SettingsEnums.ACTION_BATTERY_USAGE_DAILY_TIME_SLOT, : SettingsEnums.ACTION_BATTERY_USAGE_DAILY_TIME_SLOT,
mDailyChartIndex); mDailyChartIndex);
@@ -314,7 +321,7 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
mHourlyChartView = hourlyChartView; mHourlyChartView = hourlyChartView;
mHourlyChartView.setOnSelectListener( mHourlyChartView.setOnSelectListener(
trapezoidIndex -> { 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. // This will happen when a daily slot and an hour slot are clicked together.
return; return;
} }
@@ -327,10 +334,11 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
mHandler.post( mHandler.post(
() -> () ->
mHourlyChartView.setAccessibilityPaneTitle( mHourlyChartView.setAccessibilityPaneTitle(
getAccessibilityAnnounceMessage())); getAccessibilityAnnounceMessage(
mHourlyChartIndex != SELECTED_INDEX_ALL)));
mMetricsFeatureProvider.action( mMetricsFeatureProvider.action(
mPrefContext, mPrefContext,
trapezoidIndex == BatteryChartViewModel.SELECTED_INDEX_ALL trapezoidIndex == SELECTED_INDEX_ALL
? SettingsEnums.ACTION_BATTERY_USAGE_SHOW_ALL ? SettingsEnums.ACTION_BATTERY_USAGE_SHOW_ALL
: SettingsEnums.ACTION_BATTERY_USAGE_TIME_SLOT, : SettingsEnums.ACTION_BATTERY_USAGE_TIME_SLOT,
mHourlyChartIndex); mHourlyChartIndex);
@@ -378,27 +386,27 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
} else { } else {
mDailyChartView.setVisibility(View.VISIBLE); mDailyChartView.setVisibility(View.VISIBLE);
if (mDailyChartIndex >= mDailyViewModel.size()) { if (mDailyChartIndex >= mDailyViewModel.size()) {
mDailyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL; mDailyChartIndex = SELECTED_INDEX_ALL;
} }
mDailyViewModel.setSelectedIndex(mDailyChartIndex); mDailyViewModel.setSelectedIndex(mDailyChartIndex);
mDailyViewModel.setHighlightSlotIndex(mDailyHighlightSlotIndex); mDailyViewModel.setHighlightSlotIndex(mDailyHighlightSlotIndex);
mDailyChartView.setViewModel(mDailyViewModel); mDailyChartView.setViewModel(mDailyViewModel);
} }
if (mDailyChartIndex == BatteryChartViewModel.SELECTED_INDEX_ALL) { if (mDailyChartIndex == SELECTED_INDEX_ALL) {
// Multiple days are selected, hide the hourly chart view. // Multiple days are selected, hide the hourly chart view.
animateBatteryHourlyChartView(/* visible= */ false); animateBatteryHourlyChartView(/* visible= */ false);
} else { } else {
animateBatteryHourlyChartView(/* visible= */ true); animateBatteryHourlyChartView(/* visible= */ true);
final BatteryChartViewModel hourlyViewModel = mHourlyViewModels.get(mDailyChartIndex); final BatteryChartViewModel hourlyViewModel = mHourlyViewModels.get(mDailyChartIndex);
if (mHourlyChartIndex >= hourlyViewModel.size()) { if (mHourlyChartIndex >= hourlyViewModel.size()) {
mHourlyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL; mHourlyChartIndex = SELECTED_INDEX_ALL;
} }
hourlyViewModel.setSelectedIndex(mHourlyChartIndex); hourlyViewModel.setSelectedIndex(mHourlyChartIndex);
hourlyViewModel.setHighlightSlotIndex( hourlyViewModel.setHighlightSlotIndex(
(mDailyChartIndex == mDailyHighlightSlotIndex) (mDailyChartIndex == mDailyHighlightSlotIndex)
? mHourlyHighlightSlotIndex ? mHourlyHighlightSlotIndex
: BatteryChartViewModel.SELECTED_INDEX_INVALID); : SELECTED_INDEX_INVALID);
mHourlyChartView.setViewModel(hourlyViewModel); mHourlyChartView.setViewModel(hourlyViewModel);
} }
} }
@@ -416,7 +424,7 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
isAccessibilityText isAccessibilityText
? mDailyViewModel.getContentDescription(mDailyChartIndex) ? mDailyViewModel.getContentDescription(mDailyChartIndex)
: mDailyViewModel.getFullText(mDailyChartIndex); : mDailyViewModel.getFullText(mDailyChartIndex);
if (mHourlyChartIndex == BatteryChartViewModel.SELECTED_INDEX_ALL) { if (mHourlyChartIndex == SELECTED_INDEX_ALL) {
return selectedDayText; return selectedDayText;
} }
@@ -441,15 +449,19 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
return ""; return "";
} }
if (mDailyChartIndex == BatteryChartViewModel.SELECTED_INDEX_ALL if (mDailyChartIndex == SELECTED_INDEX_ALL || mHourlyChartIndex == SELECTED_INDEX_ALL) {
|| mHourlyChartIndex == BatteryChartViewModel.SELECTED_INDEX_ALL) {
return mDailyViewModel.getSlotBatteryLevelText(mDailyChartIndex); return mDailyViewModel.getSlotBatteryLevelText(mDailyChartIndex);
} }
return mHourlyViewModels.get(mDailyChartIndex).getSlotBatteryLevelText(mHourlyChartIndex); 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 slotInformation = getSlotInformation(/* isAccessibilityText= */ true);
final String slotInformationMessage = final String slotInformationMessage =
slotInformation == null slotInformation == null
@@ -460,7 +472,8 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
final String batteryLevelPercentageMessage = getBatteryLevelPercentageInfo(); final String batteryLevelPercentageMessage = getBatteryLevelPercentageInfo();
return mPrefContext.getString( return mPrefContext.getString(
R.string.battery_usage_time_info_and_battery_level, R.string.battery_usage_status_time_info_and_battery_level,
selectedInformation,
slotInformationMessage, slotInformationMessage,
batteryLevelPercentageMessage); batteryLevelPercentageMessage);
} }
@@ -533,9 +546,8 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
} }
private boolean isAllSelected() { private boolean isAllSelected() {
return (isBatteryLevelDataInOneDay() return (isBatteryLevelDataInOneDay() || mDailyChartIndex == SELECTED_INDEX_ALL)
|| mDailyChartIndex == BatteryChartViewModel.SELECTED_INDEX_ALL) && mHourlyChartIndex == SELECTED_INDEX_ALL;
&& mHourlyChartIndex == BatteryChartViewModel.SELECTED_INDEX_ALL;
} }
@VisibleForTesting @VisibleForTesting
@@ -571,9 +583,7 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
return null; return null;
} }
BatteryDiffData allBatteryDiffData = BatteryDiffData allBatteryDiffData =
batteryUsageData batteryUsageData.get(SELECTED_INDEX_ALL).get(SELECTED_INDEX_ALL);
.get(BatteryChartViewModel.SELECTED_INDEX_ALL)
.get(BatteryChartViewModel.SELECTED_INDEX_ALL);
return allBatteryDiffData == null ? null : allBatteryDiffData.getAppDiffEntryList(); return allBatteryDiffData == null ? null : allBatteryDiffData.getAppDiffEntryList();
} }
@@ -613,12 +623,9 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
@Override @Override
public String generateSlotBatteryLevelText(List<Integer> levels, int index) { public String generateSlotBatteryLevelText(List<Integer> levels, int index) {
final int fromBatteryLevelIndex = final int fromBatteryLevelIndex = index == SELECTED_INDEX_ALL ? 0 : index;
index == BatteryChartViewModel.SELECTED_INDEX_ALL ? 0 : index;
final int toBatteryLevelIndex = final int toBatteryLevelIndex =
index == BatteryChartViewModel.SELECTED_INDEX_ALL index == SELECTED_INDEX_ALL ? levels.size() - 1 : index + 1;
? levels.size() - 1
: index + 1;
return mPrefContext.getString( return mPrefContext.getString(
R.string.battery_level_percentage, R.string.battery_level_percentage,
generateBatteryLevelText(levels.get(fromBatteryLevelIndex)), generateBatteryLevelText(levels.get(fromBatteryLevelIndex)),
@@ -687,9 +694,9 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
return index == timestamps.size() - 1 return index == timestamps.size() - 1
? generateText(timestamps, index) ? generateText(timestamps, index)
: mContext.getString( : mContext.getString(
R.string.battery_usage_timestamps_content_description, R.string.battery_usage_timestamps_content_description,
generateText(timestamps, index), generateText(timestamps, index),
generateText(timestamps, index + 1)); generateText(timestamps, index + 1));
} }
HourlyChartLabelTextGenerator updateSpecialCaseContext( 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.Utils.formatPercentage;
import static com.android.settings.fuelgauge.batteryusage.BatteryChartViewModel.AxisLabelPosition.BETWEEN_TRAPEZOIDS; 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 com.android.settingslib.fuelgauge.BatteryStatus.BATTERY_LEVEL_UNKNOWN;
import static java.lang.Math.abs; import static java.lang.Math.abs;
@@ -81,7 +83,7 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
getContext().getResources().getConfiguration().getLayoutDirection(); getContext().getResources().getConfiguration().getLayoutDirection();
private BatteryChartViewModel mViewModel; private BatteryChartViewModel mViewModel;
private int mHoveredIndex = BatteryChartViewModel.SELECTED_INDEX_INVALID; private int mHoveredIndex = SELECTED_INDEX_INVALID;
private int mDividerWidth; private int mDividerWidth;
private int mDividerHeight; private int mDividerHeight;
private float mTrapezoidVOffset; private float mTrapezoidVOffset;
@@ -245,9 +247,9 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
// sent here. // sent here.
return true; return true;
case MotionEvent.ACTION_HOVER_EXIT: case MotionEvent.ACTION_HOVER_EXIT:
if (mHoveredIndex != BatteryChartViewModel.SELECTED_INDEX_INVALID) { if (mHoveredIndex != SELECTED_INDEX_INVALID) {
sendAccessibilityEventForHover(AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); sendAccessibilityEventForHover(AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
mHoveredIndex = BatteryChartViewModel.SELECTED_INDEX_INVALID; // reset mHoveredIndex = SELECTED_INDEX_INVALID; // reset
invalidate(); invalidate();
} }
// Ignore the super.onHoverEvent() because the hovered trapezoid has already been // 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) { public void onHoverChanged(boolean hovered) {
super.onHoverChanged(hovered); super.onHoverChanged(hovered);
if (!hovered) { if (!hovered) {
mHoveredIndex = BatteryChartViewModel.SELECTED_INDEX_INVALID; // reset mHoveredIndex = SELECTED_INDEX_INVALID; // reset
invalidate(); invalidate();
} }
} }
@@ -295,9 +297,7 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
if (mOnSelectListener != null) { if (mOnSelectListener != null) {
// Selects all if users click the same trapezoid item two times. // Selects all if users click the same trapezoid item two times.
mOnSelectListener.onSelect( mOnSelectListener.onSelect(
index == mViewModel.selectedIndex() index == mViewModel.selectedIndex() ? SELECTED_INDEX_ALL : index);
? BatteryChartViewModel.SELECTED_INDEX_ALL
: index);
} }
view.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK); view.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK);
} }
@@ -332,8 +332,8 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
setBackgroundColor(Color.TRANSPARENT); setBackgroundColor(Color.TRANSPARENT);
mTrapezoidSolidColor = Utils.getColorAccentDefaultColor(context); mTrapezoidSolidColor = Utils.getColorAccentDefaultColor(context);
mTrapezoidColor = Utils.getDisabled(context, mTrapezoidSolidColor); mTrapezoidColor = Utils.getDisabled(context, mTrapezoidSolidColor);
mTrapezoidHoverColor = context.getColor( mTrapezoidHoverColor =
com.android.internal.R.color.materialColorSecondaryContainer); context.getColor(com.android.internal.R.color.materialColorSecondaryContainer);
// Initializes the divider line paint. // Initializes the divider line paint.
final Resources resources = getContext().getResources(); final Resources resources = getContext().getResources();
mDividerWidth = resources.getDimensionPixelSize(R.dimen.chartview_divider_width); 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. // Configures the trapezoid paint color.
final int trapezoidColor = final int trapezoidColor =
(mViewModel.selectedIndex() == index (mViewModel.selectedIndex() == index
|| mViewModel.selectedIndex() || mViewModel.selectedIndex() == SELECTED_INDEX_ALL)
== BatteryChartViewModel.SELECTED_INDEX_ALL)
? mTrapezoidSolidColor ? mTrapezoidSolidColor
: mTrapezoidColor; : mTrapezoidColor;
final boolean isHoverState = final boolean isHoverState =
@@ -659,9 +658,7 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
} }
private boolean isHighlightSlotValid() { private boolean isHighlightSlotValid() {
return mViewModel != null return mViewModel != null && mViewModel.getHighlightSlotIndex() != SELECTED_INDEX_INVALID;
&& mViewModel.getHighlightSlotIndex()
!= BatteryChartViewModel.SELECTED_INDEX_INVALID;
} }
private void drawTransomLine(Canvas canvas) { 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. // Searches the corresponding trapezoid index from x location.
private int getTrapezoidIndex(float x) { private int getTrapezoidIndex(float x) {
if (mTrapezoidSlots == null) { if (mTrapezoidSlots == null) {
return BatteryChartViewModel.SELECTED_INDEX_INVALID; return SELECTED_INDEX_INVALID;
} }
for (int index = 0; index < mTrapezoidSlots.length; index++) { for (int index = 0; index < mTrapezoidSlots.length; index++) {
final TrapezoidSlot slot = mTrapezoidSlots[index]; final TrapezoidSlot slot = mTrapezoidSlots[index];
@@ -723,7 +720,7 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
return index; return index;
} }
} }
return BatteryChartViewModel.SELECTED_INDEX_INVALID; return SELECTED_INDEX_INVALID;
} }
private void initializeAxisLabelsBounds() { private void initializeAxisLabelsBounds() {
@@ -796,7 +793,11 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
childInfo.setText(slotTimeInfo); childInfo.setText(slotTimeInfo);
childInfo.setContentDescription( childInfo.setContentDescription(
mContext.getString( 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, slotTimeInfo,
batteryLevelInfo)); batteryLevelInfo));
childInfo.setAccessibilityFocused(virtualViewId == mAccessibilityFocusNodeViewId); childInfo.setAccessibilityFocused(virtualViewId == mAccessibilityFocusNodeViewId);

View File

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

View File

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

View File

@@ -124,6 +124,7 @@ public class ConfigDialogFragment extends InstrumentedDialogFragment implements
VpnProfile profile = dialog.getProfile(); VpnProfile profile = dialog.getProfile();
if (button == DialogInterface.BUTTON_POSITIVE) { if (button == DialogInterface.BUTTON_POSITIVE) {
if (!dialog.validate()) return;
// Possibly throw up a dialog to explain lockdown VPN. // Possibly throw up a dialog to explain lockdown VPN.
final boolean shouldLockdown = dialog.isVpnAlwaysOn(); final boolean shouldLockdown = dialog.isVpnAlwaysOn();
final boolean shouldConnect = shouldLockdown || !dialog.isEditing(); 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.WifiPrivacyPreferenceController;
import com.android.settings.wifi.details2.WifiPrivacyPreferenceController2; import com.android.settings.wifi.details2.WifiPrivacyPreferenceController2;
import com.android.settings.wifi.dpp.WifiDppUtils; 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;
import com.android.settingslib.utils.ThreadUtils; import com.android.settingslib.utils.ThreadUtils;
import com.android.wifi.flags.Flags; import com.android.wifi.flags.Flags;
@@ -229,7 +229,7 @@ public class WifiConfigController2 implements TextWatcher,
private final boolean mHideMeteredAndPrivacy; private final boolean mHideMeteredAndPrivacy;
private final WifiManager mWifiManager; private final WifiManager mWifiManager;
private final AndroidKeystoreAliasLoader mAndroidKeystoreAliasLoader; private final AndroidKeystoreAliasLoader mAndroidKeystoreAliasLoader;
private SsidInputGroup mSsidInputGroup; private TextInputGroup mSsidInputGroup;
private final Context mContext; private final Context mContext;
@@ -299,7 +299,8 @@ public class WifiConfigController2 implements TextWatcher,
wepWarningLayout.setVisibility(View.VISIBLE); 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); mSsidScanButton = (ImageButton) mView.findViewById(R.id.ssid_scanner_button);
mIpSettingsSpinner = (Spinner) mView.findViewById(R.id.ip_settings); mIpSettingsSpinner = (Spinner) mView.findViewById(R.id.ip_settings);
mIpSettingsSpinner.setOnItemSelectedListener(this); mIpSettingsSpinner.setOnItemSelectedListener(this);

View File

@@ -28,7 +28,7 @@ import android.widget.TextView;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import com.android.settings.R; 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.settings.wifi.utils.WifiDialogHelper;
import com.android.settingslib.RestrictedLockUtils; import com.android.settingslib.RestrictedLockUtils;
import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.RestrictedLockUtilsInternal;
@@ -120,7 +120,8 @@ public class WifiDialog extends AlertDialog implements WifiConfigUiBase,
} }
mDialogHelper = new WifiDialogHelper(this, 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 @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.Editable
import android.text.TextWatcher import android.text.TextWatcher
import android.util.Log
import android.view.View import android.view.View
import android.widget.EditText import android.widget.EditText
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
@@ -27,13 +28,17 @@ open class TextInputGroup(
private val view: View, private val view: View,
private val layoutId: Int, private val layoutId: Int,
private val editTextId: Int, private val editTextId: Int,
private val errorMessageId: Int,
) { ) {
private val View.layout: TextInputLayout? val layout: TextInputLayout
get() = findViewById(layoutId) get() = view.requireViewById(layoutId)
private val View.editText: EditText? val editText: EditText
get() = findViewById(editTextId) get() = view.requireViewById(editTextId)
val errorMessage: String
get() = view.context.getString(errorMessageId)
private val textWatcher = private val textWatcher =
object : TextWatcher { object : TextWatcher {
@@ -42,7 +47,7 @@ open class TextInputGroup(
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) { override fun afterTextChanged(s: Editable?) {
view.layout?.isErrorEnabled = false layout.isErrorEnabled = false
} }
} }
@@ -51,18 +56,37 @@ open class TextInputGroup(
} }
fun addTextChangedListener(watcher: TextWatcher) { fun addTextChangedListener(watcher: TextWatcher) {
view.editText?.addTextChangedListener(watcher) editText.addTextChangedListener(watcher)
} }
fun getText(): String { var text: String
return view.editText?.text?.toString() ?: "" 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) { companion object {
view.editText?.setText(text) const val TAG = "TextInputGroup"
}
fun setError(errorMessage: String?) {
view.layout?.apply { error = errorMessage }
} }
} }

View File

@@ -21,7 +21,7 @@ import androidx.appcompat.app.AlertDialog
class WifiDialogHelper( class WifiDialogHelper(
alertDialog: AlertDialog, alertDialog: AlertDialog,
private val ssidInputGroup: SsidInputGroup? = null, private val ssidInputGroup: TextInputGroup? = null,
) : AlertDialogHelper(alertDialog) { ) : AlertDialogHelper(alertDialog) {
override fun canDismiss(): Boolean { 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.Intent;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.icu.text.CaseMap; import android.icu.text.CaseMap;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.EnableFlags;
@@ -56,12 +57,14 @@ import androidx.fragment.app.FragmentActivity;
import androidx.preference.Preference; import androidx.preference.Preference;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import androidx.preference.PreferenceScreen; import androidx.preference.PreferenceScreen;
import androidx.preference.PreferenceViewHolder;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import com.android.settings.R; import com.android.settings.R;
import com.android.settings.flags.Flags; import com.android.settings.flags.Flags;
import com.android.settings.testutils.shadow.ShadowAccessibilityManager; import com.android.settings.testutils.shadow.ShadowAccessibilityManager;
import com.android.settings.testutils.shadow.ShadowFragment; import com.android.settings.testutils.shadow.ShadowFragment;
import com.android.settingslib.widget.IllustrationPreference;
import com.android.settingslib.widget.TopIntroPreference; import com.android.settingslib.widget.TopIntroPreference;
import com.google.android.setupcompat.util.WizardManagerHelper; import com.google.android.setupcompat.util.WizardManagerHelper;
@@ -79,6 +82,7 @@ import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config; import org.robolectric.annotation.Config;
import org.robolectric.shadow.api.Shadow; import org.robolectric.shadow.api.Shadow;
import org.robolectric.shadows.ShadowApplication; import org.robolectric.shadows.ShadowApplication;
import org.robolectric.shadows.ShadowLooper;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
@@ -315,6 +319,45 @@ public class ToggleFeaturePreferenceFragmentTest {
assertThat(mFragment.getPreferenceScreen().getPreferenceCount()).isEqualTo(0); 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 @Test
@EnableFlags(Flags.FLAG_ACCESSIBILITY_SHOW_APP_INFO_BUTTON) @EnableFlags(Flags.FLAG_ACCESSIBILITY_SHOW_APP_INFO_BUTTON)
public void createAppInfoPreference_withValidComponentName() { public void createAppInfoPreference_withValidComponentName() {

View File

@@ -613,6 +613,24 @@ public class ToggleScreenMagnificationPreferenceFragmentTest {
verify(dialogDelegate).getDialogMetricsCategory(dialogId); 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 @Test
public void getMetricsCategory_returnsCorrectCategory() { public void getMetricsCategory_returnsCorrectCategory() {
ToggleScreenMagnificationPreferenceFragment fragment = ToggleScreenMagnificationPreferenceFragment fragment =
@@ -826,6 +844,7 @@ public class ToggleScreenMagnificationPreferenceFragmentTest {
MagnificationOneFingerPanningPreferenceController.PREF_KEY, MagnificationOneFingerPanningPreferenceController.PREF_KEY,
MagnificationAlwaysOnPreferenceController.PREF_KEY, MagnificationAlwaysOnPreferenceController.PREF_KEY,
MagnificationJoystickPreferenceController.PREF_KEY, MagnificationJoystickPreferenceController.PREF_KEY,
MagnificationCursorFollowingModePreferenceController.PREF_KEY,
MagnificationFeedbackPreferenceController.PREF_KEY); MagnificationFeedbackPreferenceController.PREF_KEY);
final List<SearchIndexableRaw> rawData = ToggleScreenMagnificationPreferenceFragment final List<SearchIndexableRaw> rawData = ToggleScreenMagnificationPreferenceFragment
@@ -881,7 +900,9 @@ public class ToggleScreenMagnificationPreferenceFragmentTest {
@EnableFlags({ @EnableFlags({
com.android.settings.accessibility.Flags.FLAG_FIX_A11Y_SETTINGS_SEARCH, com.android.settings.accessibility.Flags.FLAG_FIX_A11Y_SETTINGS_SEARCH,
Flags.FLAG_ENABLE_MAGNIFICATION_ONE_FINGER_PANNING_GESTURE, 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() { public void getNonIndexableKeys_hasShortcutAndAllFeaturesEnabled_allItemsSearchable() {
mShadowAccessibilityManager.setAccessibilityShortcutTargets( mShadowAccessibilityManager.setAccessibilityShortcutTargets(
TRIPLETAP, List.of(MAGNIFICATION_CONTROLLER_NAME)); TRIPLETAP, List.of(MAGNIFICATION_CONTROLLER_NAME));

View File

@@ -34,6 +34,7 @@ import android.content.Context;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.Looper; import android.os.Looper;
import android.os.UserManager; import android.os.UserManager;
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule; import android.platform.test.flag.junit.SetFlagsRule;
import android.util.Pair; import android.util.Pair;
@@ -175,6 +176,7 @@ public class BluetoothDevicePreferenceTest {
} }
@Test @Test
@DisableFlags(Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI)
public void onClicked_deviceNotBonded_shouldLogBluetoothPairEvent() { public void onClicked_deviceNotBonded_shouldLogBluetoothPairEvent() {
when(mCachedBluetoothDevice.isConnected()).thenReturn(false); when(mCachedBluetoothDevice.isConnected()).thenReturn(false);
when(mCachedBluetoothDevice.getBondState()).thenReturn(BluetoothDevice.BOND_NONE); when(mCachedBluetoothDevice.getBondState()).thenReturn(BluetoothDevice.BOND_NONE);
@@ -192,6 +194,7 @@ public class BluetoothDevicePreferenceTest {
} }
@Test @Test
@DisableFlags(Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI)
public void onClicked_deviceNotBonded_shouldLogBluetoothPairEventAndPairWithoutNameEvent() { public void onClicked_deviceNotBonded_shouldLogBluetoothPairEventAndPairWithoutNameEvent() {
when(mCachedBluetoothDevice.isConnected()).thenReturn(false); when(mCachedBluetoothDevice.isConnected()).thenReturn(false);
when(mCachedBluetoothDevice.getBondState()).thenReturn(BluetoothDevice.BOND_NONE); 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.anyString;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy; import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
@@ -52,7 +51,9 @@ import android.media.session.ISession;
import android.media.session.ISessionController; import android.media.session.ISessionController;
import android.media.session.MediaSessionManager; import android.media.session.MediaSessionManager;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder; import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException; import android.os.RemoteException;
import android.platform.test.flag.junit.SetFlagsRule; import android.platform.test.flag.junit.SetFlagsRule;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
@@ -81,14 +82,12 @@ import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule; import org.mockito.junit.MockitoRule;
import org.robolectric.RobolectricTestRunner; import org.robolectric.RobolectricTestRunner;
import org.robolectric.android.util.concurrent.InlineExecutorService;
import org.robolectric.annotation.Config; import org.robolectric.annotation.Config;
import org.robolectric.shadow.api.Shadow; import org.robolectric.shadow.api.Shadow;
import org.robolectric.util.ReflectionHelpers; import org.robolectric.util.ReflectionHelpers;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Set;
@RunWith(RobolectricTestRunner.class) @RunWith(RobolectricTestRunner.class)
@Config( @Config(
@@ -122,6 +121,7 @@ public class AudioStreamMediaServiceTest {
@Mock private PackageManager mPackageManager; @Mock private PackageManager mPackageManager;
@Mock private DisplayMetrics mDisplayMetrics; @Mock private DisplayMetrics mDisplayMetrics;
@Mock private Context mContext; @Mock private Context mContext;
@Mock private Handler mHandler;
private FakeFeatureFactory mFeatureFactory; private FakeFeatureFactory mFeatureFactory;
private AudioStreamMediaService mAudioStreamMediaService; private AudioStreamMediaService mAudioStreamMediaService;
@@ -145,11 +145,18 @@ public class AudioStreamMediaServiceTest {
when(mCachedBluetoothDevice.getName()).thenReturn(DEVICE_NAME); when(mCachedBluetoothDevice.getName()).thenReturn(DEVICE_NAME);
when(mLocalBluetoothProfileManager.getVolumeControlProfile()) when(mLocalBluetoothProfileManager.getVolumeControlProfile())
.thenReturn(mVolumeControlProfile); .thenReturn(mVolumeControlProfile);
when(mHandler.post(any(Runnable.class))).thenAnswer(invocation -> {
mAudioStreamMediaService = spy(new AudioStreamMediaService()); ((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, "mBase", mContext);
ReflectionHelpers.setField(
mAudioStreamMediaService, "mExecutor", new InlineExecutorService());
when(mAudioStreamMediaService.getSystemService(anyString())) when(mAudioStreamMediaService.getSystemService(anyString()))
.thenReturn(mMediaSessionManager); .thenReturn(mMediaSessionManager);
when(mMediaSessionManager.createSession(any(), anyString(), any())).thenReturn(mISession); when(mMediaSessionManager.createSession(any(), anyString(), any())).thenReturn(mISession);
@@ -391,31 +398,6 @@ public class AudioStreamMediaServiceTest {
verify(mAudioStreamMediaService).stopSelf(); 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 @Test
public void mediaSessionCallback_onPause_setVolume() { public void mediaSessionCallback_onPause_setVolume() {
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); 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 com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import android.content.Context;
import android.content.res.Resources;
import android.telephony.TelephonyManager;
import com.android.settings.R;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@@ -34,28 +41,66 @@ import org.robolectric.util.ReflectionHelpers;
@RunWith(RobolectricTestRunner.class) @RunWith(RobolectricTestRunner.class)
public class NetworkResetPreferenceControllerTest { public class NetworkResetPreferenceControllerTest {
@Mock
private TelephonyManager mTelephonyManager;
@Mock @Mock
private NetworkResetRestrictionChecker mRestrictionChecker; private NetworkResetRestrictionChecker mRestrictionChecker;
private NetworkResetPreferenceController mController; private NetworkResetPreferenceController mController;
private Context mContext;
private Resources mResources;
@Before @Before
public void setUp() { public void setUp() {
MockitoAnnotations.initMocks(this); 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); 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 @Test
public void testIsAvailable_shouldReturnTrueWhenNoUserRestriction() { public void testIsAvailable_showSimInfo_notWifiOnly() {
when(mRestrictionChecker.isRestrictionEnforcedByAdmin()).thenReturn(true); 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); when(mRestrictionChecker.hasUserRestriction()).thenReturn(true);
assertThat(mController.isAvailable()).isFalse(); assertThat(mController.isAvailable()).isFalse();
verify(mRestrictionChecker, never()).isRestrictionEnforcedByAdmin();
}
@Test
public void testIsAvailable_noUserRestriction() {
when(mRestrictionChecker.isRestrictionEnforcedByAdmin()).thenReturn(true);
when(mRestrictionChecker.hasUserRestriction()).thenReturn(false); when(mRestrictionChecker.hasUserRestriction()).thenReturn(false);
assertThat(mController.isAvailable()).isTrue(); assertThat(mController.isAvailable()).isTrue();
verify(mRestrictionChecker, never()).isRestrictionEnforcedByAdmin(); verify(mRestrictionChecker, never()).isRestrictionEnforcedByAdmin();
} }
} }