Snap for 13174046 from 305a8e0ea7 to 25Q2-release
Change-Id: Ib56f1d75610e715634a3d0e9dc0697182123e995
This commit is contained in:
@@ -21,9 +21,9 @@
|
||||
android:padding="?android:attr/dialogPreferredPadding">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/accessibility_dialog_header_text_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/accessibility_magnification_area_settings_message"
|
||||
android:textSize="16sp"
|
||||
style="?android:attr/textAppearanceMedium"
|
||||
android:textColor="?android:attr/textColorAlertDialogListItem"/>
|
||||
@@ -53,6 +53,8 @@
|
||||
android:id="@+id/name_layout"
|
||||
android:hint="@string/vpn_name"
|
||||
app:endIconMode="clear_text"
|
||||
app:helperTextEnabled="true"
|
||||
app:helperText="@string/vpn_required"
|
||||
app:errorEnabled="true">
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
style="@style/vpn_value"
|
||||
@@ -73,6 +75,8 @@
|
||||
android:id="@+id/server_layout"
|
||||
android:hint="@string/vpn_server"
|
||||
app:endIconMode="clear_text"
|
||||
app:helperTextEnabled="true"
|
||||
app:helperText="@string/vpn_required"
|
||||
app:errorEnabled="true">
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
style="@style/vpn_value"
|
||||
@@ -90,7 +94,7 @@
|
||||
android:hint="@string/vpn_ipsec_identifier"
|
||||
app:endIconMode="clear_text"
|
||||
app:helperTextEnabled="true"
|
||||
app:helperText="@string/vpn_not_used"
|
||||
app:helperText="@string/vpn_required"
|
||||
app:errorEnabled="true">
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
style="@style/vpn_value"
|
||||
@@ -108,6 +112,8 @@
|
||||
android:id="@+id/ipsec_secret_layout"
|
||||
android:hint="@string/vpn_ipsec_secret"
|
||||
app:endIconMode="password_toggle"
|
||||
app:helperTextEnabled="true"
|
||||
app:helperText="@string/vpn_required"
|
||||
app:errorEnabled="true">
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
style="@style/vpn_value"
|
||||
@@ -183,7 +189,7 @@
|
||||
android:hint="@string/proxy_hostname_label"
|
||||
app:endIconMode="clear_text"
|
||||
app:helperTextEnabled="true"
|
||||
app:helperText="@string/proxy_hostname_hint"
|
||||
app:helperText="@string/vpn_optional"
|
||||
app:errorEnabled="true">
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
style="@style/vpn_value"
|
||||
@@ -197,7 +203,7 @@
|
||||
android:hint="@string/proxy_port_label"
|
||||
app:endIconMode="clear_text"
|
||||
app:helperTextEnabled="true"
|
||||
app:helperText="@string/proxy_port_hint"
|
||||
app:helperText="@string/vpn_optional"
|
||||
app:errorEnabled="true">
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
style="@style/vpn_value"
|
||||
@@ -217,6 +223,8 @@
|
||||
android:id="@+id/username_layout"
|
||||
android:hint="@string/vpn_username"
|
||||
app:endIconMode="clear_text"
|
||||
app:helperTextEnabled="true"
|
||||
app:helperText="@string/vpn_optional"
|
||||
app:errorEnabled="true">
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
style="@style/vpn_value"
|
||||
@@ -228,6 +236,8 @@
|
||||
android:id="@+id/password_layout"
|
||||
android:hint="@string/vpn_password"
|
||||
app:endIconMode="password_toggle"
|
||||
app:helperTextEnabled="true"
|
||||
app:helperText="@string/vpn_optional"
|
||||
app:errorEnabled="true">
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
style="@style/vpn_value"
|
||||
|
||||
@@ -5220,6 +5220,16 @@
|
||||
<string name="accessibility_screen_magnification_title">Magnification</string>
|
||||
<!-- Title for accessibility shortcut preference for magnification. [CHAR LIMIT=60] -->
|
||||
<string name="accessibility_screen_magnification_shortcut_title">Magnification shortcut</string>
|
||||
<!-- Title of cursor following mode preference for magnification. [CHAR LIMIT=60] -->
|
||||
<string name="accessibility_magnification_cursor_following_title">Cursor following</string>
|
||||
<!-- Header message of cursor following mode dialog for magnification. [CHAR LIMIT=none] -->
|
||||
<string name="accessibility_magnification_cursor_following_header">Choose how Magnification follows your cursor.</string>
|
||||
<!-- Option title of cursor following continuous mode in the mode selection dialog. [CHAR LIMIT=none] -->
|
||||
<string name="accessibility_magnification_cursor_following_continuous">Move screen continuously as mouse moves</string>
|
||||
<!-- Option title of cursor following center mode in the mode selection dialog. [CHAR LIMIT=none] -->
|
||||
<string name="accessibility_magnification_cursor_following_center">Move screen keeping mouse at center of screen</string>
|
||||
<!-- Option title of cursor following edge mode in the mode selection dialog. [CHAR LIMIT=none] -->
|
||||
<string name="accessibility_magnification_cursor_following_edge">Move screen when mouse touches edges of screen</string>
|
||||
<!-- Title for accessibility follow typing preference for magnification. [CHAR LIMIT=35] -->
|
||||
<string name="accessibility_screen_magnification_follow_typing_title">Magnify typing</string>
|
||||
<!-- Summary for accessibility follow typing preference for magnification. [CHAR LIMIT=none] -->
|
||||
@@ -6601,8 +6611,12 @@
|
||||
<string name="battery_usage_timestamps_content_description"><xliff:g id="from_timestamp">%1$s</xliff:g> to <xliff:g id="to_timestamp">%2$s</xliff:g></string>
|
||||
<!-- [CHAR_LIMIT=NONE] The first slot is a week day (e.g. "Monday"); the second slot is a hourly time span (e.g. "6 AM - 8 AM"). -->
|
||||
<string name="battery_usage_day_and_hour"><xliff:g id="day">%1$s</xliff:g> <xliff:g id="hour">%2$s</xliff:g></string>
|
||||
<!-- [CHAR_LIMIT=NONE] Accessibility content description for each slot in battery chart view. -->
|
||||
<string name="battery_usage_time_info_and_battery_level"><xliff:g id="time_info" example="Battery usage for Monday 6 AM - 8 AM">%1$s</xliff:g> <xliff:g id="battery_level" example="Battery level percentage from 83% to 59%">%2$s</xliff:g></string>
|
||||
<!-- [CHAR_LIMIT=NONE] Accessibility content description for each slot in battery chart view. Please reuse the words in tc/6732629268310936155 -->
|
||||
<string name="battery_usage_status_time_info_and_battery_level"><xliff:g id="selected_status" example="Selected">%1$s</xliff:g>, <xliff:g id="time_info" example="Battery usage for Monday 6 AM - 8 AM">%2$s</xliff:g> <xliff:g id="battery_level" example="Battery level percentage from 83% to 59%">%3$s</xliff:g></string>
|
||||
<!-- [CHAR_LIMIT=NONE] Accessibility content description for the battery usage slot is selected. -->
|
||||
<string name="battery_chart_slot_status_selected">Selected</string>
|
||||
<!-- [CHAR_LIMIT=NONE] Accessibility content description for the battery usage slot is not selected -->
|
||||
<string name="battery_chart_slot_status_unselected">Unselected</string>
|
||||
<!-- [CHAR_LIMIT=NONE] Accessibility content description for battery chart view. -->
|
||||
<string name="battery_usage_chart">Battery usage chart</string>
|
||||
<!-- [CHAR_LIMIT=NONE] Accessibility content description for daily battery chart view. -->
|
||||
@@ -7276,6 +7290,12 @@ Data usage charges may apply.</string>
|
||||
generic error. [CHAR LIMIT=120] -->
|
||||
<string name="vpn_always_on_invalid_reason_other">The information entered doesn\'t support
|
||||
always-on VPN</string>
|
||||
<!-- Hint for an optional field in a VPN profile. [CHAR LIMIT=40] -->
|
||||
<string name="vpn_optional">(optional)</string>
|
||||
<!-- Hint for a required field in a VPN profile. [CHAR LIMIT=40] -->
|
||||
<string name="vpn_required">(required)</string>
|
||||
<!-- Error message displayed below the VPN EditText when the filed is required. [CHAR LIMIT=NONE] -->
|
||||
<string name="vpn_field_required">The field is required</string>
|
||||
|
||||
<!-- Button label to cancel changing a VPN profile. [CHAR LIMIT=40] -->
|
||||
<string name="vpn_cancel">Cancel</string>
|
||||
@@ -12924,12 +12944,12 @@ Data usage charges may apply.</string>
|
||||
<!-- Title for a toggle that enables freeform windows. Freeform windows enables users to freely arrange and resize overlapping apps. [CHAR LIMIT=50] -->
|
||||
<string name="enable_desktop_mode">Enable freeform windows</string>
|
||||
|
||||
<!-- Title for a toggle that enables desktop experience features. This includes desktop view and connected displays. [CHAR LIMIT=50] -->
|
||||
<!-- Title for a toggle that enables desktop experience features. This includes desktop windowing and connected displays. [CHAR LIMIT=50] -->
|
||||
<string name="enable_desktop_experience_features">Enable desktop experience features</string>
|
||||
<!-- Summary for a toggle that enables desktop experience features when the device itself can show the desktop (but it is not available without the developer option). [CHAR LIMIT=NONE] -->
|
||||
<string name="enable_desktop_experience_features_summary_with_desktop">Enable Desktop View on the device and on secondary displays.</string>
|
||||
<!-- Summary for a toggle that enables desktop experience features when desktop views don't need to be enable. [CHAR LIMIT=NONE] -->
|
||||
<string name="enable_desktop_experience_features_summary_without_desktop">Enable Desktop View on secondary displays.</string>
|
||||
<string name="enable_desktop_experience_features_summary_with_desktop">Enable desktop windowing on the device and on secondary displays.</string>
|
||||
<!-- Summary for a toggle that enables desktop experience features when desktop windowing doesn't need to be enabled. [CHAR LIMIT=NONE] -->
|
||||
<string name="enable_desktop_experience_features_summary_without_desktop">Enable desktop windowing on secondary displays.</string>
|
||||
|
||||
<!-- Title for a toggle that enables freeform windows on secondary display. Freeform windows enables users to freely arrange and resize overlapping apps. [CHAR LIMIT=50] -->
|
||||
<string name="enable_desktop_mode_on_secondary_display">Enable freeform windows on secondary display</string>
|
||||
@@ -14283,4 +14303,6 @@ Data usage charges may apply.</string>
|
||||
<string name="supervision_add_forgot_pin_preference_title">Forgot PIN</string>
|
||||
<!-- Title for web content filters entry [CHAR LIMIT=60] -->
|
||||
<string name="supervision_web_content_filters_title">Web content filters</string>
|
||||
<!-- Generic content description that is attached to the preview illustration at the top of an Accessibility feature toggle page. [CHAR LIMIT=NONE] -->
|
||||
<string name="accessibility_illustration_content_description"><xliff:g id="feature" example="Select to Speak">%1$s</xliff:g> animation</string>
|
||||
</resources>
|
||||
|
||||
@@ -104,6 +104,11 @@ public class AccessibilityDialogUtils {
|
||||
* screen / Switch between full and partial screen > Save.
|
||||
*/
|
||||
int DIALOG_MAGNIFICATION_TRIPLE_TAP_WARNING = 1011;
|
||||
|
||||
/**
|
||||
* OPEN: Settings > Accessibility > Magnification > Cursor following.
|
||||
*/
|
||||
int DIALOG_MAGNIFICATION_CURSOR_FOLLOWING_MODE = 1012;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -176,8 +176,12 @@ public class MagnificationModePreferenceController extends BasePreferenceControl
|
||||
mContext, mModeInfos, this::onMagnificationModeSelected);
|
||||
|
||||
final View headerView = LayoutInflater.from(mContext).inflate(
|
||||
R.layout.accessibility_magnification_mode_header,
|
||||
getMagnificationModesListView(), /* attachToRoot= */false);
|
||||
R.layout.accessibility_dialog_header, getMagnificationModesListView(),
|
||||
/* attachToRoot= */false);
|
||||
final TextView textView = Preconditions.checkNotNull(headerView.findViewById(
|
||||
R.id.accessibility_dialog_header_text_view));
|
||||
textView.setText(
|
||||
mContext.getString(R.string.accessibility_magnification_area_settings_message));
|
||||
getMagnificationModesListView().addHeaderView(headerView, /* data= */null,
|
||||
/* isSelectable= */false);
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ import com.android.settings.dashboard.DashboardFragment;
|
||||
import com.android.settings.flags.Flags;
|
||||
import com.android.settings.widget.SettingsMainSwitchBar;
|
||||
import com.android.settings.widget.SettingsMainSwitchPreference;
|
||||
import com.android.settingslib.utils.ThreadUtils;
|
||||
import com.android.settingslib.widget.IllustrationPreference;
|
||||
import com.android.settingslib.widget.TopIntroPreference;
|
||||
|
||||
@@ -311,6 +312,11 @@ public abstract class ToggleFeaturePreferenceFragment extends DashboardFragment
|
||||
return getString(R.string.accessibility_shortcut_title, mFeatureName);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
CharSequence getContentDescriptionForAnimatedIllustration() {
|
||||
return getString(R.string.accessibility_illustration_content_description, mFeatureName);
|
||||
}
|
||||
|
||||
protected void onPreferenceToggled(String preferenceKey, boolean enabled) {
|
||||
}
|
||||
|
||||
@@ -427,22 +433,38 @@ public abstract class ToggleFeaturePreferenceFragment extends DashboardFragment
|
||||
|
||||
return drawable;
|
||||
}
|
||||
|
||||
private void initAnimatedImagePreference() {
|
||||
if (mImageUri == null) {
|
||||
initAnimatedImagePreference(mImageUri, new IllustrationPreference(getPrefContext()));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void initAnimatedImagePreference(
|
||||
@Nullable Uri imageUri,
|
||||
@NonNull IllustrationPreference preference) {
|
||||
if (imageUri == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final int displayHalfHeight =
|
||||
AccessibilityUtil.getDisplayBounds(getPrefContext()).height() / 2;
|
||||
final IllustrationPreference illustrationPreference =
|
||||
new IllustrationPreference(getPrefContext());
|
||||
illustrationPreference.setImageUri(mImageUri);
|
||||
illustrationPreference.setSelectable(false);
|
||||
illustrationPreference.setMaxHeight(displayHalfHeight);
|
||||
illustrationPreference.setKey(KEY_ANIMATED_IMAGE);
|
||||
|
||||
getPreferenceScreen().addPreference(illustrationPreference);
|
||||
preference.setImageUri(imageUri);
|
||||
preference.setSelectable(false);
|
||||
preference.setMaxHeight(displayHalfHeight);
|
||||
preference.setKey(KEY_ANIMATED_IMAGE);
|
||||
preference.setOnBindListener(view -> {
|
||||
// isAnimatable is decided in
|
||||
// {@link IllustrationPreference#onBindViewHolder(PreferenceViewHolder)}. Therefore, we
|
||||
// wait until the view is bond to set the content description for it.
|
||||
// The content description is added for an animation illustration only. Since the static
|
||||
// images are decorative.
|
||||
ThreadUtils.getUiThreadHandler().post(() -> {
|
||||
if (preference.isAnimatable()) {
|
||||
preference.setContentDescription(
|
||||
getContentDescriptionForAnimatedIllustration());
|
||||
}
|
||||
});
|
||||
});
|
||||
getPreferenceScreen().addPreference(preference);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
|
||||
@@ -93,6 +93,8 @@ public class ToggleScreenMagnificationPreferenceFragment extends
|
||||
private TouchExplorationStateChangeListener mTouchExplorationStateChangeListener;
|
||||
@Nullable
|
||||
private DialogCreatable mMagnificationModeDialogDelegate;
|
||||
@Nullable
|
||||
private DialogCreatable mMagnificationCursorFollowingModeDialogDelegate;
|
||||
|
||||
@Nullable
|
||||
MagnificationOneFingerPanningPreferenceController mOneFingerPanningPreferenceController;
|
||||
@@ -104,6 +106,12 @@ public class ToggleScreenMagnificationPreferenceFragment extends
|
||||
mMagnificationModeDialogDelegate = delegate;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setMagnificationCursorFollowingModeDialogDelegate(
|
||||
@NonNull DialogCreatable delegate) {
|
||||
mMagnificationCursorFollowingModeDialogDelegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -186,6 +194,9 @@ public class ToggleScreenMagnificationPreferenceFragment extends
|
||||
case DialogEnums.DIALOG_MAGNIFICATION_TRIPLE_TAP_WARNING:
|
||||
return Preconditions.checkNotNull(mMagnificationModeDialogDelegate)
|
||||
.onCreateDialog(dialogId);
|
||||
case DialogEnums.DIALOG_MAGNIFICATION_CURSOR_FOLLOWING_MODE:
|
||||
return Preconditions.checkNotNull(mMagnificationCursorFollowingModeDialogDelegate)
|
||||
.onCreateDialog(dialogId);
|
||||
case DialogEnums.GESTURE_NAVIGATION_TUTORIAL:
|
||||
return AccessibilityShortcutsTutorial
|
||||
.showAccessibilityGestureTutorialDialog(getPrefContext());
|
||||
@@ -201,6 +212,11 @@ public class ToggleScreenMagnificationPreferenceFragment extends
|
||||
PackageManager.FEATURE_WINDOW_MAGNIFICATION);
|
||||
}
|
||||
|
||||
private static boolean isMagnificationCursorFollowingModeDialogSupported() {
|
||||
// TODO(b/398066000): Hide the setting when no pointer device exists for most form factors.
|
||||
return com.android.settings.accessibility.Flags.enableMagnificationCursorFollowingDialog();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initSettingsPreference() {
|
||||
final PreferenceCategory generalCategory = findPreference(KEY_GENERAL_CATEGORY);
|
||||
@@ -213,6 +229,7 @@ public class ToggleScreenMagnificationPreferenceFragment extends
|
||||
addJoystickSetting(generalCategory);
|
||||
// LINT.ThenChange(:search_data)
|
||||
}
|
||||
addCursorFollowingSetting(generalCategory);
|
||||
addFeedbackSetting(generalCategory);
|
||||
}
|
||||
|
||||
@@ -286,6 +303,31 @@ public class ToggleScreenMagnificationPreferenceFragment extends
|
||||
addPreferenceController(magnificationModePreferenceController);
|
||||
}
|
||||
|
||||
private static Preference createCursorFollowingPreference(Context context) {
|
||||
final Preference pref = new Preference(context);
|
||||
pref.setTitle(R.string.accessibility_magnification_cursor_following_title);
|
||||
pref.setKey(MagnificationCursorFollowingModePreferenceController.PREF_KEY);
|
||||
pref.setPersistent(false);
|
||||
return pref;
|
||||
}
|
||||
|
||||
private void addCursorFollowingSetting(PreferenceCategory generalCategory) {
|
||||
if (!isMagnificationCursorFollowingModeDialogSupported()) {
|
||||
return;
|
||||
}
|
||||
|
||||
generalCategory.addPreference(createCursorFollowingPreference(getPrefContext()));
|
||||
|
||||
final MagnificationCursorFollowingModePreferenceController controller =
|
||||
new MagnificationCursorFollowingModePreferenceController(
|
||||
getContext(),
|
||||
MagnificationCursorFollowingModePreferenceController.PREF_KEY);
|
||||
controller.setDialogHelper(/* dialogHelper= */this);
|
||||
mMagnificationCursorFollowingModeDialogDelegate = controller;
|
||||
controller.displayPreference(getPreferenceScreen());
|
||||
addPreferenceController(controller);
|
||||
}
|
||||
|
||||
private static Preference createFollowTypingPreference(Context context) {
|
||||
final Preference pref = new SwitchPreferenceCompat(context);
|
||||
pref.setTitle(R.string.accessibility_screen_magnification_follow_typing_title);
|
||||
@@ -510,6 +552,9 @@ public class ToggleScreenMagnificationPreferenceFragment extends
|
||||
case DialogEnums.DIALOG_MAGNIFICATION_TRIPLE_TAP_WARNING:
|
||||
return Preconditions.checkNotNull(mMagnificationModeDialogDelegate)
|
||||
.getDialogMetricsCategory(dialogId);
|
||||
case DialogEnums.DIALOG_MAGNIFICATION_CURSOR_FOLLOWING_MODE:
|
||||
return Preconditions.checkNotNull(mMagnificationCursorFollowingModeDialogDelegate)
|
||||
.getDialogMetricsCategory(dialogId);
|
||||
case DialogEnums.GESTURE_NAVIGATION_TUTORIAL:
|
||||
return SettingsEnums.DIALOG_TOGGLE_SCREEN_MAGNIFICATION_GESTURE_NAVIGATION;
|
||||
case DialogEnums.ACCESSIBILITY_BUTTON_TUTORIAL:
|
||||
@@ -667,6 +712,11 @@ public class ToggleScreenMagnificationPreferenceFragment extends
|
||||
return rawData;
|
||||
}
|
||||
|
||||
// Add all preferences to search raw data so that they are included in
|
||||
// indexing, which happens infrequently. Irrelevant preferences should be
|
||||
// hidden from the live returned search results by `getNonIndexableKeys`,
|
||||
// which is called every time a search occurs. This allows for dynamic search
|
||||
// entries that hide or show depending on current device state.
|
||||
rawData.add(createShortcutPreferenceSearchData(context));
|
||||
Stream.of(
|
||||
createMagnificationModePreference(context),
|
||||
@@ -674,6 +724,7 @@ public class ToggleScreenMagnificationPreferenceFragment extends
|
||||
createOneFingerPanningPreference(context),
|
||||
createAlwaysOnPreference(context),
|
||||
createJoystickPreference(context),
|
||||
createCursorFollowingPreference(context),
|
||||
createFeedbackPreference(context)
|
||||
)
|
||||
.forEach(pref ->
|
||||
@@ -714,6 +765,10 @@ public class ToggleScreenMagnificationPreferenceFragment extends
|
||||
}
|
||||
}
|
||||
|
||||
if (!isMagnificationCursorFollowingModeDialogSupported()) {
|
||||
niks.add(MagnificationCursorFollowingModePreferenceController.PREF_KEY);
|
||||
}
|
||||
|
||||
if (!Flags.enableLowVisionHats()) {
|
||||
niks.add(MagnificationFeedbackPreferenceController.PREF_KEY);
|
||||
}
|
||||
|
||||
@@ -34,7 +34,10 @@ import android.media.MediaMetadata;
|
||||
import android.media.session.MediaSession;
|
||||
import android.media.session.PlaybackState;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.IBinder;
|
||||
import android.os.Process;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
|
||||
@@ -51,24 +54,21 @@ import com.android.settingslib.bluetooth.BluetoothUtils;
|
||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
|
||||
import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
|
||||
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
|
||||
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState;
|
||||
import com.android.settingslib.bluetooth.LocalBluetoothManager;
|
||||
import com.android.settingslib.bluetooth.VolumeControlProfile;
|
||||
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
|
||||
import com.android.settingslib.utils.ThreadUtils;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class AudioStreamMediaService extends Service {
|
||||
static final String BROADCAST_ID = "audio_stream_media_service_broadcast_id";
|
||||
static final String BROADCAST_TITLE = "audio_stream_media_service_broadcast_title";
|
||||
static final String DEVICES = "audio_stream_media_service_devices";
|
||||
private static final String TAG = "AudioStreamMediaService";
|
||||
private static final int NOTIFICATION_ID = 1;
|
||||
private static final int NOTIFICATION_ID = R.string.audio_streams_title;
|
||||
private static final int BROADCAST_LISTENING_NOW_TEXT = R.string.audio_streams_listening_now;
|
||||
private static final int BROADCAST_STREAM_PAUSED_TEXT = R.string.audio_streams_present_now;
|
||||
@VisibleForTesting static final String LEAVE_BROADCAST_ACTION = "leave_broadcast_action";
|
||||
@@ -113,17 +113,16 @@ public class AudioStreamMediaService extends Service {
|
||||
|
||||
private final MetricsFeatureProvider mMetricsFeatureProvider =
|
||||
FeatureFactory.getFeatureFactory().getMetricsFeatureProvider();
|
||||
private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
|
||||
private final AtomicBoolean mIsMuted = new AtomicBoolean(false);
|
||||
private final AtomicBoolean mIsHysteresis = new AtomicBoolean(false);
|
||||
private final HandlerThread mHandlerThread = new HandlerThread(TAG,
|
||||
Process.THREAD_PRIORITY_BACKGROUND);
|
||||
private boolean mIsMuted = false;
|
||||
// Set 25 as default as the volume range from `VolumeControlProfile` is from 0 to 255.
|
||||
// If the initial volume from `onDeviceVolumeChanged` is larger than zero (not muted), we will
|
||||
// override this value. Otherwise, we raise the volume to 25 when the play button is clicked.
|
||||
private final AtomicInteger mLatestPositiveVolume = new AtomicInteger(25);
|
||||
private final Object mLocalSessionLock = new Object();
|
||||
private int mLatestPositiveVolume = 25;
|
||||
private boolean mHysteresisModeFixAvailable;
|
||||
private int mBroadcastId;
|
||||
@Nullable private List<BluetoothDevice> mDevices;
|
||||
@Nullable private Map<BluetoothDevice, LocalBluetoothLeBroadcastSourceState> mStateByDevice;
|
||||
@Nullable private LocalBluetoothManager mLocalBtManager;
|
||||
@Nullable private AudioStreamsHelper mAudioStreamsHelper;
|
||||
@Nullable private LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
|
||||
@@ -154,7 +153,6 @@ public class AudioStreamMediaService extends Service {
|
||||
Log.w(TAG, "onCreate() : mLeBroadcastAssistant is null!");
|
||||
return;
|
||||
}
|
||||
mHysteresisModeFixAvailable = BluetoothUtils.isAudioSharingHysteresisModeFixAvailable(this);
|
||||
|
||||
mNotificationManager = getSystemService(NotificationManager.class);
|
||||
if (mNotificationManager == null) {
|
||||
@@ -162,7 +160,8 @@ public class AudioStreamMediaService extends Service {
|
||||
return;
|
||||
}
|
||||
|
||||
mExecutor.execute(
|
||||
mHandlerThread.start();
|
||||
getHandler().post(
|
||||
() -> {
|
||||
if (mLocalBtManager == null
|
||||
|| mLeBroadcastAssistant == null
|
||||
@@ -184,32 +183,36 @@ public class AudioStreamMediaService extends Service {
|
||||
mVolumeControl = mLocalBtManager.getProfileManager().getVolumeControlProfile();
|
||||
if (mVolumeControl != null) {
|
||||
mVolumeControlCallback = new VolumeControlCallback();
|
||||
mVolumeControl.registerCallback(mExecutor, mVolumeControlCallback);
|
||||
mVolumeControl.registerCallback(getHandler()::post, mVolumeControlCallback);
|
||||
}
|
||||
|
||||
mBroadcastAssistantCallback = new AssistantCallback();
|
||||
mLeBroadcastAssistant.registerServiceCallBack(
|
||||
mExecutor, mBroadcastAssistantCallback);
|
||||
getHandler()::post, mBroadcastAssistantCallback);
|
||||
|
||||
mHysteresisModeFixAvailable =
|
||||
BluetoothUtils.isAudioSharingHysteresisModeFixAvailable(this);
|
||||
});
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
Handler getHandler() {
|
||||
return mHandlerThread.getThreadHandler();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
Log.d(TAG, "onDestroy()");
|
||||
super.onDestroy();
|
||||
if (BluetoothUtils.isAudioSharingUIAvailable(this)) {
|
||||
if (mDevices != null) {
|
||||
mDevices.clear();
|
||||
mDevices = null;
|
||||
getHandler().post(
|
||||
() -> {
|
||||
if (mStateByDevice != null) {
|
||||
mStateByDevice.clear();
|
||||
mStateByDevice = null;
|
||||
}
|
||||
synchronized (mLocalSessionLock) {
|
||||
if (mLocalSession != null) {
|
||||
mLocalSession.release();
|
||||
mLocalSession = null;
|
||||
}
|
||||
}
|
||||
mExecutor.execute(
|
||||
() -> {
|
||||
if (mLocalBtManager != null) {
|
||||
mLocalBtManager.getEventManager().unregisterCallback(
|
||||
mBluetoothCallback);
|
||||
@@ -222,7 +225,7 @@ public class AudioStreamMediaService extends Service {
|
||||
mVolumeControl.unregisterCallback(mVolumeControlCallback);
|
||||
}
|
||||
});
|
||||
}
|
||||
mHandlerThread.quitSafely();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -233,27 +236,29 @@ public class AudioStreamMediaService extends Service {
|
||||
stopSelf();
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
getHandler().post(() -> {
|
||||
mBroadcastId = intent.getIntExtra(BROADCAST_ID, -1);
|
||||
if (mBroadcastId == -1) {
|
||||
Log.w(TAG, "Invalid broadcast ID. Service will not start.");
|
||||
stopSelf();
|
||||
return START_NOT_STICKY;
|
||||
return;
|
||||
}
|
||||
var extra = intent.getParcelableArrayListExtra(DEVICES, BluetoothDevice.class);
|
||||
if (extra == null || extra.isEmpty()) {
|
||||
var devices = intent.getParcelableArrayListExtra(DEVICES, BluetoothDevice.class);
|
||||
if (devices == null || devices.isEmpty()) {
|
||||
Log.w(TAG, "No device. Service will not start.");
|
||||
stopSelf();
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
mDevices = Collections.synchronizedList(extra);
|
||||
} else {
|
||||
mStateByDevice = new HashMap<>();
|
||||
devices.forEach(d -> mStateByDevice.put(d, STREAMING));
|
||||
MediaSession.Token token =
|
||||
getOrCreateLocalMediaSession(intent.getStringExtra(BROADCAST_TITLE));
|
||||
startForeground(NOTIFICATION_ID, buildNotification(token));
|
||||
}
|
||||
});
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
private MediaSession.Token getOrCreateLocalMediaSession(String title) {
|
||||
synchronized (mLocalSessionLock) {
|
||||
if (mLocalSession != null) {
|
||||
return mLocalSession.getSessionToken();
|
||||
}
|
||||
@@ -266,20 +271,24 @@ public class AudioStreamMediaService extends Service {
|
||||
mLocalSession.setActive(true);
|
||||
mLocalSession.setPlaybackState(getPlaybackState());
|
||||
mMediaSessionCallback = new MediaSessionCallback();
|
||||
mLocalSession.setCallback(mMediaSessionCallback);
|
||||
mLocalSession.setCallback(mMediaSessionCallback, getHandler());
|
||||
return mLocalSession.getSessionToken();
|
||||
}
|
||||
}
|
||||
|
||||
private PlaybackState getPlaybackState() {
|
||||
if (mIsHysteresis.get()) {
|
||||
if (isAllDeviceHysteresis()) {
|
||||
return mPlayStateHysteresisBuilder.build();
|
||||
}
|
||||
return mIsMuted.get() ? mPlayStatePausingBuilder.build() : mPlayStatePlayingBuilder.build();
|
||||
return mIsMuted ? mPlayStatePausingBuilder.build() : mPlayStatePlayingBuilder.build();
|
||||
}
|
||||
|
||||
private boolean isAllDeviceHysteresis() {
|
||||
return mHysteresisModeFixAvailable && mStateByDevice != null
|
||||
&& mStateByDevice.values().stream().allMatch(v -> v == PAUSED);
|
||||
}
|
||||
|
||||
private String getDeviceName() {
|
||||
if (mDevices == null || mDevices.isEmpty() || mLocalBtManager == null) {
|
||||
if (mStateByDevice == null || mStateByDevice.isEmpty() || mLocalBtManager == null) {
|
||||
return DEFAULT_DEVICE_NAME;
|
||||
}
|
||||
|
||||
@@ -288,7 +297,8 @@ public class AudioStreamMediaService extends Service {
|
||||
return DEFAULT_DEVICE_NAME;
|
||||
}
|
||||
|
||||
CachedBluetoothDevice device = manager.findDevice(mDevices.get(0));
|
||||
CachedBluetoothDevice device = manager.findDevice(
|
||||
mStateByDevice.keySet().iterator().next());
|
||||
return device != null ? device.getName() : DEFAULT_DEVICE_NAME;
|
||||
}
|
||||
|
||||
@@ -304,7 +314,7 @@ public class AudioStreamMediaService extends Service {
|
||||
.setSmallIcon(com.android.settingslib.R.drawable.ic_bt_le_audio_sharing)
|
||||
.setStyle(mediaStyle)
|
||||
.setContentText(getString(
|
||||
mIsHysteresis.get() ? BROADCAST_STREAM_PAUSED_TEXT :
|
||||
isAllDeviceHysteresis() ? BROADCAST_STREAM_PAUSED_TEXT :
|
||||
BROADCAST_LISTENING_NOW_TEXT))
|
||||
.setSilent(true);
|
||||
return notificationBuilder.build();
|
||||
@@ -333,7 +343,8 @@ public class AudioStreamMediaService extends Service {
|
||||
public void onReceiveStateChanged(
|
||||
BluetoothDevice sink, int sourceId, BluetoothLeBroadcastReceiveState state) {
|
||||
super.onReceiveStateChanged(sink, sourceId, state);
|
||||
if (!mHysteresisModeFixAvailable || mDevices == null || !mDevices.contains(sink)) {
|
||||
if (!mHysteresisModeFixAvailable || mStateByDevice == null
|
||||
|| !mStateByDevice.containsKey(sink)) {
|
||||
return;
|
||||
}
|
||||
var sourceState = LocalBluetoothLeBroadcastAssistant.getLocalSourceState(state);
|
||||
@@ -343,12 +354,10 @@ public class AudioStreamMediaService extends Service {
|
||||
if (!streaming && !paused) {
|
||||
return;
|
||||
}
|
||||
// Atomically update mIsHysteresis if its current value is not the current paused state
|
||||
if (mIsHysteresis.compareAndSet(!paused, paused)) {
|
||||
synchronized (mLocalSessionLock) {
|
||||
if (mLocalSession == null) {
|
||||
return;
|
||||
}
|
||||
boolean shouldUpdate = mStateByDevice.get(sink) != sourceState;
|
||||
if (shouldUpdate) {
|
||||
mStateByDevice.put(sink, sourceState);
|
||||
if (mLocalSession != null) {
|
||||
mLocalSession.setPlaybackState(getPlaybackState());
|
||||
if (mNotificationManager != null) {
|
||||
mNotificationManager.notify(
|
||||
@@ -356,7 +365,7 @@ public class AudioStreamMediaService extends Service {
|
||||
buildNotification(mLocalSession.getSessionToken())
|
||||
);
|
||||
}
|
||||
Log.d(TAG, "updating hysteresis mode to : " + paused);
|
||||
Log.d(TAG, "updating source state to : " + sourceState);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -374,36 +383,36 @@ public class AudioStreamMediaService extends Service {
|
||||
@Override
|
||||
public void onDeviceVolumeChanged(
|
||||
@NonNull BluetoothDevice device, @IntRange(from = -255, to = 255) int volume) {
|
||||
if (mDevices == null || mDevices.isEmpty()) {
|
||||
if (mStateByDevice == null || mStateByDevice.isEmpty()) {
|
||||
Log.w(TAG, "active device or device has source is null!");
|
||||
return;
|
||||
}
|
||||
Log.d(
|
||||
TAG,
|
||||
"onDeviceVolumeChanged() bluetoothDevice : " + device + " volume: " + volume);
|
||||
if (mDevices.contains(device)) {
|
||||
if (mStateByDevice.containsKey(device)) {
|
||||
if (volume == 0) {
|
||||
mIsMuted.set(true);
|
||||
mIsMuted = true;
|
||||
} else {
|
||||
mIsMuted.set(false);
|
||||
mLatestPositiveVolume.set(volume);
|
||||
mIsMuted = false;
|
||||
mLatestPositiveVolume = volume;
|
||||
}
|
||||
synchronized (mLocalSessionLock) {
|
||||
if (mLocalSession != null) {
|
||||
mLocalSession.setPlaybackState(getPlaybackState());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class BtCallback implements BluetoothCallback {
|
||||
@Override
|
||||
public void onBluetoothStateChanged(int bluetoothState) {
|
||||
getHandler().post(() -> {
|
||||
if (BluetoothAdapter.STATE_OFF == bluetoothState) {
|
||||
Log.d(TAG, "onBluetoothStateChanged() : stopSelf");
|
||||
stopSelf();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -411,24 +420,17 @@ public class AudioStreamMediaService extends Service {
|
||||
@NonNull CachedBluetoothDevice cachedDevice,
|
||||
@ConnectionState int state,
|
||||
int bluetoothProfile) {
|
||||
getHandler().post(() -> {
|
||||
if (state == BluetoothAdapter.STATE_DISCONNECTED
|
||||
&& bluetoothProfile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT
|
||||
&& mDevices != null) {
|
||||
mDevices.remove(cachedDevice.getDevice());
|
||||
cachedDevice
|
||||
.getMemberDevice()
|
||||
.forEach(
|
||||
m -> {
|
||||
// Check nullability to pass NullAway check
|
||||
if (mDevices != null) {
|
||||
mDevices.remove(m.getDevice());
|
||||
&& mStateByDevice != null) {
|
||||
mStateByDevice.remove(cachedDevice.getDevice());
|
||||
}
|
||||
});
|
||||
}
|
||||
if (mDevices == null || mDevices.isEmpty()) {
|
||||
if (mStateByDevice == null || mStateByDevice.isEmpty()) {
|
||||
Log.d(TAG, "onProfileConnectionStateChanged() : stopSelf");
|
||||
stopSelf();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -454,12 +456,10 @@ public class AudioStreamMediaService extends Service {
|
||||
@Override
|
||||
public void onSeekTo(long pos) {
|
||||
Log.d(TAG, "onSeekTo: " + pos);
|
||||
synchronized (mLocalSessionLock) {
|
||||
if (mLocalSession != null) {
|
||||
mLocalSession.setPlaybackState(getPlaybackState());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
@@ -484,28 +484,26 @@ public class AudioStreamMediaService extends Service {
|
||||
}
|
||||
|
||||
private void handleOnPlay() {
|
||||
if (mDevices == null || mDevices.isEmpty()) {
|
||||
if (mStateByDevice == null || mStateByDevice.isEmpty()) {
|
||||
Log.w(TAG, "active device or device has source is null!");
|
||||
return;
|
||||
}
|
||||
Log.d(
|
||||
TAG,
|
||||
"onPlay() setting volume for device : "
|
||||
+ mDevices.getFirst()
|
||||
+ " volume: "
|
||||
+ mLatestPositiveVolume.get());
|
||||
setDeviceVolume(mDevices.getFirst(), mLatestPositiveVolume.get());
|
||||
mStateByDevice.keySet().forEach(device -> {
|
||||
Log.d(TAG, "onPlay() setting volume for device : " + device + " volume: "
|
||||
+ mLatestPositiveVolume);
|
||||
setDeviceVolume(device, mLatestPositiveVolume);
|
||||
});
|
||||
}
|
||||
|
||||
private void handleOnPause() {
|
||||
if (mDevices == null || mDevices.isEmpty()) {
|
||||
if (mStateByDevice == null || mStateByDevice.isEmpty()) {
|
||||
Log.w(TAG, "active device or device has source is null!");
|
||||
return;
|
||||
}
|
||||
Log.d(
|
||||
TAG,
|
||||
"onPause() setting volume for device : " + mDevices.getFirst() + " volume: " + 0);
|
||||
setDeviceVolume(mDevices.getFirst(), /* volume= */ 0);
|
||||
mStateByDevice.keySet().forEach(device -> {
|
||||
Log.d(TAG, "onPause() setting volume for device : " + device + " volume: " + 0);
|
||||
setDeviceVolume(device, /* volume= */ 0);
|
||||
});
|
||||
}
|
||||
|
||||
private void setDeviceVolume(BluetoothDevice device, int volume) {
|
||||
@@ -514,7 +512,7 @@ public class AudioStreamMediaService extends Service {
|
||||
ThreadUtils.postOnBackgroundThread(
|
||||
() -> {
|
||||
if (mVolumeControl != null) {
|
||||
mVolumeControl.setDeviceVolume(device, volume, true);
|
||||
mVolumeControl.setDeviceVolume(device, volume, false);
|
||||
mMetricsFeatureProvider.action(
|
||||
getApplicationContext(), event, volume == 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -109,7 +109,8 @@ public class ExternalDisplaySettingsConfiguration {
|
||||
private final Handler mHandler;
|
||||
|
||||
Injector(@Nullable Context context) {
|
||||
this(context, new FeatureFlagsImpl(), new Handler(Looper.getMainLooper()));
|
||||
this(context, new DesktopExperienceFlags(new FeatureFlagsImpl()),
|
||||
new Handler(Looper.getMainLooper()));
|
||||
}
|
||||
|
||||
Injector(@Nullable Context context, @NonNull FeatureFlags flags, @NonNull Handler handler) {
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
|
||||
package com.android.settings.fuelgauge.batteryusage;
|
||||
|
||||
import static com.android.settings.fuelgauge.batteryusage.BatteryChartViewModel.SELECTED_INDEX_ALL;
|
||||
import static com.android.settings.fuelgauge.batteryusage.BatteryChartViewModel.SELECTED_INDEX_INVALID;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.app.settings.SettingsEnums;
|
||||
@@ -82,10 +85,10 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
|
||||
@VisibleForTesting TextView mChartSummaryTextView;
|
||||
@VisibleForTesting BatteryChartView mDailyChartView;
|
||||
@VisibleForTesting BatteryChartView mHourlyChartView;
|
||||
@VisibleForTesting int mDailyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL;
|
||||
@VisibleForTesting int mHourlyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL;
|
||||
@VisibleForTesting int mDailyHighlightSlotIndex = BatteryChartViewModel.SELECTED_INDEX_INVALID;
|
||||
@VisibleForTesting int mHourlyHighlightSlotIndex = BatteryChartViewModel.SELECTED_INDEX_INVALID;
|
||||
@VisibleForTesting int mDailyChartIndex = SELECTED_INDEX_ALL;
|
||||
@VisibleForTesting int mHourlyChartIndex = SELECTED_INDEX_ALL;
|
||||
@VisibleForTesting int mDailyHighlightSlotIndex = SELECTED_INDEX_INVALID;
|
||||
@VisibleForTesting int mHourlyHighlightSlotIndex = SELECTED_INDEX_INVALID;
|
||||
|
||||
private boolean mIs24HourFormat;
|
||||
private View mBatteryChartViewGroup;
|
||||
@@ -198,8 +201,8 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
|
||||
getTotalHours(batteryLevelData));
|
||||
|
||||
if (batteryLevelData == null) {
|
||||
mDailyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL;
|
||||
mHourlyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL;
|
||||
mDailyChartIndex = SELECTED_INDEX_ALL;
|
||||
mHourlyChartIndex = SELECTED_INDEX_ALL;
|
||||
mDailyViewModel = null;
|
||||
mHourlyViewModels = null;
|
||||
refreshUi();
|
||||
@@ -226,9 +229,9 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
|
||||
}
|
||||
|
||||
boolean isHighlightSlotFocused() {
|
||||
return (mDailyHighlightSlotIndex != BatteryChartViewModel.SELECTED_INDEX_INVALID
|
||||
return (mDailyHighlightSlotIndex != SELECTED_INDEX_INVALID
|
||||
&& mDailyHighlightSlotIndex == mDailyChartIndex
|
||||
&& mHourlyHighlightSlotIndex != BatteryChartViewModel.SELECTED_INDEX_INVALID
|
||||
&& mHourlyHighlightSlotIndex != SELECTED_INDEX_INVALID
|
||||
&& mHourlyHighlightSlotIndex == mHourlyChartIndex);
|
||||
}
|
||||
|
||||
@@ -242,8 +245,8 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
|
||||
}
|
||||
|
||||
void selectHighlightSlotIndex() {
|
||||
if (mDailyHighlightSlotIndex == BatteryChartViewModel.SELECTED_INDEX_INVALID
|
||||
|| mHourlyHighlightSlotIndex == BatteryChartViewModel.SELECTED_INDEX_INVALID) {
|
||||
if (mDailyHighlightSlotIndex == SELECTED_INDEX_INVALID
|
||||
|| mHourlyHighlightSlotIndex == SELECTED_INDEX_INVALID) {
|
||||
return;
|
||||
}
|
||||
if (mDailyHighlightSlotIndex == mDailyChartIndex
|
||||
@@ -258,8 +261,11 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
|
||||
"onDailyChartSelect:%d, onHourlyChartSelect:%d",
|
||||
mDailyChartIndex, mHourlyChartIndex));
|
||||
refreshUi();
|
||||
// The highlight slot must be selected.
|
||||
mHandler.post(
|
||||
() -> mDailyChartView.setAccessibilityPaneTitle(getAccessibilityAnnounceMessage()));
|
||||
() ->
|
||||
mDailyChartView.setAccessibilityPaneTitle(
|
||||
getAccessibilityAnnounceMessage(/* isSlotSelected= */ true)));
|
||||
if (mOnSelectedIndexUpdatedListener != null) {
|
||||
mOnSelectedIndexUpdatedListener.onSelectedIndexUpdated();
|
||||
}
|
||||
@@ -295,15 +301,16 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
|
||||
}
|
||||
Log.d(TAG, "onDailyChartSelect:" + trapezoidIndex);
|
||||
mDailyChartIndex = trapezoidIndex;
|
||||
mHourlyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL;
|
||||
mHourlyChartIndex = SELECTED_INDEX_ALL;
|
||||
refreshUi();
|
||||
mHandler.post(
|
||||
() ->
|
||||
mDailyChartView.setAccessibilityPaneTitle(
|
||||
getAccessibilityAnnounceMessage()));
|
||||
getAccessibilityAnnounceMessage(
|
||||
mDailyChartIndex != SELECTED_INDEX_ALL)));
|
||||
mMetricsFeatureProvider.action(
|
||||
mPrefContext,
|
||||
trapezoidIndex == BatteryChartViewModel.SELECTED_INDEX_ALL
|
||||
trapezoidIndex == SELECTED_INDEX_ALL
|
||||
? SettingsEnums.ACTION_BATTERY_USAGE_DAILY_SHOW_ALL
|
||||
: SettingsEnums.ACTION_BATTERY_USAGE_DAILY_TIME_SLOT,
|
||||
mDailyChartIndex);
|
||||
@@ -314,7 +321,7 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
|
||||
mHourlyChartView = hourlyChartView;
|
||||
mHourlyChartView.setOnSelectListener(
|
||||
trapezoidIndex -> {
|
||||
if (mDailyChartIndex == BatteryChartViewModel.SELECTED_INDEX_ALL) {
|
||||
if (mDailyChartIndex == SELECTED_INDEX_ALL) {
|
||||
// This will happen when a daily slot and an hour slot are clicked together.
|
||||
return;
|
||||
}
|
||||
@@ -327,10 +334,11 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
|
||||
mHandler.post(
|
||||
() ->
|
||||
mHourlyChartView.setAccessibilityPaneTitle(
|
||||
getAccessibilityAnnounceMessage()));
|
||||
getAccessibilityAnnounceMessage(
|
||||
mHourlyChartIndex != SELECTED_INDEX_ALL)));
|
||||
mMetricsFeatureProvider.action(
|
||||
mPrefContext,
|
||||
trapezoidIndex == BatteryChartViewModel.SELECTED_INDEX_ALL
|
||||
trapezoidIndex == SELECTED_INDEX_ALL
|
||||
? SettingsEnums.ACTION_BATTERY_USAGE_SHOW_ALL
|
||||
: SettingsEnums.ACTION_BATTERY_USAGE_TIME_SLOT,
|
||||
mHourlyChartIndex);
|
||||
@@ -378,27 +386,27 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
|
||||
} else {
|
||||
mDailyChartView.setVisibility(View.VISIBLE);
|
||||
if (mDailyChartIndex >= mDailyViewModel.size()) {
|
||||
mDailyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL;
|
||||
mDailyChartIndex = SELECTED_INDEX_ALL;
|
||||
}
|
||||
mDailyViewModel.setSelectedIndex(mDailyChartIndex);
|
||||
mDailyViewModel.setHighlightSlotIndex(mDailyHighlightSlotIndex);
|
||||
mDailyChartView.setViewModel(mDailyViewModel);
|
||||
}
|
||||
|
||||
if (mDailyChartIndex == BatteryChartViewModel.SELECTED_INDEX_ALL) {
|
||||
if (mDailyChartIndex == SELECTED_INDEX_ALL) {
|
||||
// Multiple days are selected, hide the hourly chart view.
|
||||
animateBatteryHourlyChartView(/* visible= */ false);
|
||||
} else {
|
||||
animateBatteryHourlyChartView(/* visible= */ true);
|
||||
final BatteryChartViewModel hourlyViewModel = mHourlyViewModels.get(mDailyChartIndex);
|
||||
if (mHourlyChartIndex >= hourlyViewModel.size()) {
|
||||
mHourlyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL;
|
||||
mHourlyChartIndex = SELECTED_INDEX_ALL;
|
||||
}
|
||||
hourlyViewModel.setSelectedIndex(mHourlyChartIndex);
|
||||
hourlyViewModel.setHighlightSlotIndex(
|
||||
(mDailyChartIndex == mDailyHighlightSlotIndex)
|
||||
? mHourlyHighlightSlotIndex
|
||||
: BatteryChartViewModel.SELECTED_INDEX_INVALID);
|
||||
: SELECTED_INDEX_INVALID);
|
||||
mHourlyChartView.setViewModel(hourlyViewModel);
|
||||
}
|
||||
}
|
||||
@@ -416,7 +424,7 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
|
||||
isAccessibilityText
|
||||
? mDailyViewModel.getContentDescription(mDailyChartIndex)
|
||||
: mDailyViewModel.getFullText(mDailyChartIndex);
|
||||
if (mHourlyChartIndex == BatteryChartViewModel.SELECTED_INDEX_ALL) {
|
||||
if (mHourlyChartIndex == SELECTED_INDEX_ALL) {
|
||||
return selectedDayText;
|
||||
}
|
||||
|
||||
@@ -441,15 +449,19 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
|
||||
return "";
|
||||
}
|
||||
|
||||
if (mDailyChartIndex == BatteryChartViewModel.SELECTED_INDEX_ALL
|
||||
|| mHourlyChartIndex == BatteryChartViewModel.SELECTED_INDEX_ALL) {
|
||||
if (mDailyChartIndex == SELECTED_INDEX_ALL || mHourlyChartIndex == SELECTED_INDEX_ALL) {
|
||||
return mDailyViewModel.getSlotBatteryLevelText(mDailyChartIndex);
|
||||
}
|
||||
|
||||
return mHourlyViewModels.get(mDailyChartIndex).getSlotBatteryLevelText(mHourlyChartIndex);
|
||||
}
|
||||
|
||||
private String getAccessibilityAnnounceMessage() {
|
||||
private String getAccessibilityAnnounceMessage(final boolean isSlotSelected) {
|
||||
final String selectedInformation =
|
||||
mPrefContext.getString(
|
||||
isSlotSelected
|
||||
? R.string.battery_chart_slot_status_selected
|
||||
: R.string.battery_chart_slot_status_unselected);
|
||||
final String slotInformation = getSlotInformation(/* isAccessibilityText= */ true);
|
||||
final String slotInformationMessage =
|
||||
slotInformation == null
|
||||
@@ -460,7 +472,8 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
|
||||
final String batteryLevelPercentageMessage = getBatteryLevelPercentageInfo();
|
||||
|
||||
return mPrefContext.getString(
|
||||
R.string.battery_usage_time_info_and_battery_level,
|
||||
R.string.battery_usage_status_time_info_and_battery_level,
|
||||
selectedInformation,
|
||||
slotInformationMessage,
|
||||
batteryLevelPercentageMessage);
|
||||
}
|
||||
@@ -533,9 +546,8 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
|
||||
}
|
||||
|
||||
private boolean isAllSelected() {
|
||||
return (isBatteryLevelDataInOneDay()
|
||||
|| mDailyChartIndex == BatteryChartViewModel.SELECTED_INDEX_ALL)
|
||||
&& mHourlyChartIndex == BatteryChartViewModel.SELECTED_INDEX_ALL;
|
||||
return (isBatteryLevelDataInOneDay() || mDailyChartIndex == SELECTED_INDEX_ALL)
|
||||
&& mHourlyChartIndex == SELECTED_INDEX_ALL;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@@ -571,9 +583,7 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
|
||||
return null;
|
||||
}
|
||||
BatteryDiffData allBatteryDiffData =
|
||||
batteryUsageData
|
||||
.get(BatteryChartViewModel.SELECTED_INDEX_ALL)
|
||||
.get(BatteryChartViewModel.SELECTED_INDEX_ALL);
|
||||
batteryUsageData.get(SELECTED_INDEX_ALL).get(SELECTED_INDEX_ALL);
|
||||
return allBatteryDiffData == null ? null : allBatteryDiffData.getAppDiffEntryList();
|
||||
}
|
||||
|
||||
@@ -613,12 +623,9 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
|
||||
|
||||
@Override
|
||||
public String generateSlotBatteryLevelText(List<Integer> levels, int index) {
|
||||
final int fromBatteryLevelIndex =
|
||||
index == BatteryChartViewModel.SELECTED_INDEX_ALL ? 0 : index;
|
||||
final int fromBatteryLevelIndex = index == SELECTED_INDEX_ALL ? 0 : index;
|
||||
final int toBatteryLevelIndex =
|
||||
index == BatteryChartViewModel.SELECTED_INDEX_ALL
|
||||
? levels.size() - 1
|
||||
: index + 1;
|
||||
index == SELECTED_INDEX_ALL ? levels.size() - 1 : index + 1;
|
||||
return mPrefContext.getString(
|
||||
R.string.battery_level_percentage,
|
||||
generateBatteryLevelText(levels.get(fromBatteryLevelIndex)),
|
||||
|
||||
@@ -17,6 +17,8 @@ package com.android.settings.fuelgauge.batteryusage;
|
||||
|
||||
import static com.android.settings.Utils.formatPercentage;
|
||||
import static com.android.settings.fuelgauge.batteryusage.BatteryChartViewModel.AxisLabelPosition.BETWEEN_TRAPEZOIDS;
|
||||
import static com.android.settings.fuelgauge.batteryusage.BatteryChartViewModel.SELECTED_INDEX_ALL;
|
||||
import static com.android.settings.fuelgauge.batteryusage.BatteryChartViewModel.SELECTED_INDEX_INVALID;
|
||||
import static com.android.settingslib.fuelgauge.BatteryStatus.BATTERY_LEVEL_UNKNOWN;
|
||||
|
||||
import static java.lang.Math.abs;
|
||||
@@ -81,7 +83,7 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
|
||||
getContext().getResources().getConfiguration().getLayoutDirection();
|
||||
|
||||
private BatteryChartViewModel mViewModel;
|
||||
private int mHoveredIndex = BatteryChartViewModel.SELECTED_INDEX_INVALID;
|
||||
private int mHoveredIndex = SELECTED_INDEX_INVALID;
|
||||
private int mDividerWidth;
|
||||
private int mDividerHeight;
|
||||
private float mTrapezoidVOffset;
|
||||
@@ -245,9 +247,9 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
|
||||
// sent here.
|
||||
return true;
|
||||
case MotionEvent.ACTION_HOVER_EXIT:
|
||||
if (mHoveredIndex != BatteryChartViewModel.SELECTED_INDEX_INVALID) {
|
||||
if (mHoveredIndex != SELECTED_INDEX_INVALID) {
|
||||
sendAccessibilityEventForHover(AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
|
||||
mHoveredIndex = BatteryChartViewModel.SELECTED_INDEX_INVALID; // reset
|
||||
mHoveredIndex = SELECTED_INDEX_INVALID; // reset
|
||||
invalidate();
|
||||
}
|
||||
// Ignore the super.onHoverEvent() because the hovered trapezoid has already been
|
||||
@@ -262,7 +264,7 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
|
||||
public void onHoverChanged(boolean hovered) {
|
||||
super.onHoverChanged(hovered);
|
||||
if (!hovered) {
|
||||
mHoveredIndex = BatteryChartViewModel.SELECTED_INDEX_INVALID; // reset
|
||||
mHoveredIndex = SELECTED_INDEX_INVALID; // reset
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
@@ -295,9 +297,7 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
|
||||
if (mOnSelectListener != null) {
|
||||
// Selects all if users click the same trapezoid item two times.
|
||||
mOnSelectListener.onSelect(
|
||||
index == mViewModel.selectedIndex()
|
||||
? BatteryChartViewModel.SELECTED_INDEX_ALL
|
||||
: index);
|
||||
index == mViewModel.selectedIndex() ? SELECTED_INDEX_ALL : index);
|
||||
}
|
||||
view.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK);
|
||||
}
|
||||
@@ -332,8 +332,8 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
|
||||
setBackgroundColor(Color.TRANSPARENT);
|
||||
mTrapezoidSolidColor = Utils.getColorAccentDefaultColor(context);
|
||||
mTrapezoidColor = Utils.getDisabled(context, mTrapezoidSolidColor);
|
||||
mTrapezoidHoverColor = context.getColor(
|
||||
com.android.internal.R.color.materialColorSecondaryContainer);
|
||||
mTrapezoidHoverColor =
|
||||
context.getColor(com.android.internal.R.color.materialColorSecondaryContainer);
|
||||
// Initializes the divider line paint.
|
||||
final Resources resources = getContext().getResources();
|
||||
mDividerWidth = resources.getDimensionPixelSize(R.dimen.chartview_divider_width);
|
||||
@@ -623,8 +623,7 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
|
||||
// Configures the trapezoid paint color.
|
||||
final int trapezoidColor =
|
||||
(mViewModel.selectedIndex() == index
|
||||
|| mViewModel.selectedIndex()
|
||||
== BatteryChartViewModel.SELECTED_INDEX_ALL)
|
||||
|| mViewModel.selectedIndex() == SELECTED_INDEX_ALL)
|
||||
? mTrapezoidSolidColor
|
||||
: mTrapezoidColor;
|
||||
final boolean isHoverState =
|
||||
@@ -659,9 +658,7 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
|
||||
}
|
||||
|
||||
private boolean isHighlightSlotValid() {
|
||||
return mViewModel != null
|
||||
&& mViewModel.getHighlightSlotIndex()
|
||||
!= BatteryChartViewModel.SELECTED_INDEX_INVALID;
|
||||
return mViewModel != null && mViewModel.getHighlightSlotIndex() != SELECTED_INDEX_INVALID;
|
||||
}
|
||||
|
||||
private void drawTransomLine(Canvas canvas) {
|
||||
@@ -715,7 +712,7 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
|
||||
// Searches the corresponding trapezoid index from x location.
|
||||
private int getTrapezoidIndex(float x) {
|
||||
if (mTrapezoidSlots == null) {
|
||||
return BatteryChartViewModel.SELECTED_INDEX_INVALID;
|
||||
return SELECTED_INDEX_INVALID;
|
||||
}
|
||||
for (int index = 0; index < mTrapezoidSlots.length; index++) {
|
||||
final TrapezoidSlot slot = mTrapezoidSlots[index];
|
||||
@@ -723,7 +720,7 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
|
||||
return index;
|
||||
}
|
||||
}
|
||||
return BatteryChartViewModel.SELECTED_INDEX_INVALID;
|
||||
return SELECTED_INDEX_INVALID;
|
||||
}
|
||||
|
||||
private void initializeAxisLabelsBounds() {
|
||||
@@ -796,7 +793,11 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick
|
||||
childInfo.setText(slotTimeInfo);
|
||||
childInfo.setContentDescription(
|
||||
mContext.getString(
|
||||
R.string.battery_usage_time_info_and_battery_level,
|
||||
R.string.battery_usage_status_time_info_and_battery_level,
|
||||
mContext.getString(
|
||||
mViewModel.selectedIndex() == virtualViewId
|
||||
? R.string.battery_chart_slot_status_selected
|
||||
: R.string.battery_chart_slot_status_unselected),
|
||||
slotTimeInfo,
|
||||
batteryLevelInfo));
|
||||
childInfo.setAccessibilityFocused(virtualViewId == mAccessibilityFocusNodeViewId);
|
||||
|
||||
@@ -20,6 +20,7 @@ import android.content.Context;
|
||||
|
||||
import com.android.settings.core.PreferenceControllerMixin;
|
||||
import com.android.settings.network.SubscriptionUtil;
|
||||
import com.android.settingslib.Utils;
|
||||
import com.android.settingslib.core.AbstractPreferenceController;
|
||||
|
||||
public class NetworkResetPreferenceController extends AbstractPreferenceController
|
||||
@@ -34,8 +35,9 @@ public class NetworkResetPreferenceController extends AbstractPreferenceControll
|
||||
|
||||
@Override
|
||||
public boolean isAvailable() {
|
||||
return (SubscriptionUtil.isSimHardwareVisible(mContext) &&
|
||||
(!mRestrictionChecker.hasUserRestriction()));
|
||||
return (SubscriptionUtil.isSimHardwareVisible(mContext)
|
||||
&& !Utils.isWifiOnly(mContext)
|
||||
&& !mRestrictionChecker.hasUserRestriction());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -40,6 +40,7 @@ import com.android.internal.net.VpnProfile;
|
||||
import com.android.net.module.util.ProxyUtils;
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.utils.AndroidKeystoreAliasLoader;
|
||||
import com.android.settings.wifi.utils.TextInputGroup;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
@@ -70,16 +71,17 @@ class ConfigDialog extends AlertDialog implements TextWatcher,
|
||||
|
||||
private View mView;
|
||||
|
||||
private TextView mName;
|
||||
private TextInputGroup mNameInput;
|
||||
private Spinner mType;
|
||||
private TextView mServer;
|
||||
private TextView mUsername;
|
||||
private TextInputGroup mServerInput;
|
||||
private TextInputGroup mUsernameInput;
|
||||
private TextInputGroup mPasswordInput;
|
||||
private TextView mPassword;
|
||||
private Spinner mProxySettings;
|
||||
private TextView mProxyHost;
|
||||
private TextView mProxyPort;
|
||||
private TextView mIpsecIdentifier;
|
||||
private TextView mIpsecSecret;
|
||||
private TextInputGroup mIpsecIdentifierInput;
|
||||
private TextInputGroup mIpsecSecretInput;
|
||||
private Spinner mIpsecUserCert;
|
||||
private Spinner mIpsecCaCert;
|
||||
private Spinner mIpsecServerCert;
|
||||
@@ -106,16 +108,22 @@ class ConfigDialog extends AlertDialog implements TextWatcher,
|
||||
Context context = getContext();
|
||||
|
||||
// First, find out all the fields.
|
||||
mName = (TextView) mView.findViewById(R.id.name);
|
||||
mNameInput = new TextInputGroup(mView, R.id.name_layout, R.id.name,
|
||||
R.string.vpn_field_required);
|
||||
mType = (Spinner) mView.findViewById(R.id.type);
|
||||
mServer = (TextView) mView.findViewById(R.id.server);
|
||||
mUsername = (TextView) mView.findViewById(R.id.username);
|
||||
mPassword = (TextView) mView.findViewById(R.id.password);
|
||||
mServerInput = new TextInputGroup(mView, R.id.server_layout, R.id.server,
|
||||
R.string.vpn_field_required);
|
||||
mUsernameInput = new TextInputGroup(mView, R.id.username_layout, R.id.username,
|
||||
R.string.vpn_field_required);
|
||||
mPasswordInput = new TextInputGroup(mView, R.id.password_layout, R.id.password,
|
||||
R.string.vpn_field_required);
|
||||
mProxySettings = (Spinner) mView.findViewById(R.id.vpn_proxy_settings);
|
||||
mProxyHost = (TextView) mView.findViewById(R.id.vpn_proxy_host);
|
||||
mProxyPort = (TextView) mView.findViewById(R.id.vpn_proxy_port);
|
||||
mIpsecIdentifier = (TextView) mView.findViewById(R.id.ipsec_identifier);
|
||||
mIpsecSecret = (TextView) mView.findViewById(R.id.ipsec_secret);
|
||||
mIpsecIdentifierInput = new TextInputGroup(mView, R.id.ipsec_identifier_layout,
|
||||
R.id.ipsec_identifier, R.string.vpn_field_required);
|
||||
mIpsecSecretInput = new TextInputGroup(mView, R.id.ipsec_secret_layout, R.id.ipsec_secret,
|
||||
R.string.vpn_field_required);
|
||||
mIpsecUserCert = (Spinner) mView.findViewById(R.id.ipsec_user_cert);
|
||||
mIpsecCaCert = (Spinner) mView.findViewById(R.id.ipsec_ca_cert);
|
||||
mIpsecServerCert = (Spinner) mView.findViewById(R.id.ipsec_server_cert);
|
||||
@@ -125,21 +133,21 @@ class ConfigDialog extends AlertDialog implements TextWatcher,
|
||||
mAlwaysOnInvalidReason = (TextView) mView.findViewById(R.id.always_on_invalid_reason);
|
||||
|
||||
// Second, copy values from the profile.
|
||||
mName.setText(mProfile.name);
|
||||
mNameInput.setText(mProfile.name);
|
||||
setTypesByFeature(mType);
|
||||
mType.setSelection(convertVpnProfileConstantToTypeIndex(mProfile.type));
|
||||
mServer.setText(mProfile.server);
|
||||
mServerInput.setText(mProfile.server);
|
||||
if (mProfile.saveLogin) {
|
||||
mUsername.setText(mProfile.username);
|
||||
mPassword.setText(mProfile.password);
|
||||
mUsernameInput.setText(mProfile.username);
|
||||
mPasswordInput.setText(mProfile.password);
|
||||
}
|
||||
if (mProfile.proxy != null) {
|
||||
mProxyHost.setText(mProfile.proxy.getHost());
|
||||
int port = mProfile.proxy.getPort();
|
||||
mProxyPort.setText(port == 0 ? "" : Integer.toString(port));
|
||||
}
|
||||
mIpsecIdentifier.setText(mProfile.ipsecIdentifier);
|
||||
mIpsecSecret.setText(mProfile.ipsecSecret);
|
||||
mIpsecIdentifierInput.setText(mProfile.ipsecIdentifier);
|
||||
mIpsecSecretInput.setText(mProfile.ipsecSecret);
|
||||
final AndroidKeystoreAliasLoader androidKeystoreAliasLoader =
|
||||
new AndroidKeystoreAliasLoader(null);
|
||||
loadCertificates(mIpsecUserCert, androidKeystoreAliasLoader.getKeyCertAliases(), 0,
|
||||
@@ -150,7 +158,8 @@ class ConfigDialog extends AlertDialog implements TextWatcher,
|
||||
R.string.vpn_no_server_cert, mProfile.ipsecServerCert);
|
||||
mSaveLogin.setChecked(mProfile.saveLogin);
|
||||
mAlwaysOnVpn.setChecked(mProfile.key.equals(VpnUtils.getLockdownVpn()));
|
||||
mPassword.setTextAppearance(android.R.style.TextAppearance_DeviceDefault_Medium);
|
||||
mPasswordInput.getEditText()
|
||||
.setTextAppearance(android.R.style.TextAppearance_DeviceDefault_Medium);
|
||||
|
||||
// Hide lockdown VPN on devices that require IMS authentication
|
||||
if (SystemProperties.getBoolean("persist.radio.imsregrequired", false)) {
|
||||
@@ -158,16 +167,16 @@ class ConfigDialog extends AlertDialog implements TextWatcher,
|
||||
}
|
||||
|
||||
// Third, add listeners to required fields.
|
||||
mName.addTextChangedListener(this);
|
||||
mNameInput.addTextChangedListener(this);
|
||||
mType.setOnItemSelectedListener(this);
|
||||
mServer.addTextChangedListener(this);
|
||||
mUsername.addTextChangedListener(this);
|
||||
mPassword.addTextChangedListener(this);
|
||||
mServerInput.addTextChangedListener(this);
|
||||
mUsernameInput.addTextChangedListener(this);
|
||||
mPasswordInput.addTextChangedListener(this);
|
||||
mProxySettings.setOnItemSelectedListener(this);
|
||||
mProxyHost.addTextChangedListener(this);
|
||||
mProxyPort.addTextChangedListener(this);
|
||||
mIpsecIdentifier.addTextChangedListener(this);
|
||||
mIpsecSecret.addTextChangedListener(this);
|
||||
mIpsecIdentifierInput.addTextChangedListener(this);
|
||||
mIpsecSecretInput.addTextChangedListener(this);
|
||||
mIpsecUserCert.setOnItemSelectedListener(this);
|
||||
mShowOptions.setOnClickListener(this);
|
||||
mAlwaysOnVpn.setOnCheckedChangeListener(this);
|
||||
@@ -202,6 +211,8 @@ class ConfigDialog extends AlertDialog implements TextWatcher,
|
||||
setTitle(context.getString(R.string.vpn_connect_to, mProfile.name));
|
||||
|
||||
setUsernamePasswordVisibility(mProfile.type);
|
||||
mUsernameInput.setHelperText(context.getString(R.string.vpn_required));
|
||||
mPasswordInput.setHelperText(context.getString(R.string.vpn_required));
|
||||
|
||||
// Create a button to connect the network.
|
||||
setButton(DialogInterface.BUTTON_POSITIVE,
|
||||
@@ -260,6 +271,10 @@ class ConfigDialog extends AlertDialog implements TextWatcher,
|
||||
updateProxyFieldsVisibility(position);
|
||||
}
|
||||
updateUiControls();
|
||||
mNameInput.setError("");
|
||||
mServerInput.setError("");
|
||||
mIpsecIdentifierInput.setError("");
|
||||
mIpsecSecretInput.setError("");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -375,30 +390,16 @@ class ConfigDialog extends AlertDialog implements TextWatcher,
|
||||
return false;
|
||||
}
|
||||
|
||||
final int position = mType.getSelectedItemPosition();
|
||||
final int type = VPN_TYPES.get(position);
|
||||
if (!editing && requiresUsernamePassword(type)) {
|
||||
return mUsername.getText().length() != 0 && mPassword.getText().length() != 0;
|
||||
}
|
||||
if (mName.getText().length() == 0 || mServer.getText().length() == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// All IKEv2 methods require an identifier
|
||||
if (mIpsecIdentifier.getText().length() == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!validateProxy()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
switch (getVpnType()) {
|
||||
case VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS:
|
||||
return true;
|
||||
|
||||
case VpnProfile.TYPE_IKEV2_IPSEC_PSK:
|
||||
return mIpsecSecret.getText().length() != 0;
|
||||
return true;
|
||||
|
||||
case VpnProfile.TYPE_IKEV2_IPSEC_RSA:
|
||||
return mIpsecUserCert.getSelectedItemPosition() != 0;
|
||||
@@ -406,6 +407,29 @@ class ConfigDialog extends AlertDialog implements TextWatcher,
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean validate() {
|
||||
boolean isValidate = true;
|
||||
int type = getVpnType();
|
||||
if (!mEditing && requiresUsernamePassword(type)) {
|
||||
if (!mUsernameInput.validate()) isValidate = false;
|
||||
if (!mPasswordInput.validate()) isValidate = false;
|
||||
return isValidate;
|
||||
}
|
||||
|
||||
if (!mNameInput.validate()) isValidate = false;
|
||||
if (!mServerInput.validate()) isValidate = false;
|
||||
if (!mIpsecIdentifierInput.validate()) isValidate = false;
|
||||
if (type == VpnProfile.TYPE_IKEV2_IPSEC_PSK && !mIpsecSecretInput.validate()) {
|
||||
isValidate = false;
|
||||
}
|
||||
if (!isValidate) Log.w(TAG, "Failed to validate VPN profile!");
|
||||
return isValidate;
|
||||
}
|
||||
|
||||
private int getVpnType() {
|
||||
return VPN_TYPES.get(mType.getSelectedItemPosition());
|
||||
}
|
||||
|
||||
private void setTypesByFeature(Spinner typeSpinner) {
|
||||
String[] types = getContext().getResources().getStringArray(R.array.vpn_types);
|
||||
if (types.length != VPN_TYPES.size()) {
|
||||
@@ -487,15 +511,14 @@ class ConfigDialog extends AlertDialog implements TextWatcher,
|
||||
VpnProfile getProfile() {
|
||||
// First, save common fields.
|
||||
VpnProfile profile = new VpnProfile(mProfile.key);
|
||||
profile.name = mName.getText().toString();
|
||||
final int position = mType.getSelectedItemPosition();
|
||||
profile.type = VPN_TYPES.get(position);
|
||||
profile.server = mServer.getText().toString().trim();
|
||||
profile.username = mUsername.getText().toString();
|
||||
profile.password = mPassword.getText().toString();
|
||||
profile.name = mNameInput.getText();
|
||||
profile.type = getVpnType();
|
||||
profile.server = mServerInput.getText().trim();
|
||||
profile.username = mUsernameInput.getText();
|
||||
profile.password = mPasswordInput.getText();
|
||||
|
||||
// Save fields based on VPN type.
|
||||
profile.ipsecIdentifier = mIpsecIdentifier.getText().toString();
|
||||
profile.ipsecIdentifier = mIpsecIdentifierInput.getText();
|
||||
|
||||
if (hasProxy()) {
|
||||
String proxyHost = mProxyHost.getText().toString().trim();
|
||||
@@ -517,7 +540,7 @@ class ConfigDialog extends AlertDialog implements TextWatcher,
|
||||
// Then, save type-specific fields.
|
||||
switch (profile.type) {
|
||||
case VpnProfile.TYPE_IKEV2_IPSEC_PSK:
|
||||
profile.ipsecSecret = mIpsecSecret.getText().toString();
|
||||
profile.ipsecSecret = mIpsecSecretInput.getText();
|
||||
break;
|
||||
|
||||
case VpnProfile.TYPE_IKEV2_IPSEC_RSA:
|
||||
|
||||
@@ -124,6 +124,7 @@ public class ConfigDialogFragment extends InstrumentedDialogFragment implements
|
||||
VpnProfile profile = dialog.getProfile();
|
||||
|
||||
if (button == DialogInterface.BUTTON_POSITIVE) {
|
||||
if (!dialog.validate()) return;
|
||||
// Possibly throw up a dialog to explain lockdown VPN.
|
||||
final boolean shouldLockdown = dialog.isVpnAlwaysOn();
|
||||
final boolean shouldConnect = shouldLockdown || !dialog.isEditing();
|
||||
|
||||
@@ -77,7 +77,7 @@ import com.android.settings.utils.AndroidKeystoreAliasLoader;
|
||||
import com.android.settings.wifi.details2.WifiPrivacyPreferenceController;
|
||||
import com.android.settings.wifi.details2.WifiPrivacyPreferenceController2;
|
||||
import com.android.settings.wifi.dpp.WifiDppUtils;
|
||||
import com.android.settings.wifi.utils.SsidInputGroup;
|
||||
import com.android.settings.wifi.utils.TextInputGroup;
|
||||
import com.android.settingslib.Utils;
|
||||
import com.android.settingslib.utils.ThreadUtils;
|
||||
import com.android.wifi.flags.Flags;
|
||||
@@ -229,7 +229,7 @@ public class WifiConfigController2 implements TextWatcher,
|
||||
private final boolean mHideMeteredAndPrivacy;
|
||||
private final WifiManager mWifiManager;
|
||||
private final AndroidKeystoreAliasLoader mAndroidKeystoreAliasLoader;
|
||||
private SsidInputGroup mSsidInputGroup;
|
||||
private TextInputGroup mSsidInputGroup;
|
||||
|
||||
private final Context mContext;
|
||||
|
||||
@@ -299,7 +299,8 @@ public class WifiConfigController2 implements TextWatcher,
|
||||
wepWarningLayout.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
mSsidInputGroup = new SsidInputGroup(mContext, mView, R.id.ssid_layout, R.id.ssid);
|
||||
mSsidInputGroup = new TextInputGroup(mView, R.id.ssid_layout, R.id.ssid,
|
||||
R.string.wifi_ssid_hint);
|
||||
mSsidScanButton = (ImageButton) mView.findViewById(R.id.ssid_scanner_button);
|
||||
mIpSettingsSpinner = (Spinner) mView.findViewById(R.id.ip_settings);
|
||||
mIpSettingsSpinner.setOnItemSelectedListener(this);
|
||||
|
||||
@@ -28,7 +28,7 @@ import android.widget.TextView;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.wifi.utils.SsidInputGroup;
|
||||
import com.android.settings.wifi.utils.TextInputGroup;
|
||||
import com.android.settings.wifi.utils.WifiDialogHelper;
|
||||
import com.android.settingslib.RestrictedLockUtils;
|
||||
import com.android.settingslib.RestrictedLockUtilsInternal;
|
||||
@@ -120,7 +120,8 @@ public class WifiDialog extends AlertDialog implements WifiConfigUiBase,
|
||||
}
|
||||
|
||||
mDialogHelper = new WifiDialogHelper(this,
|
||||
new SsidInputGroup(getContext(), mView, R.id.ssid_layout, R.id.ssid));
|
||||
new TextInputGroup(mView, R.id.ssid_layout, R.id.ssid,
|
||||
R.string.vpn_field_required));
|
||||
}
|
||||
|
||||
@SuppressWarnings("MissingSuperCall") // TODO: Fix me
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ package com.android.settings.wifi.utils
|
||||
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
@@ -27,13 +28,17 @@ open class TextInputGroup(
|
||||
private val view: View,
|
||||
private val layoutId: Int,
|
||||
private val editTextId: Int,
|
||||
private val errorMessageId: Int,
|
||||
) {
|
||||
|
||||
private val View.layout: TextInputLayout?
|
||||
get() = findViewById(layoutId)
|
||||
val layout: TextInputLayout
|
||||
get() = view.requireViewById(layoutId)
|
||||
|
||||
private val View.editText: EditText?
|
||||
get() = findViewById(editTextId)
|
||||
val editText: EditText
|
||||
get() = view.requireViewById(editTextId)
|
||||
|
||||
val errorMessage: String
|
||||
get() = view.context.getString(errorMessageId)
|
||||
|
||||
private val textWatcher =
|
||||
object : TextWatcher {
|
||||
@@ -42,7 +47,7 @@ open class TextInputGroup(
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
view.layout?.isErrorEnabled = false
|
||||
layout.isErrorEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,18 +56,37 @@ open class TextInputGroup(
|
||||
}
|
||||
|
||||
fun addTextChangedListener(watcher: TextWatcher) {
|
||||
view.editText?.addTextChangedListener(watcher)
|
||||
editText.addTextChangedListener(watcher)
|
||||
}
|
||||
|
||||
fun getText(): String {
|
||||
return view.editText?.text?.toString() ?: ""
|
||||
var text: String
|
||||
get() = editText.text?.toString() ?: ""
|
||||
set(value) {
|
||||
editText.setText(value)
|
||||
}
|
||||
|
||||
fun setText(text: String) {
|
||||
view.editText?.setText(text)
|
||||
var helperText: String
|
||||
get() = layout.helperText?.toString() ?: ""
|
||||
set(value) {
|
||||
layout.setHelperText(value)
|
||||
}
|
||||
|
||||
fun setError(errorMessage: String?) {
|
||||
view.layout?.apply { error = errorMessage }
|
||||
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
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "TextInputGroup"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import androidx.appcompat.app.AlertDialog
|
||||
|
||||
class WifiDialogHelper(
|
||||
alertDialog: AlertDialog,
|
||||
private val ssidInputGroup: SsidInputGroup? = null,
|
||||
private val ssidInputGroup: TextInputGroup? = null,
|
||||
) : AlertDialogHelper(alertDialog) {
|
||||
|
||||
override fun canDismiss(): Boolean {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,7 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.icu.text.CaseMap;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.platform.test.annotations.DisableFlags;
|
||||
import android.platform.test.annotations.EnableFlags;
|
||||
@@ -56,12 +57,14 @@ import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import androidx.preference.PreferenceScreen;
|
||||
import androidx.preference.PreferenceViewHolder;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.flags.Flags;
|
||||
import com.android.settings.testutils.shadow.ShadowAccessibilityManager;
|
||||
import com.android.settings.testutils.shadow.ShadowFragment;
|
||||
import com.android.settingslib.widget.IllustrationPreference;
|
||||
import com.android.settingslib.widget.TopIntroPreference;
|
||||
|
||||
import com.google.android.setupcompat.util.WizardManagerHelper;
|
||||
@@ -79,6 +82,7 @@ import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
import org.robolectric.shadow.api.Shadow;
|
||||
import org.robolectric.shadows.ShadowApplication;
|
||||
import org.robolectric.shadows.ShadowLooper;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
@@ -315,6 +319,45 @@ public class ToggleFeaturePreferenceFragmentTest {
|
||||
assertThat(mFragment.getPreferenceScreen().getPreferenceCount()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void initAnimatedImagePreference_isAnimatable_setContentDescription() {
|
||||
mFragment.mFeatureName = "Test Feature";
|
||||
final View view =
|
||||
LayoutInflater.from(mContext).inflate(
|
||||
com.android.settingslib.widget.preference.illustration
|
||||
.R.layout.illustration_preference,
|
||||
null);
|
||||
IllustrationPreference preference = spy(new IllustrationPreference(mFragment.getContext()));
|
||||
when(preference.isAnimatable()).thenReturn(true);
|
||||
mFragment.initAnimatedImagePreference(mock(Uri.class), preference);
|
||||
|
||||
preference.onBindViewHolder(PreferenceViewHolder.createInstanceForTests(view));
|
||||
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
|
||||
|
||||
String expectedContentDescription = mFragment.getString(
|
||||
R.string.accessibility_illustration_content_description, mFragment.mFeatureName);
|
||||
assertThat(preference.getContentDescription().toString())
|
||||
.isEqualTo(expectedContentDescription);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void initAnimatedImagePreference_isNotAnimatable_notSetContentDescription() {
|
||||
mFragment.mFeatureName = "Test Feature";
|
||||
final View view =
|
||||
LayoutInflater.from(mContext).inflate(
|
||||
com.android.settingslib.widget.preference.illustration
|
||||
.R.layout.illustration_preference,
|
||||
null);
|
||||
IllustrationPreference preference = spy(new IllustrationPreference(mFragment.getContext()));
|
||||
when(preference.isAnimatable()).thenReturn(false);
|
||||
mFragment.initAnimatedImagePreference(mock(Uri.class), preference);
|
||||
|
||||
preference.onBindViewHolder(PreferenceViewHolder.createInstanceForTests(view));
|
||||
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
|
||||
|
||||
verify(preference, never()).setContentDescription(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@EnableFlags(Flags.FLAG_ACCESSIBILITY_SHOW_APP_INFO_BUTTON)
|
||||
public void createAppInfoPreference_withValidComponentName() {
|
||||
|
||||
@@ -613,6 +613,24 @@ public class ToggleScreenMagnificationPreferenceFragmentTest {
|
||||
verify(dialogDelegate).getDialogMetricsCategory(dialogId);
|
||||
}
|
||||
|
||||
@Test
|
||||
@EnableFlags(com.android.settings.accessibility.Flags
|
||||
.FLAG_ENABLE_MAGNIFICATION_CURSOR_FOLLOWING_DIALOG)
|
||||
public void onCreateDialog_setCursorFollowingModeDialogDelegate_invokeDialogDelegate() {
|
||||
ToggleScreenMagnificationPreferenceFragment fragment =
|
||||
mFragController.create(
|
||||
R.id.main_content, /* bundle= */ null).start().resume().get();
|
||||
final DialogCreatable dialogDelegate = mock(DialogCreatable.class, RETURNS_DEEP_STUBS);
|
||||
final int dialogId = DialogEnums.DIALOG_MAGNIFICATION_CURSOR_FOLLOWING_MODE;
|
||||
when(dialogDelegate.getDialogMetricsCategory(anyInt())).thenReturn(dialogId);
|
||||
fragment.setMagnificationCursorFollowingModeDialogDelegate(dialogDelegate);
|
||||
|
||||
fragment.onCreateDialog(dialogId);
|
||||
fragment.getDialogMetricsCategory(dialogId);
|
||||
verify(dialogDelegate).onCreateDialog(dialogId);
|
||||
verify(dialogDelegate).getDialogMetricsCategory(dialogId);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getMetricsCategory_returnsCorrectCategory() {
|
||||
ToggleScreenMagnificationPreferenceFragment fragment =
|
||||
@@ -826,6 +844,7 @@ public class ToggleScreenMagnificationPreferenceFragmentTest {
|
||||
MagnificationOneFingerPanningPreferenceController.PREF_KEY,
|
||||
MagnificationAlwaysOnPreferenceController.PREF_KEY,
|
||||
MagnificationJoystickPreferenceController.PREF_KEY,
|
||||
MagnificationCursorFollowingModePreferenceController.PREF_KEY,
|
||||
MagnificationFeedbackPreferenceController.PREF_KEY);
|
||||
|
||||
final List<SearchIndexableRaw> rawData = ToggleScreenMagnificationPreferenceFragment
|
||||
@@ -881,7 +900,9 @@ public class ToggleScreenMagnificationPreferenceFragmentTest {
|
||||
@EnableFlags({
|
||||
com.android.settings.accessibility.Flags.FLAG_FIX_A11Y_SETTINGS_SEARCH,
|
||||
Flags.FLAG_ENABLE_MAGNIFICATION_ONE_FINGER_PANNING_GESTURE,
|
||||
Flags.FLAG_ENABLE_LOW_VISION_HATS})
|
||||
Flags.FLAG_ENABLE_LOW_VISION_HATS,
|
||||
com.android.settings.accessibility.Flags
|
||||
.FLAG_ENABLE_MAGNIFICATION_CURSOR_FOLLOWING_DIALOG})
|
||||
public void getNonIndexableKeys_hasShortcutAndAllFeaturesEnabled_allItemsSearchable() {
|
||||
mShadowAccessibilityManager.setAccessibilityShortcutTargets(
|
||||
TRIPLETAP, List.of(MAGNIFICATION_CONTROLLER_NAME));
|
||||
|
||||
@@ -34,6 +34,7 @@ import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Looper;
|
||||
import android.os.UserManager;
|
||||
import android.platform.test.annotations.DisableFlags;
|
||||
import android.platform.test.annotations.EnableFlags;
|
||||
import android.platform.test.flag.junit.SetFlagsRule;
|
||||
import android.util.Pair;
|
||||
@@ -175,6 +176,7 @@ public class BluetoothDevicePreferenceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisableFlags(Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI)
|
||||
public void onClicked_deviceNotBonded_shouldLogBluetoothPairEvent() {
|
||||
when(mCachedBluetoothDevice.isConnected()).thenReturn(false);
|
||||
when(mCachedBluetoothDevice.getBondState()).thenReturn(BluetoothDevice.BOND_NONE);
|
||||
@@ -192,6 +194,7 @@ public class BluetoothDevicePreferenceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisableFlags(Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI)
|
||||
public void onClicked_deviceNotBonded_shouldLogBluetoothPairEventAndPairWithoutNameEvent() {
|
||||
when(mCachedBluetoothDevice.isConnected()).thenReturn(false);
|
||||
when(mCachedBluetoothDevice.getBondState()).thenReturn(BluetoothDevice.BOND_NONE);
|
||||
|
||||
@@ -28,7 +28,6 @@ import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -52,7 +51,9 @@ import android.media.session.ISession;
|
||||
import android.media.session.ISessionController;
|
||||
import android.media.session.MediaSessionManager;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import android.os.RemoteException;
|
||||
import android.platform.test.flag.junit.SetFlagsRule;
|
||||
import android.util.DisplayMetrics;
|
||||
@@ -81,14 +82,12 @@ import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnit;
|
||||
import org.mockito.junit.MockitoRule;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.android.util.concurrent.InlineExecutorService;
|
||||
import org.robolectric.annotation.Config;
|
||||
import org.robolectric.shadow.api.Shadow;
|
||||
import org.robolectric.util.ReflectionHelpers;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(
|
||||
@@ -122,6 +121,7 @@ public class AudioStreamMediaServiceTest {
|
||||
@Mock private PackageManager mPackageManager;
|
||||
@Mock private DisplayMetrics mDisplayMetrics;
|
||||
@Mock private Context mContext;
|
||||
@Mock private Handler mHandler;
|
||||
private FakeFeatureFactory mFeatureFactory;
|
||||
private AudioStreamMediaService mAudioStreamMediaService;
|
||||
|
||||
@@ -145,11 +145,18 @@ public class AudioStreamMediaServiceTest {
|
||||
when(mCachedBluetoothDevice.getName()).thenReturn(DEVICE_NAME);
|
||||
when(mLocalBluetoothProfileManager.getVolumeControlProfile())
|
||||
.thenReturn(mVolumeControlProfile);
|
||||
|
||||
mAudioStreamMediaService = spy(new AudioStreamMediaService());
|
||||
when(mHandler.post(any(Runnable.class))).thenAnswer(invocation -> {
|
||||
((Runnable) invocation.getArgument(0)).run();
|
||||
return null;
|
||||
});
|
||||
when(mHandler.getLooper()).thenReturn(Looper.getMainLooper());
|
||||
mAudioStreamMediaService = spy(new AudioStreamMediaService() {
|
||||
@Override
|
||||
Handler getHandler() {
|
||||
return mHandler;
|
||||
}
|
||||
});
|
||||
ReflectionHelpers.setField(mAudioStreamMediaService, "mBase", mContext);
|
||||
ReflectionHelpers.setField(
|
||||
mAudioStreamMediaService, "mExecutor", new InlineExecutorService());
|
||||
when(mAudioStreamMediaService.getSystemService(anyString()))
|
||||
.thenReturn(mMediaSessionManager);
|
||||
when(mMediaSessionManager.createSession(any(), anyString(), any())).thenReturn(mISession);
|
||||
@@ -391,31 +398,6 @@ public class AudioStreamMediaServiceTest {
|
||||
verify(mAudioStreamMediaService).stopSelf();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bluetoothCallback_onMemberDeviceDisconnect_stopSelf() {
|
||||
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
|
||||
when(mCachedBluetoothDevice.getDevice()).thenReturn(mock(BluetoothDevice.class));
|
||||
CachedBluetoothDevice member = mock(CachedBluetoothDevice.class);
|
||||
when(mCachedBluetoothDevice.getMemberDevice()).thenReturn(Set.of(member));
|
||||
when(member.getDevice()).thenReturn(mDevice);
|
||||
var devices = new ArrayList<BluetoothDevice>();
|
||||
devices.add(mDevice);
|
||||
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra(BROADCAST_ID, 1);
|
||||
intent.putParcelableArrayListExtra(DEVICES, devices);
|
||||
|
||||
mAudioStreamMediaService.onCreate();
|
||||
assertThat(mAudioStreamMediaService.mBluetoothCallback).isNotNull();
|
||||
mAudioStreamMediaService.onStartCommand(intent, /* flags= */ 0, /* startId= */ 0);
|
||||
mAudioStreamMediaService.mBluetoothCallback.onProfileConnectionStateChanged(
|
||||
mCachedBluetoothDevice,
|
||||
BluetoothAdapter.STATE_DISCONNECTED,
|
||||
BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
|
||||
|
||||
verify(mAudioStreamMediaService).stopSelf();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mediaSessionCallback_onPause_setVolume() {
|
||||
mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
|
||||
|
||||
@@ -19,9 +19,16 @@ package com.android.settings.network;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.telephony.TelephonyManager;
|
||||
|
||||
import com.android.settings.R;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
@@ -34,28 +41,66 @@ import org.robolectric.util.ReflectionHelpers;
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class NetworkResetPreferenceControllerTest {
|
||||
|
||||
@Mock
|
||||
private TelephonyManager mTelephonyManager;
|
||||
@Mock
|
||||
private NetworkResetRestrictionChecker mRestrictionChecker;
|
||||
private NetworkResetPreferenceController mController;
|
||||
private Context mContext;
|
||||
private Resources mResources;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
mController = new NetworkResetPreferenceController(RuntimeEnvironment.application);
|
||||
mContext = spy(RuntimeEnvironment.application);
|
||||
|
||||
mResources = spy(mContext.getResources());
|
||||
when(mContext.getResources()).thenReturn(mResources);
|
||||
when(mContext.getSystemService(Context.TELEPHONY_SERVICE)).thenReturn(mTelephonyManager);
|
||||
|
||||
mController = new NetworkResetPreferenceController(mContext);
|
||||
ReflectionHelpers.setField(mController, "mRestrictionChecker", mRestrictionChecker);
|
||||
|
||||
// Availability defaults
|
||||
when(mTelephonyManager.isDataCapable()).thenReturn(true);
|
||||
when(mResources.getBoolean(R.bool.config_show_sim_info)).thenReturn(true);
|
||||
when(mRestrictionChecker.isRestrictionEnforcedByAdmin()).thenReturn(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsAvailable_shouldReturnTrueWhenNoUserRestriction() {
|
||||
when(mRestrictionChecker.isRestrictionEnforcedByAdmin()).thenReturn(true);
|
||||
public void testIsAvailable_showSimInfo_notWifiOnly() {
|
||||
assertThat(mController.isAvailable()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsAvailable_hideSimInfo_notWifiOnly() {
|
||||
when(mResources.getBoolean(R.bool.config_show_sim_info)).thenReturn(false);
|
||||
assertThat(mController.isAvailable()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsAvailable_showSimInfo_wifiOnly() {
|
||||
when(mTelephonyManager.isDataCapable()).thenReturn(false);
|
||||
assertThat(mController.isAvailable()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsAvailable_userRestriction() {
|
||||
when(mRestrictionChecker.isRestrictionEnforcedByAdmin()).thenReturn(true);
|
||||
when(mRestrictionChecker.hasUserRestriction()).thenReturn(true);
|
||||
|
||||
assertThat(mController.isAvailable()).isFalse();
|
||||
|
||||
verify(mRestrictionChecker, never()).isRestrictionEnforcedByAdmin();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsAvailable_noUserRestriction() {
|
||||
when(mRestrictionChecker.isRestrictionEnforcedByAdmin()).thenReturn(true);
|
||||
when(mRestrictionChecker.hasUserRestriction()).thenReturn(false);
|
||||
|
||||
assertThat(mController.isAvailable()).isTrue();
|
||||
|
||||
verify(mRestrictionChecker, never()).isRestrictionEnforcedByAdmin();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user