diff --git a/res/layout/accessibility_magnification_mode_header.xml b/res/layout/accessibility_dialog_header.xml similarity index 92% rename from res/layout/accessibility_magnification_mode_header.xml rename to res/layout/accessibility_dialog_header.xml index e4765535f27..ace8b239d0e 100644 --- a/res/layout/accessibility_magnification_mode_header.xml +++ b/res/layout/accessibility_dialog_header.xml @@ -21,9 +21,9 @@ android:padding="?android:attr/dialogPreferredPadding"> diff --git a/res/layout/vpn_dialog.xml b/res/layout/vpn_dialog.xml index fadd2025f14..f0e7b836c64 100644 --- a/res/layout/vpn_dialog.xml +++ b/res/layout/vpn_dialog.xml @@ -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"> Magnification Magnification shortcut + + Cursor following + + Choose how Magnification follows your cursor. + + Move screen continuously as mouse moves + + Move screen keeping mouse at center of screen + + Move screen when mouse touches edges of screen Magnify typing @@ -6601,8 +6611,12 @@ %1$s to %2$s %1$s %2$s - - %1$s %2$s + + %1$s, %2$s %3$s + + Selected + + Unselected Battery usage chart @@ -7276,6 +7290,12 @@ Data usage charges may apply. generic error. [CHAR LIMIT=120] --> The information entered doesn\'t support always-on VPN + + (optional) + + (required) + + The field is required Cancel @@ -12924,12 +12944,12 @@ Data usage charges may apply. Enable freeform windows - + Enable desktop experience features - Enable Desktop View on the device and on secondary displays. - - Enable Desktop View on secondary displays. + Enable desktop windowing on the device and on secondary displays. + + Enable desktop windowing on secondary displays. Enable freeform windows on secondary display @@ -14283,4 +14303,6 @@ Data usage charges may apply. Forgot PIN Web content filters + + %1$s animation diff --git a/src/com/android/settings/accessibility/AccessibilityDialogUtils.java b/src/com/android/settings/accessibility/AccessibilityDialogUtils.java index c89b8d7122a..dc4900861a2 100644 --- a/src/com/android/settings/accessibility/AccessibilityDialogUtils.java +++ b/src/com/android/settings/accessibility/AccessibilityDialogUtils.java @@ -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; } /** diff --git a/src/com/android/settings/accessibility/MagnificationCursorFollowingModePreferenceController.java b/src/com/android/settings/accessibility/MagnificationCursorFollowingModePreferenceController.java new file mode 100644 index 00000000000..d217ead007f --- /dev/null +++ b/src/com/android/settings/accessibility/MagnificationCursorFollowingModePreferenceController.java @@ -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 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; + } + } +} diff --git a/src/com/android/settings/accessibility/MagnificationModePreferenceController.java b/src/com/android/settings/accessibility/MagnificationModePreferenceController.java index 71ea4c23958..93cb23b7e2a 100644 --- a/src/com/android/settings/accessibility/MagnificationModePreferenceController.java +++ b/src/com/android/settings/accessibility/MagnificationModePreferenceController.java @@ -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); diff --git a/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java b/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java index 66c32df1798..d8c39856368 100644 --- a/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java +++ b/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java @@ -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 diff --git a/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java b/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java index 8b525079bef..71c95c0c7bf 100644 --- a/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java +++ b/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java @@ -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); } diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaService.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaService.java index a1bb84c1daa..ec8d7bcc206 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaService.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaService.java @@ -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 mDevices; + @Nullable private Map mStateByDevice; @Nullable private LocalBluetoothManager mLocalBtManager; @Nullable private AudioStreamsHelper mAudioStreamsHelper; @Nullable private LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant; @@ -154,7 +153,6 @@ public class AudioStreamMediaService extends Service { Log.w(TAG, "onCreate() : mLeBroadcastAssistant is null!"); return; } - mHysteresisModeFixAvailable = BluetoothUtils.isAudioSharingHysteresisModeFixAvailable(this); mNotificationManager = getSystemService(NotificationManager.class); if (mNotificationManager == null) { @@ -162,7 +160,8 @@ public class AudioStreamMediaService extends Service { return; } - mExecutor.execute( + mHandlerThread.start(); + getHandler().post( () -> { if (mLocalBtManager == null || mLeBroadcastAssistant == null @@ -184,45 +183,49 @@ public class AudioStreamMediaService extends Service { mVolumeControl = mLocalBtManager.getProfileManager().getVolumeControlProfile(); if (mVolumeControl != null) { mVolumeControlCallback = new VolumeControlCallback(); - mVolumeControl.registerCallback(mExecutor, mVolumeControlCallback); + mVolumeControl.registerCallback(getHandler()::post, mVolumeControlCallback); } mBroadcastAssistantCallback = new AssistantCallback(); mLeBroadcastAssistant.registerServiceCallBack( - mExecutor, mBroadcastAssistantCallback); + getHandler()::post, mBroadcastAssistantCallback); + + mHysteresisModeFixAvailable = + BluetoothUtils.isAudioSharingHysteresisModeFixAvailable(this); }); } + @VisibleForTesting + Handler getHandler() { + return mHandlerThread.getThreadHandler(); + } + @Override public void onDestroy() { Log.d(TAG, "onDestroy()"); - super.onDestroy(); - if (BluetoothUtils.isAudioSharingUIAvailable(this)) { - if (mDevices != null) { - mDevices.clear(); - mDevices = null; - } - synchronized (mLocalSessionLock) { - if (mLocalSession != null) { - mLocalSession.release(); - mLocalSession = null; - } - } - mExecutor.execute( - () -> { - if (mLocalBtManager != null) { - mLocalBtManager.getEventManager().unregisterCallback( - mBluetoothCallback); - } - if (mLeBroadcastAssistant != null && mBroadcastAssistantCallback != null) { - mLeBroadcastAssistant.unregisterServiceCallBack( - mBroadcastAssistantCallback); - } - if (mVolumeControl != null && mVolumeControlCallback != null) { - mVolumeControl.unregisterCallback(mVolumeControlCallback); - } - }); - } + getHandler().post( + () -> { + if (mStateByDevice != null) { + mStateByDevice.clear(); + mStateByDevice = null; + } + if (mLocalSession != null) { + mLocalSession.release(); + mLocalSession = null; + } + if (mLocalBtManager != null) { + mLocalBtManager.getEventManager().unregisterCallback( + mBluetoothCallback); + } + if (mLeBroadcastAssistant != null && mBroadcastAssistantCallback != null) { + mLeBroadcastAssistant.unregisterServiceCallBack( + mBroadcastAssistantCallback); + } + if (mVolumeControl != null && mVolumeControlCallback != null) { + mVolumeControl.unregisterCallback(mVolumeControlCallback); + } + }); + mHandlerThread.quitSafely(); } @Override @@ -233,53 +236,59 @@ public class AudioStreamMediaService extends Service { stopSelf(); return START_NOT_STICKY; } - mBroadcastId = intent.getIntExtra(BROADCAST_ID, -1); - if (mBroadcastId == -1) { - Log.w(TAG, "Invalid broadcast ID. Service will not start."); - stopSelf(); - return START_NOT_STICKY; - } - var extra = intent.getParcelableArrayListExtra(DEVICES, BluetoothDevice.class); - if (extra == null || extra.isEmpty()) { - Log.w(TAG, "No device. Service will not start."); - stopSelf(); - return START_NOT_STICKY; - } - mDevices = Collections.synchronizedList(extra); - MediaSession.Token token = - getOrCreateLocalMediaSession(intent.getStringExtra(BROADCAST_TITLE)); - startForeground(NOTIFICATION_ID, buildNotification(token)); + getHandler().post(() -> { + mBroadcastId = intent.getIntExtra(BROADCAST_ID, -1); + if (mBroadcastId == -1) { + Log.w(TAG, "Invalid broadcast ID. Service will not start."); + stopSelf(); + return; + } + var devices = intent.getParcelableArrayListExtra(DEVICES, BluetoothDevice.class); + if (devices == null || devices.isEmpty()) { + Log.w(TAG, "No device. Service will not start."); + stopSelf(); + } else { + mStateByDevice = new HashMap<>(); + devices.forEach(d -> mStateByDevice.put(d, STREAMING)); + MediaSession.Token token = + getOrCreateLocalMediaSession(intent.getStringExtra(BROADCAST_TITLE)); + startForeground(NOTIFICATION_ID, buildNotification(token)); + } + }); return START_NOT_STICKY; } private MediaSession.Token getOrCreateLocalMediaSession(String title) { - synchronized (mLocalSessionLock) { - if (mLocalSession != null) { - return mLocalSession.getSessionToken(); - } - mLocalSession = new MediaSession(this, TAG); - mLocalSession.setMetadata( - new MediaMetadata.Builder() - .putString(MediaMetadata.METADATA_KEY_TITLE, title) - .putLong(MediaMetadata.METADATA_KEY_DURATION, STATIC_PLAYBACK_DURATION) - .build()); - mLocalSession.setActive(true); - mLocalSession.setPlaybackState(getPlaybackState()); - mMediaSessionCallback = new MediaSessionCallback(); - mLocalSession.setCallback(mMediaSessionCallback); + if (mLocalSession != null) { return mLocalSession.getSessionToken(); } + mLocalSession = new MediaSession(this, TAG); + mLocalSession.setMetadata( + new MediaMetadata.Builder() + .putString(MediaMetadata.METADATA_KEY_TITLE, title) + .putLong(MediaMetadata.METADATA_KEY_DURATION, STATIC_PLAYBACK_DURATION) + .build()); + mLocalSession.setActive(true); + mLocalSession.setPlaybackState(getPlaybackState()); + mMediaSessionCallback = new MediaSessionCallback(); + mLocalSession.setCallback(mMediaSessionCallback, getHandler()); + return mLocalSession.getSessionToken(); } private PlaybackState getPlaybackState() { - if (mIsHysteresis.get()) { + if (isAllDeviceHysteresis()) { return mPlayStateHysteresisBuilder.build(); } - return mIsMuted.get() ? mPlayStatePausingBuilder.build() : mPlayStatePlayingBuilder.build(); + return mIsMuted ? mPlayStatePausingBuilder.build() : mPlayStatePlayingBuilder.build(); + } + + private boolean isAllDeviceHysteresis() { + return mHysteresisModeFixAvailable && mStateByDevice != null + && mStateByDevice.values().stream().allMatch(v -> v == PAUSED); } private String getDeviceName() { - if (mDevices == null || mDevices.isEmpty() || mLocalBtManager == null) { + if (mStateByDevice == null || mStateByDevice.isEmpty() || mLocalBtManager == null) { return DEFAULT_DEVICE_NAME; } @@ -288,7 +297,8 @@ public class AudioStreamMediaService extends Service { return DEFAULT_DEVICE_NAME; } - CachedBluetoothDevice device = manager.findDevice(mDevices.get(0)); + CachedBluetoothDevice device = manager.findDevice( + mStateByDevice.keySet().iterator().next()); return device != null ? device.getName() : DEFAULT_DEVICE_NAME; } @@ -304,7 +314,7 @@ public class AudioStreamMediaService extends Service { .setSmallIcon(com.android.settingslib.R.drawable.ic_bt_le_audio_sharing) .setStyle(mediaStyle) .setContentText(getString( - mIsHysteresis.get() ? BROADCAST_STREAM_PAUSED_TEXT : + isAllDeviceHysteresis() ? BROADCAST_STREAM_PAUSED_TEXT : BROADCAST_LISTENING_NOW_TEXT)) .setSilent(true); return notificationBuilder.build(); @@ -333,7 +343,8 @@ public class AudioStreamMediaService extends Service { public void onReceiveStateChanged( BluetoothDevice sink, int sourceId, BluetoothLeBroadcastReceiveState state) { super.onReceiveStateChanged(sink, sourceId, state); - if (!mHysteresisModeFixAvailable || mDevices == null || !mDevices.contains(sink)) { + if (!mHysteresisModeFixAvailable || mStateByDevice == null + || !mStateByDevice.containsKey(sink)) { return; } var sourceState = LocalBluetoothLeBroadcastAssistant.getLocalSourceState(state); @@ -343,12 +354,10 @@ public class AudioStreamMediaService extends Service { if (!streaming && !paused) { return; } - // Atomically update mIsHysteresis if its current value is not the current paused state - if (mIsHysteresis.compareAndSet(!paused, paused)) { - synchronized (mLocalSessionLock) { - if (mLocalSession == null) { - return; - } + boolean shouldUpdate = mStateByDevice.get(sink) != sourceState; + if (shouldUpdate) { + mStateByDevice.put(sink, sourceState); + if (mLocalSession != null) { mLocalSession.setPlaybackState(getPlaybackState()); if (mNotificationManager != null) { mNotificationManager.notify( @@ -356,7 +365,7 @@ public class AudioStreamMediaService extends Service { buildNotification(mLocalSession.getSessionToken()) ); } - Log.d(TAG, "updating hysteresis mode to : " + paused); + Log.d(TAG, "updating source state to : " + sourceState); } } } @@ -374,24 +383,22 @@ public class AudioStreamMediaService extends Service { @Override public void onDeviceVolumeChanged( @NonNull BluetoothDevice device, @IntRange(from = -255, to = 255) int volume) { - if (mDevices == null || mDevices.isEmpty()) { + if (mStateByDevice == null || mStateByDevice.isEmpty()) { Log.w(TAG, "active device or device has source is null!"); return; } Log.d( TAG, "onDeviceVolumeChanged() bluetoothDevice : " + device + " volume: " + volume); - if (mDevices.contains(device)) { + if (mStateByDevice.containsKey(device)) { if (volume == 0) { - mIsMuted.set(true); + mIsMuted = true; } else { - mIsMuted.set(false); - mLatestPositiveVolume.set(volume); + mIsMuted = false; + mLatestPositiveVolume = volume; } - synchronized (mLocalSessionLock) { - if (mLocalSession != null) { - mLocalSession.setPlaybackState(getPlaybackState()); - } + if (mLocalSession != null) { + mLocalSession.setPlaybackState(getPlaybackState()); } } } @@ -400,10 +407,12 @@ public class AudioStreamMediaService extends Service { private class BtCallback implements BluetoothCallback { @Override public void onBluetoothStateChanged(int bluetoothState) { - if (BluetoothAdapter.STATE_OFF == bluetoothState) { - Log.d(TAG, "onBluetoothStateChanged() : stopSelf"); - stopSelf(); - } + getHandler().post(() -> { + if (BluetoothAdapter.STATE_OFF == bluetoothState) { + Log.d(TAG, "onBluetoothStateChanged() : stopSelf"); + stopSelf(); + } + }); } @Override @@ -411,24 +420,17 @@ public class AudioStreamMediaService extends Service { @NonNull CachedBluetoothDevice cachedDevice, @ConnectionState int state, int bluetoothProfile) { - if (state == BluetoothAdapter.STATE_DISCONNECTED - && bluetoothProfile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT - && mDevices != null) { - mDevices.remove(cachedDevice.getDevice()); - cachedDevice - .getMemberDevice() - .forEach( - m -> { - // Check nullability to pass NullAway check - if (mDevices != null) { - mDevices.remove(m.getDevice()); - } - }); - } - if (mDevices == null || mDevices.isEmpty()) { - Log.d(TAG, "onProfileConnectionStateChanged() : stopSelf"); - stopSelf(); - } + getHandler().post(() -> { + if (state == BluetoothAdapter.STATE_DISCONNECTED + && bluetoothProfile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT + && mStateByDevice != null) { + mStateByDevice.remove(cachedDevice.getDevice()); + } + if (mStateByDevice == null || mStateByDevice.isEmpty()) { + Log.d(TAG, "onProfileConnectionStateChanged() : stopSelf"); + stopSelf(); + } + }); } } @@ -454,10 +456,8 @@ public class AudioStreamMediaService extends Service { @Override public void onSeekTo(long pos) { Log.d(TAG, "onSeekTo: " + pos); - synchronized (mLocalSessionLock) { - if (mLocalSession != null) { - mLocalSession.setPlaybackState(getPlaybackState()); - } + if (mLocalSession != null) { + mLocalSession.setPlaybackState(getPlaybackState()); } } @@ -484,28 +484,26 @@ public class AudioStreamMediaService extends Service { } private void handleOnPlay() { - if (mDevices == null || mDevices.isEmpty()) { + if (mStateByDevice == null || mStateByDevice.isEmpty()) { Log.w(TAG, "active device or device has source is null!"); return; } - Log.d( - TAG, - "onPlay() setting volume for device : " - + mDevices.getFirst() - + " volume: " - + mLatestPositiveVolume.get()); - setDeviceVolume(mDevices.getFirst(), mLatestPositiveVolume.get()); + mStateByDevice.keySet().forEach(device -> { + Log.d(TAG, "onPlay() setting volume for device : " + device + " volume: " + + mLatestPositiveVolume); + setDeviceVolume(device, mLatestPositiveVolume); + }); } private void handleOnPause() { - if (mDevices == null || mDevices.isEmpty()) { + if (mStateByDevice == null || mStateByDevice.isEmpty()) { Log.w(TAG, "active device or device has source is null!"); return; } - Log.d( - TAG, - "onPause() setting volume for device : " + mDevices.getFirst() + " volume: " + 0); - setDeviceVolume(mDevices.getFirst(), /* volume= */ 0); + mStateByDevice.keySet().forEach(device -> { + Log.d(TAG, "onPause() setting volume for device : " + device + " volume: " + 0); + setDeviceVolume(device, /* volume= */ 0); + }); } private void setDeviceVolume(BluetoothDevice device, int volume) { @@ -514,7 +512,7 @@ public class AudioStreamMediaService extends Service { ThreadUtils.postOnBackgroundThread( () -> { if (mVolumeControl != null) { - mVolumeControl.setDeviceVolume(device, volume, true); + mVolumeControl.setDeviceVolume(device, volume, false); mMetricsFeatureProvider.action( getApplicationContext(), event, volume == 0 ? 1 : 0); } diff --git a/src/com/android/settings/connecteddevice/display/DesktopExperienceFlags.kt b/src/com/android/settings/connecteddevice/display/DesktopExperienceFlags.kt new file mode 100644 index 00000000000..c6ce00d285d --- /dev/null +++ b/src/com/android/settings/connecteddevice/display/DesktopExperienceFlags.kt @@ -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 +} \ No newline at end of file diff --git a/src/com/android/settings/connecteddevice/display/ExternalDisplaySettingsConfiguration.java b/src/com/android/settings/connecteddevice/display/ExternalDisplaySettingsConfiguration.java index 1148aa59704..52ec8d27c63 100644 --- a/src/com/android/settings/connecteddevice/display/ExternalDisplaySettingsConfiguration.java +++ b/src/com/android/settings/connecteddevice/display/ExternalDisplaySettingsConfiguration.java @@ -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) { diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceController.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceController.java index a248bdff16f..2681067000c 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceController.java +++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceController.java @@ -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 levels, int index) { - final int fromBatteryLevelIndex = - index == BatteryChartViewModel.SELECTED_INDEX_ALL ? 0 : index; + final int fromBatteryLevelIndex = index == SELECTED_INDEX_ALL ? 0 : index; final int toBatteryLevelIndex = - index == BatteryChartViewModel.SELECTED_INDEX_ALL - ? levels.size() - 1 - : index + 1; + index == SELECTED_INDEX_ALL ? levels.size() - 1 : index + 1; return mPrefContext.getString( R.string.battery_level_percentage, generateBatteryLevelText(levels.get(fromBatteryLevelIndex)), @@ -687,9 +694,9 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll return index == timestamps.size() - 1 ? generateText(timestamps, index) : mContext.getString( - R.string.battery_usage_timestamps_content_description, - generateText(timestamps, index), - generateText(timestamps, index + 1)); + R.string.battery_usage_timestamps_content_description, + generateText(timestamps, index), + generateText(timestamps, index + 1)); } HourlyChartLabelTextGenerator updateSpecialCaseContext( diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryChartView.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartView.java index eafccdb124e..393d751c0ae 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/BatteryChartView.java +++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartView.java @@ -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); diff --git a/src/com/android/settings/network/NetworkResetPreferenceController.java b/src/com/android/settings/network/NetworkResetPreferenceController.java index af288fa88a5..ef3dca2fcd5 100644 --- a/src/com/android/settings/network/NetworkResetPreferenceController.java +++ b/src/com/android/settings/network/NetworkResetPreferenceController.java @@ -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 diff --git a/src/com/android/settings/vpn2/ConfigDialog.java b/src/com/android/settings/vpn2/ConfigDialog.java index 1c001cb8bab..8dbcf94b023 100644 --- a/src/com/android/settings/vpn2/ConfigDialog.java +++ b/src/com/android/settings/vpn2/ConfigDialog.java @@ -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: diff --git a/src/com/android/settings/vpn2/ConfigDialogFragment.java b/src/com/android/settings/vpn2/ConfigDialogFragment.java index 559003aa4c0..6bffef7c6d5 100644 --- a/src/com/android/settings/vpn2/ConfigDialogFragment.java +++ b/src/com/android/settings/vpn2/ConfigDialogFragment.java @@ -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(); diff --git a/src/com/android/settings/wifi/WifiConfigController2.java b/src/com/android/settings/wifi/WifiConfigController2.java index 1bf1102dde1..a080fc8c5bc 100644 --- a/src/com/android/settings/wifi/WifiConfigController2.java +++ b/src/com/android/settings/wifi/WifiConfigController2.java @@ -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); diff --git a/src/com/android/settings/wifi/WifiDialog.java b/src/com/android/settings/wifi/WifiDialog.java index 40d22e60fa2..38c99b6a759 100644 --- a/src/com/android/settings/wifi/WifiDialog.java +++ b/src/com/android/settings/wifi/WifiDialog.java @@ -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 diff --git a/src/com/android/settings/wifi/utils/SsidInputGroup.kt b/src/com/android/settings/wifi/utils/SsidInputGroup.kt deleted file mode 100644 index 5d8f8d418e3..00000000000 --- a/src/com/android/settings/wifi/utils/SsidInputGroup.kt +++ /dev/null @@ -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 - } -} diff --git a/src/com/android/settings/wifi/utils/TextInputGroup.kt b/src/com/android/settings/wifi/utils/TextInputGroup.kt index 8006dad3bc4..53c80ffb241 100644 --- a/src/com/android/settings/wifi/utils/TextInputGroup.kt +++ b/src/com/android/settings/wifi/utils/TextInputGroup.kt @@ -18,6 +18,7 @@ package com.android.settings.wifi.utils import android.text.Editable import android.text.TextWatcher +import android.util.Log import android.view.View import android.widget.EditText import com.google.android.material.textfield.TextInputLayout @@ -27,13 +28,17 @@ open class TextInputGroup( private val view: View, private val layoutId: Int, private val editTextId: Int, + private val errorMessageId: Int, ) { - private val View.layout: TextInputLayout? - get() = findViewById(layoutId) + val layout: TextInputLayout + get() = view.requireViewById(layoutId) - private val View.editText: EditText? - get() = findViewById(editTextId) + val editText: EditText + get() = view.requireViewById(editTextId) + + val errorMessage: String + get() = view.context.getString(errorMessageId) private val textWatcher = object : TextWatcher { @@ -42,7 +47,7 @@ open class TextInputGroup( override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} override fun afterTextChanged(s: Editable?) { - view.layout?.isErrorEnabled = false + layout.isErrorEnabled = false } } @@ -51,18 +56,37 @@ open class TextInputGroup( } fun addTextChangedListener(watcher: TextWatcher) { - view.editText?.addTextChangedListener(watcher) + editText.addTextChangedListener(watcher) } - fun getText(): String { - return view.editText?.text?.toString() ?: "" + var text: String + get() = editText.text?.toString() ?: "" + set(value) { + editText.setText(value) + } + + var helperText: String + get() = layout.helperText?.toString() ?: "" + set(value) { + layout.setHelperText(value) + } + + var error: String + get() = layout.error?.toString() ?: "" + set(value) { + layout.setError(value) + } + + open fun validate(): Boolean { + val isValid = text.isNotEmpty() + if (!isValid) { + Log.w(TAG, "validate failed in ${layout.hint ?: "unknown"}") + error = errorMessage.toString() + } + return isValid } - fun setText(text: String) { - view.editText?.setText(text) - } - - fun setError(errorMessage: String?) { - view.layout?.apply { error = errorMessage } + companion object { + const val TAG = "TextInputGroup" } } diff --git a/src/com/android/settings/wifi/utils/WifiDialogHelper.kt b/src/com/android/settings/wifi/utils/WifiDialogHelper.kt index 3b23b1a7e50..aa41b969a6e 100644 --- a/src/com/android/settings/wifi/utils/WifiDialogHelper.kt +++ b/src/com/android/settings/wifi/utils/WifiDialogHelper.kt @@ -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 { diff --git a/tests/robotests/src/com/android/settings/accessibility/MagnificationCursorFollowingModePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/MagnificationCursorFollowingModePreferenceControllerTest.java new file mode 100644 index 00000000000..42efdfe6784 --- /dev/null +++ b/tests/robotests/src/com/android/settings/accessibility/MagnificationCursorFollowingModePreferenceControllerTest.java @@ -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; + } + } +} diff --git a/tests/robotests/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragmentTest.java b/tests/robotests/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragmentTest.java index 571075cba31..f72b591353a 100644 --- a/tests/robotests/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragmentTest.java +++ b/tests/robotests/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragmentTest.java @@ -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() { diff --git a/tests/robotests/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentTest.java b/tests/robotests/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentTest.java index 3c136f04356..6407c081aea 100644 --- a/tests/robotests/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentTest.java +++ b/tests/robotests/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentTest.java @@ -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 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)); diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDevicePreferenceTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDevicePreferenceTest.java index 6a72c7d2e0c..d318e061656 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDevicePreferenceTest.java +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDevicePreferenceTest.java @@ -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); diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaServiceTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaServiceTest.java index a0e971b1d45..c82c9787ccd 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaServiceTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaServiceTest.java @@ -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(); - 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); diff --git a/tests/robotests/src/com/android/settings/network/NetworkResetPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/network/NetworkResetPreferenceControllerTest.java index 73f4b6a0b7b..e263ea7e1df 100644 --- a/tests/robotests/src/com/android/settings/network/NetworkResetPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/network/NetworkResetPreferenceControllerTest.java @@ -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(); } }