From 6b4c754f5ea63b4fdcdadf9c16285be816c01b8d Mon Sep 17 00:00:00 2001 From: Jan Tomljanovic Date: Tue, 19 Dec 2023 23:23:28 +0000 Subject: [PATCH 1/8] Add auth challenge for increasing screen timeout. We only require one auth after onStart(), and only for increasing the timeout. Test: atest SettingsRoboTests:com.android.settings.display.ScreenTimeoutSettingsTest Test: also manually tested Bug: 315937886 Change-Id: If4aed67736cd7545d3a518aadd8253ea6a9fae43 --- ...settings_display_flag_declarations.aconfig | 9 ++ .../display/ScreenTimeoutSettings.java | 150 ++++++++++++------ .../display/ScreenTimeoutSettingsTest.java | 95 +++++++++++ 3 files changed, 207 insertions(+), 47 deletions(-) create mode 100644 aconfig/settings_display_flag_declarations.aconfig diff --git a/aconfig/settings_display_flag_declarations.aconfig b/aconfig/settings_display_flag_declarations.aconfig new file mode 100644 index 00000000000..52a326da45c --- /dev/null +++ b/aconfig/settings_display_flag_declarations.aconfig @@ -0,0 +1,9 @@ +package: "com.android.settings.flags" + +flag { + name: "protect_screen_timeout_with_auth" + namespace: "safety_center" + description: "Require an auth challenge for increasing screen timeout." + bug: "315937886" +} + diff --git a/src/com/android/settings/display/ScreenTimeoutSettings.java b/src/com/android/settings/display/ScreenTimeoutSettings.java index f7be3192a76..1c99d5f70a8 100644 --- a/src/com/android/settings/display/ScreenTimeoutSettings.java +++ b/src/com/android/settings/display/ScreenTimeoutSettings.java @@ -37,10 +37,12 @@ import android.util.Log; import androidx.preference.PreferenceScreen; import com.android.settings.R; +import com.android.settings.flags.Flags; import com.android.settings.overlay.FeatureFactory; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.support.actionbar.HelpResourceProvider; import com.android.settings.widget.RadioButtonPickerFragment; +import com.android.settings.wifi.dpp.WifiDppUtils; import com.android.settingslib.RestrictedLockUtils; import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; @@ -55,13 +57,12 @@ import com.google.common.annotations.VisibleForTesting; import java.util.ArrayList; import java.util.List; -/** - * Fragment that is used to control screen timeout. - */ +/** Fragment that is used to control screen timeout. */ @SearchIndexable(forTarget = SearchIndexable.ALL & ~SearchIndexable.ARC) -public class ScreenTimeoutSettings extends RadioButtonPickerFragment implements - HelpResourceProvider { +public class ScreenTimeoutSettings extends RadioButtonPickerFragment + implements HelpResourceProvider { private static final String TAG = "ScreenTimeout"; + /** If there is no setting in the provider, use this. */ public static final int FALLBACK_SCREEN_TIMEOUT_VALUE = 30000; @@ -72,25 +73,24 @@ public class ScreenTimeoutSettings extends RadioButtonPickerFragment implements private FooterPreference mPrivacyPreference; private final MetricsFeatureProvider mMetricsFeatureProvider; private SensorPrivacyManager mPrivacyManager; - private final BroadcastReceiver mReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - mAdaptiveSleepBatterySaverPreferenceController.updateVisibility(); - mAdaptiveSleepController.updatePreference(); - } - }; + private final BroadcastReceiver mReceiver = + new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + mAdaptiveSleepBatterySaverPreferenceController.updateVisibility(); + mAdaptiveSleepController.updatePreference(); + } + }; private DevicePolicyManager mDevicePolicyManager; private SensorPrivacyManager.OnSensorPrivacyChangedListener mPrivacyChangedListener; + private boolean mIsUserAuthenticated = false; - @VisibleForTesting - Context mContext; + @VisibleForTesting Context mContext; - @VisibleForTesting - RestrictedLockUtils.EnforcedAdmin mAdmin; + @VisibleForTesting RestrictedLockUtils.EnforcedAdmin mAdmin; - @VisibleForTesting - FooterPreference mDisableOptionsPreference; + @VisibleForTesting FooterPreference mDisableOptionsPreference; @VisibleForTesting FooterPreference mPowerConsumptionPreference; @@ -101,16 +101,14 @@ public class ScreenTimeoutSettings extends RadioButtonPickerFragment implements @VisibleForTesting AdaptiveSleepCameraStatePreferenceController mAdaptiveSleepCameraStatePreferenceController; - @VisibleForTesting - AdaptiveSleepPreferenceController mAdaptiveSleepController; + @VisibleForTesting AdaptiveSleepPreferenceController mAdaptiveSleepController; @VisibleForTesting AdaptiveSleepBatterySaverPreferenceController mAdaptiveSleepBatterySaverPreferenceController; public ScreenTimeoutSettings() { super(); - mMetricsFeatureProvider = FeatureFactory.getFeatureFactory() - .getMetricsFeatureProvider(); + mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider(); } @Override @@ -121,8 +119,8 @@ public class ScreenTimeoutSettings extends RadioButtonPickerFragment implements mInitialEntries = getResources().getStringArray(R.array.screen_timeout_entries); mInitialValues = getResources().getStringArray(R.array.screen_timeout_values); mAdaptiveSleepController = new AdaptiveSleepPreferenceController(context); - mAdaptiveSleepPermissionController = new AdaptiveSleepPermissionPreferenceController( - context); + mAdaptiveSleepPermissionController = + new AdaptiveSleepPermissionPreferenceController(context); mAdaptiveSleepCameraStatePreferenceController = new AdaptiveSleepCameraStatePreferenceController(context, getLifecycle()); mAdaptiveSleepBatterySaverPreferenceController = @@ -144,8 +142,9 @@ public class ScreenTimeoutSettings extends RadioButtonPickerFragment implements if (mInitialValues != null) { for (int i = 0; i < mInitialValues.length; ++i) { if (Long.parseLong(mInitialValues[i].toString()) <= maxTimeout) { - candidates.add(new TimeoutCandidateInfo(mInitialEntries[i], - mInitialValues[i].toString(), true)); + candidates.add( + new TimeoutCandidateInfo( + mInitialEntries[i], mInitialValues[i].toString(), true)); } } } else { @@ -161,9 +160,10 @@ public class ScreenTimeoutSettings extends RadioButtonPickerFragment implements mAdaptiveSleepCameraStatePreferenceController.updateVisibility(); mAdaptiveSleepBatterySaverPreferenceController.updateVisibility(); mAdaptiveSleepController.updatePreference(); - mContext.registerReceiver(mReceiver, - new IntentFilter(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED)); + mContext.registerReceiver( + mReceiver, new IntentFilter(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED)); mPrivacyManager.addSensorPrivacyListener(CAMERA, mPrivacyChangedListener); + mIsUserAuthenticated = false; } @Override @@ -185,19 +185,21 @@ public class ScreenTimeoutSettings extends RadioButtonPickerFragment implements } for (CandidateInfo info : candidateList) { - SelectorWithWidgetPreference pref = - new SelectorWithWidgetPreference(getPrefContext()); + ProtectedSelectorWithWidgetPreference pref = + new ProtectedSelectorWithWidgetPreference( + getPrefContext(), info.getKey(), this); bindPreference(pref, info.getKey(), info, defaultKey); screen.addPreference(pref); } - final long selectedTimeout = Long.parseLong(defaultKey); + final long selectedTimeout = getTimeoutFromKey(defaultKey); final long maxTimeout = getMaxScreenTimeout(getContext()); if (!candidateList.isEmpty() && (selectedTimeout > maxTimeout)) { // The selected time out value is longer than the max timeout allowed by the admin. // Select the largest value from the list by default. - final SelectorWithWidgetPreference preferenceWithLargestTimeout = - (SelectorWithWidgetPreference) screen.getPreference(candidateList.size() - 1); + final ProtectedSelectorWithWidgetPreference preferenceWithLargestTimeout = + (ProtectedSelectorWithWidgetPreference) + screen.getPreference(candidateList.size() - 1); preferenceWithLargestTimeout.setChecked(true); } @@ -225,20 +227,34 @@ public class ScreenTimeoutSettings extends RadioButtonPickerFragment implements } } + boolean isUserAuthenticated() { + return mIsUserAuthenticated; + } + + void setUserAuthenticated(boolean isUserAuthenticated) { + mIsUserAuthenticated = isUserAuthenticated; + } + @VisibleForTesting void setupDisabledFooterPreference() { - final String textDisabledByAdmin = mDevicePolicyManager.getResources().getString( - OTHER_OPTIONS_DISABLED_BY_ADMIN, () -> getResources().getString( - R.string.admin_disabled_other_options)); + final String textDisabledByAdmin = + mDevicePolicyManager + .getResources() + .getString( + OTHER_OPTIONS_DISABLED_BY_ADMIN, + () -> + getResources() + .getString(R.string.admin_disabled_other_options)); final String textMoreDetails = getResources().getString(R.string.admin_more_details); mDisableOptionsPreference = new FooterPreference(getContext()); mDisableOptionsPreference.setTitle(textDisabledByAdmin); mDisableOptionsPreference.setSelectable(false); mDisableOptionsPreference.setLearnMoreText(textMoreDetails); - mDisableOptionsPreference.setLearnMoreAction(v -> { - RestrictedLockUtils.sendShowAdminSupportDetailsIntent(getContext(), mAdmin); - }); + mDisableOptionsPreference.setLearnMoreAction( + v -> { + RestrictedLockUtils.sendShowAdminSupportDetailsIntent(getContext(), mAdmin); + }); mDisableOptionsPreference.setIcon(R.drawable.ic_info_outline_24dp); // The 'disabled by admin' preference should always be at the end of the setting page. @@ -303,17 +319,20 @@ public class ScreenTimeoutSettings extends RadioButtonPickerFragment implements if (context == null) { return Long.toString(FALLBACK_SCREEN_TIMEOUT_VALUE); } else { - return Long.toString(Settings.System.getLong(context.getContentResolver(), - SCREEN_OFF_TIMEOUT, FALLBACK_SCREEN_TIMEOUT_VALUE)); + return Long.toString( + Settings.System.getLong( + context.getContentResolver(), + SCREEN_OFF_TIMEOUT, + FALLBACK_SCREEN_TIMEOUT_VALUE)); } } private void setCurrentSystemScreenTimeout(Context context, String key) { try { if (context != null) { - final long value = Long.parseLong(key); - mMetricsFeatureProvider.action(context, SettingsEnums.ACTION_SCREEN_TIMEOUT_CHANGED, - (int) value); + final long value = getTimeoutFromKey(key); + mMetricsFeatureProvider.action( + context, SettingsEnums.ACTION_SCREEN_TIMEOUT_CHANGED, (int) value); Settings.System.putLong(context.getContentResolver(), SCREEN_OFF_TIMEOUT, value); } } catch (NumberFormatException e) { @@ -325,7 +344,12 @@ public class ScreenTimeoutSettings extends RadioButtonPickerFragment implements return AdaptiveSleepPreferenceController.isAdaptiveSleepSupported(context); } - private static class TimeoutCandidateInfo extends CandidateInfo { + private static long getTimeoutFromKey(String key) { + return Long.parseLong(key); + } + + @VisibleForTesting + static class TimeoutCandidateInfo extends CandidateInfo { private final CharSequence mLabel; private final String mKey; @@ -351,10 +375,42 @@ public class ScreenTimeoutSettings extends RadioButtonPickerFragment implements } } + @VisibleForTesting + static class ProtectedSelectorWithWidgetPreference + extends SelectorWithWidgetPreference { + + private final long mTimeoutMs; + private final ScreenTimeoutSettings mScreenTimeoutSettings; + + ProtectedSelectorWithWidgetPreference( + Context context, String key, ScreenTimeoutSettings screenTimeoutSettings) { + super(context); + mTimeoutMs = getTimeoutFromKey(key); + mScreenTimeoutSettings = screenTimeoutSettings; + } + + @Override + public void onClick() { + if (Flags.protectScreenTimeoutWithAuth() + && !mScreenTimeoutSettings.isUserAuthenticated() + && !isChecked() + && mTimeoutMs > getTimeoutFromKey(mScreenTimeoutSettings.getDefaultKey())) { + WifiDppUtils.showLockScreen( + getContext(), + () -> { + mScreenTimeoutSettings.setUserAuthenticated(true); + super.onClick(); + }); + } else { + super.onClick(); + } + } + } + public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = new BaseSearchIndexProvider(R.xml.screen_timeout_settings) { - public List getRawDataToIndex(Context context, - boolean enabled) { + public List getRawDataToIndex( + Context context, boolean enabled) { if (!isScreenAttentionAvailable(context)) { return null; } diff --git a/tests/robotests/src/com/android/settings/display/ScreenTimeoutSettingsTest.java b/tests/robotests/src/com/android/settings/display/ScreenTimeoutSettingsTest.java index 9e193ffc97e..1a6a1128068 100644 --- a/tests/robotests/src/com/android/settings/display/ScreenTimeoutSettingsTest.java +++ b/tests/robotests/src/com/android/settings/display/ScreenTimeoutSettingsTest.java @@ -33,6 +33,7 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.app.KeyguardManager; import android.app.admin.DevicePolicyManager; import android.content.ContentResolver; import android.content.Context; @@ -41,31 +42,44 @@ import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.content.res.Resources; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.provider.SearchIndexableResource; import android.provider.Settings; import androidx.preference.PreferenceScreen; import com.android.settings.R; +import com.android.settings.flags.Flags; import com.android.settings.testutils.FakeFeatureFactory; import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.widget.CandidateInfo; import com.android.settingslib.widget.FooterPreference; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; +import org.robolectric.Shadows; import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowKeyguardManager; import java.util.List; @RunWith(RobolectricTestRunner.class) @Config(shadows = { com.android.settings.testutils.shadow.ShadowFragment.class, + ShadowKeyguardManager.class }) public class ScreenTimeoutSettingsTest { + + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + private static final String[] TIMEOUT_ENTRIES = new String[]{"15 secs", "30 secs"}; private static final String[] TIMEOUT_VALUES = new String[]{"15000", "30000"}; @@ -218,4 +232,85 @@ public class ScreenTimeoutSettingsTest { assertThat(Long.toString(timeout)).isEqualTo(TIMEOUT_VALUES[0]); } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_PROTECT_SCREEN_TIMEOUT_WITH_AUTH) + public void onClick_whenUserAlreadyAuthenticated_buttonChecked() { + String key = "222"; + String defaultKey = "1"; + mSettings.setDefaultKey(defaultKey); + CandidateInfo info = new ScreenTimeoutSettings.TimeoutCandidateInfo("label", key, false); + ScreenTimeoutSettings.ProtectedSelectorWithWidgetPreference pref = + new ScreenTimeoutSettings.ProtectedSelectorWithWidgetPreference( + mContext, info.getKey(), mSettings); + mSettings.bindPreference(pref, info.getKey(), info, defaultKey); + mSettings.setUserAuthenticated(true); + + pref.onClick(); + + assertThat(mSettings.getDefaultKey()).isEqualTo(key); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_PROTECT_SCREEN_TIMEOUT_WITH_AUTH) + public void onClick_whenButtonAlreadyChecked_noAuthNeeded() { + String key = "222"; + mSettings.setDefaultKey(key); + CandidateInfo info = new ScreenTimeoutSettings.TimeoutCandidateInfo("label", key, false); + ScreenTimeoutSettings.ProtectedSelectorWithWidgetPreference pref = + new ScreenTimeoutSettings.ProtectedSelectorWithWidgetPreference( + mContext, info.getKey(), mSettings); + mSettings.bindPreference(pref, info.getKey(), info, key); + mSettings.setUserAuthenticated(false); + setAuthPassesAutomatically(); + + pref.onClick(); + + assertThat(mSettings.isUserAuthenticated()).isFalse(); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_PROTECT_SCREEN_TIMEOUT_WITH_AUTH) + public void onClick_whenReducingTimeout_noAuthNeeded() { + String key = "1"; + String defaultKey = "222"; + mSettings.setDefaultKey(defaultKey); + CandidateInfo info = new ScreenTimeoutSettings.TimeoutCandidateInfo("label", key, false); + ScreenTimeoutSettings.ProtectedSelectorWithWidgetPreference pref = + new ScreenTimeoutSettings.ProtectedSelectorWithWidgetPreference( + mContext, info.getKey(), mSettings); + mSettings.bindPreference(pref, info.getKey(), info, defaultKey); + mSettings.setUserAuthenticated(false); + setAuthPassesAutomatically(); + + pref.onClick(); + + assertThat(mSettings.isUserAuthenticated()).isFalse(); + assertThat(mSettings.getDefaultKey()).isEqualTo(key); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_PROTECT_SCREEN_TIMEOUT_WITH_AUTH) + public void onClick_whenIncreasingTimeout_authNeeded() { + String key = "222"; + String defaultKey = "1"; + mSettings.setDefaultKey(defaultKey); + CandidateInfo info = new ScreenTimeoutSettings.TimeoutCandidateInfo("label", key, false); + ScreenTimeoutSettings.ProtectedSelectorWithWidgetPreference pref = + new ScreenTimeoutSettings.ProtectedSelectorWithWidgetPreference( + mContext, info.getKey(), mSettings); + mSettings.bindPreference(pref, info.getKey(), info, defaultKey); + mSettings.setUserAuthenticated(false); + setAuthPassesAutomatically(); + + pref.onClick(); + + assertThat(mSettings.getDefaultKey()).isEqualTo(key); + assertThat(mSettings.isUserAuthenticated()).isTrue(); + } + + private void setAuthPassesAutomatically() { + Shadows.shadowOf(mContext.getSystemService(KeyguardManager.class)) + .setIsKeyguardSecure(false); + } } From da00c1e86fd3e01683ba9ca602bdcf6c91dc6102 Mon Sep 17 00:00:00 2001 From: Oli Thompson Date: Thu, 28 Dec 2023 17:15:53 +0000 Subject: [PATCH 2/8] Fix incorrect test runner Bug: 317966701 Test: atest WorkModePreferenceControllerTest Change-Id: Idf2d66940bebff622c8f65c41ed846d9c24d0ba6 --- .../settings/accounts/WorkModePreferenceControllerTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/robotests/src/com/android/settings/accounts/WorkModePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/accounts/WorkModePreferenceControllerTest.java index 8b4ebcaa145..91b240ea9e5 100644 --- a/tests/robotests/src/com/android/settings/accounts/WorkModePreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/accounts/WorkModePreferenceControllerTest.java @@ -41,13 +41,12 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.ParameterizedRobolectricTestRunner; -import org.robolectric.RobolectricTestRunner; import java.util.Arrays; import java.util.Collections; import java.util.List; -@RunWith(RobolectricTestRunner.class) +@RunWith(ParameterizedRobolectricTestRunner.class) public class WorkModePreferenceControllerTest { private static final String PREF_KEY = "work_mode"; From 315f2546fc170036fe9bd85f357bafe6c5642d7e Mon Sep 17 00:00:00 2001 From: Vaibhav Devmurari Date: Wed, 27 Dec 2023 18:34:25 +0000 Subject: [PATCH 3/8] Add A11Y settings for Bounce keys and Sticky keys Test: manual Bug: 294546335 Change-Id: I943300c31d0c8e142f64f084f4c65e05249a72ec --- res/values/strings.xml | 10 ++ res/xml/physical_keyboard_settings.xml | 18 +++ .../inputmethod/PhysicalKeyboardFragment.java | 127 +++++++++++++++--- 3 files changed, 139 insertions(+), 16 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index 88bd4d730bc..22a1cbce4ab 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -4254,12 +4254,22 @@ Manage on-screen keyboards Options + + Accessibility Physical keyboard Use on-screen keyboard Keep it on screen while physical keyboard is active + + Bounce keys + + Enable Bounce keys for physical keyboard accessibility + + Sticky keys + + Enable Sticky keys for physical keyboard accessibility Keyboard shortcuts diff --git a/res/xml/physical_keyboard_settings.xml b/res/xml/physical_keyboard_settings.xml index d8e66bb9066..dc424d19875 100644 --- a/res/xml/physical_keyboard_settings.xml +++ b/res/xml/physical_keyboard_settings.xml @@ -38,4 +38,22 @@ android:summary="@string/modifier_keys_settings_summary" android:fragment="com.android.settings.inputmethod.ModifierKeysSettings" /> + + + + + + + + diff --git a/src/com/android/settings/inputmethod/PhysicalKeyboardFragment.java b/src/com/android/settings/inputmethod/PhysicalKeyboardFragment.java index 0e95840a1ca..38de93ea2d2 100644 --- a/src/com/android/settings/inputmethod/PhysicalKeyboardFragment.java +++ b/src/com/android/settings/inputmethod/PhysicalKeyboardFragment.java @@ -24,7 +24,9 @@ import android.content.Intent; import android.database.ContentObserver; import android.hardware.input.InputDeviceIdentifier; import android.hardware.input.InputManager; +import android.hardware.input.InputSettings; import android.hardware.input.KeyboardLayout; +import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.UserHandle; @@ -65,10 +67,19 @@ public final class PhysicalKeyboardFragment extends SettingsPreferenceFragment KeyboardLayoutDialogFragment.OnSetupKeyboardLayoutsListener { private static final String KEYBOARD_OPTIONS_CATEGORY = "keyboard_options_category"; + private static final String KEYBOARD_A11Y_CATEGORY = "keyboard_a11y_category"; private static final String SHOW_VIRTUAL_KEYBOARD_SWITCH = "show_virtual_keyboard_switch"; + private static final String ACCESSIBILITY_BOUNCE_KEYS = "accessibility_bounce_keys"; + private static final String ACCESSIBILITY_STICKY_KEYS = "accessibility_sticky_keys"; private static final String KEYBOARD_SHORTCUTS_HELPER = "keyboard_shortcuts_helper"; private static final String MODIFIER_KEYS_SETTINGS = "modifier_keys_settings"; private static final String EXTRA_AUTO_SELECTION = "auto_selection"; + private static final Uri sVirtualKeyboardSettingsUri = Secure.getUriFor( + Secure.SHOW_IME_WITH_HARD_KEYBOARD); + private static final Uri sAccessibilityBounceKeysUri = Secure.getUriFor( + Secure.ACCESSIBILITY_BOUNCE_KEYS); + private static final Uri sAccessibilityStickyKeysUri = Secure.getUriFor( + Secure.ACCESSIBILITY_STICKY_KEYS); @NonNull private final ArrayList mLastHardKeyboards = new ArrayList<>(); @@ -80,7 +91,14 @@ public final class PhysicalKeyboardFragment extends SettingsPreferenceFragment @NonNull private PreferenceCategory mKeyboardAssistanceCategory; @Nullable + private PreferenceCategory mKeyboardA11yCategory = null; + @Nullable private TwoStatePreference mShowVirtualKeyboardSwitch = null; + @Nullable + private TwoStatePreference mAccessibilityBounceKeys = null; + @Nullable + private TwoStatePreference mAccessibilityStickyKeys = null; + private Intent mIntentWaitingForResult; private boolean mIsNewKeyboardSettings; @@ -102,10 +120,15 @@ public final class PhysicalKeyboardFragment extends SettingsPreferenceFragment mIm = Preconditions.checkNotNull(activity.getSystemService(InputManager.class)); mImm = Preconditions.checkNotNull(activity.getSystemService(InputMethodManager.class)); mKeyboardAssistanceCategory = Preconditions.checkNotNull( - (PreferenceCategory) findPreference(KEYBOARD_OPTIONS_CATEGORY)); - mShowVirtualKeyboardSwitch = Preconditions.checkNotNull( - (TwoStatePreference) mKeyboardAssistanceCategory.findPreference( - SHOW_VIRTUAL_KEYBOARD_SWITCH)); + findPreference(KEYBOARD_OPTIONS_CATEGORY)); + mShowVirtualKeyboardSwitch = Objects.requireNonNull( + mKeyboardAssistanceCategory.findPreference(SHOW_VIRTUAL_KEYBOARD_SWITCH)); + + mKeyboardA11yCategory = Objects.requireNonNull(findPreference(KEYBOARD_A11Y_CATEGORY)); + mAccessibilityBounceKeys = Objects.requireNonNull( + mKeyboardA11yCategory.findPreference(ACCESSIBILITY_BOUNCE_KEYS)); + mAccessibilityStickyKeys = Objects.requireNonNull( + mKeyboardA11yCategory.findPreference(ACCESSIBILITY_STICKY_KEYS)); FeatureFactory featureFactory = FeatureFactory.getFeatureFactory(); mMetricsFeatureProvider = featureFactory.getMetricsFeatureProvider(); @@ -121,6 +144,12 @@ public final class PhysicalKeyboardFragment extends SettingsPreferenceFragment if (!isModifierKeySettingsEnabled) { mKeyboardAssistanceCategory.removePreference(findPreference(MODIFIER_KEYS_SETTINGS)); } + if (!InputSettings.isAccessibilityBounceKeysFeatureEnabled()) { + mKeyboardA11yCategory.removePreference(mAccessibilityBounceKeys); + } + if (!InputSettings.isAccessibilityStickyKeysFeatureEnabled()) { + mKeyboardA11yCategory.removePreference(mAccessibilityStickyKeys); + } InputDeviceIdentifier inputDeviceIdentifier = activity.getIntent().getParcelableExtra( KeyboardLayoutPickerFragment.EXTRA_INPUT_DEVICE_IDENTIFIER); int intentFromWhere = @@ -161,9 +190,13 @@ public final class PhysicalKeyboardFragment extends SettingsPreferenceFragment mLastHardKeyboards.clear(); scheduleUpdateHardKeyboards(); mIm.registerInputDeviceListener(this, null); - mShowVirtualKeyboardSwitch.setOnPreferenceChangeListener( + Objects.requireNonNull(mShowVirtualKeyboardSwitch).setOnPreferenceChangeListener( mShowVirtualKeyboardSwitchPreferenceChangeListener); - registerShowVirtualKeyboardSettingsObserver(); + Objects.requireNonNull(mAccessibilityBounceKeys).setOnPreferenceChangeListener( + mAccessibilityBounceKeysSwitchPreferenceChangeListener); + Objects.requireNonNull(mAccessibilityStickyKeys).setOnPreferenceChangeListener( + mAccessibilityStickyKeysSwitchPreferenceChangeListener); + registerSettingsObserver(); } @Override @@ -171,8 +204,10 @@ public final class PhysicalKeyboardFragment extends SettingsPreferenceFragment super.onPause(); mLastHardKeyboards.clear(); mIm.unregisterInputDeviceListener(this); - mShowVirtualKeyboardSwitch.setOnPreferenceChangeListener(null); - unregisterShowVirtualKeyboardSettingsObserver(); + Objects.requireNonNull(mShowVirtualKeyboardSwitch).setOnPreferenceChangeListener(null); + Objects.requireNonNull(mAccessibilityBounceKeys).setOnPreferenceChangeListener(null); + Objects.requireNonNull(mAccessibilityStickyKeys).setOnPreferenceChangeListener(null); + unregisterSettingsObserver(); } @Override @@ -276,6 +311,14 @@ public final class PhysicalKeyboardFragment extends SettingsPreferenceFragment mFeatureProvider.addFirmwareUpdateCategory(getPrefContext(), preferenceScreen); } updateShowVirtualKeyboardSwitch(); + + if (InputSettings.isAccessibilityBounceKeysFeatureEnabled() + || InputSettings.isAccessibilityStickyKeysFeatureEnabled()) { + Objects.requireNonNull(mKeyboardA11yCategory).setOrder(2); + preferenceScreen.addPreference(mKeyboardA11yCategory); + updateAccessibilityBounceKeysSwitch(); + updateAccessibilityStickyKeysSwitch(); + } } private void showKeyboardLayoutDialog(InputDeviceIdentifier inputDeviceIdentifier) { @@ -296,25 +339,58 @@ public final class PhysicalKeyboardFragment extends SettingsPreferenceFragment .launch(); } - private void registerShowVirtualKeyboardSettingsObserver() { - unregisterShowVirtualKeyboardSettingsObserver(); - getActivity().getContentResolver().registerContentObserver( - Secure.getUriFor(Secure.SHOW_IME_WITH_HARD_KEYBOARD), + private void registerSettingsObserver() { + unregisterSettingsObserver(); + ContentResolver contentResolver = getActivity().getContentResolver(); + contentResolver.registerContentObserver( + sVirtualKeyboardSettingsUri, false, mContentObserver, UserHandle.myUserId()); + if (InputSettings.isAccessibilityBounceKeysFeatureEnabled()) { + contentResolver.registerContentObserver( + sAccessibilityBounceKeysUri, + false, + mContentObserver, + UserHandle.myUserId()); + } + if (InputSettings.isAccessibilityStickyKeysFeatureEnabled()) { + contentResolver.registerContentObserver( + sAccessibilityStickyKeysUri, + false, + mContentObserver, + UserHandle.myUserId()); + } updateShowVirtualKeyboardSwitch(); + updateAccessibilityBounceKeysSwitch(); + updateAccessibilityStickyKeysSwitch(); } - private void unregisterShowVirtualKeyboardSettingsObserver() { + private void unregisterSettingsObserver() { getActivity().getContentResolver().unregisterContentObserver(mContentObserver); } private void updateShowVirtualKeyboardSwitch() { - mShowVirtualKeyboardSwitch.setChecked( + Objects.requireNonNull(mShowVirtualKeyboardSwitch).setChecked( Secure.getInt(getContentResolver(), Secure.SHOW_IME_WITH_HARD_KEYBOARD, 0) != 0); } + private void updateAccessibilityBounceKeysSwitch() { + if (!InputSettings.isAccessibilityBounceKeysFeatureEnabled()) { + return; + } + Objects.requireNonNull(mAccessibilityBounceKeys).setChecked( + InputSettings.isAccessibilityBounceKeysEnabled(getContext())); + } + + private void updateAccessibilityStickyKeysSwitch() { + if (!InputSettings.isAccessibilityStickyKeysFeatureEnabled()) { + return; + } + Objects.requireNonNull(mAccessibilityStickyKeys).setChecked( + InputSettings.isAccessibilityStickyKeysEnabled(getContext())); + } + private void toggleKeyboardShortcutsMenu() { getActivity().requestShowKeyboardShortcuts(); } @@ -328,10 +404,29 @@ public final class PhysicalKeyboardFragment extends SettingsPreferenceFragment return true; }; + private final OnPreferenceChangeListener + mAccessibilityBounceKeysSwitchPreferenceChangeListener = (preference, newValue) -> { + InputSettings.setAccessibilityBounceKeysThreshold(getContext(), + ((Boolean) newValue) ? 500 : 0); + return true; + }; + + private final OnPreferenceChangeListener + mAccessibilityStickyKeysSwitchPreferenceChangeListener = (preference, newValue) -> { + InputSettings.setAccessibilityStickyKeysEnabled(getContext(), (Boolean) newValue); + return true; + }; + private final ContentObserver mContentObserver = new ContentObserver(new Handler(true)) { @Override - public void onChange(boolean selfChange) { - updateShowVirtualKeyboardSwitch(); + public void onChange(boolean selfChange, Uri uri) { + if (sVirtualKeyboardSettingsUri.equals(uri)) { + updateShowVirtualKeyboardSwitch(); + } else if (sAccessibilityBounceKeysUri.equals(uri)) { + updateAccessibilityBounceKeysSwitch(); + } else if (sAccessibilityStickyKeysUri.equals(uri)) { + updateAccessibilityStickyKeysSwitch(); + } } }; From 355144675a41ed8e531c02d8c275c91ce7f18efa Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Fri, 29 Dec 2023 13:26:09 +0800 Subject: [PATCH 4/8] Fix ANR in WifiCallingPreferenceController.getAvailabilityStatus Move the following to background thread to avoid block main thread, - MobileNetworkUtils.isWifiCallingEnabled(mContext, mSubId, null) - MobileNetworkUtils.buildPhoneAccountConfigureIntent() - getSummaryForWfcMode() - Call State Since WifiCallingPreferenceController no longer calculate availability in getAvailabilityStatus(), also update the CallingPreferenceCategoryController accordingly. Also introduce ImsMmTelRepository for split business logic for easy testing. Fix: 292401934 Test: manual - on Mobile Settings Test: unit test Change-Id: If92e2c8f6e137e40b83e578294c03c1b917eef8e --- .../CallingPreferenceCategoryController.java | 31 --- .../CallingPreferenceCategoryController.kt | 48 ++++ .../telephony/MobileNetworkSettings.java | 11 +- .../VideoCallingPreferenceController.java | 10 +- .../WifiCallingPreferenceController.java | 230 ------------------ .../WifiCallingPreferenceController.kt | 140 +++++++++++ .../telephony/ims/ImsMmTelRepository.kt | 57 +++++ .../VideoCallingPreferenceControllerTest.java | 3 +- ...CallingPreferenceCategoryControllerTest.kt | 80 ++++++ .../WifiCallingPreferenceControllerTest.kt | 140 +++++++++++ .../telephony/ims/ImsMmTelRepositoryTest.kt | 115 +++++++++ .../WifiCallingPreferenceControllerTest.java | 227 ----------------- 12 files changed, 597 insertions(+), 495 deletions(-) delete mode 100644 src/com/android/settings/network/telephony/CallingPreferenceCategoryController.java create mode 100644 src/com/android/settings/network/telephony/CallingPreferenceCategoryController.kt delete mode 100644 src/com/android/settings/network/telephony/WifiCallingPreferenceController.java create mode 100644 src/com/android/settings/network/telephony/WifiCallingPreferenceController.kt create mode 100644 src/com/android/settings/network/telephony/ims/ImsMmTelRepository.kt create mode 100644 tests/spa_unit/src/com/android/settings/network/telephony/CallingPreferenceCategoryControllerTest.kt create mode 100644 tests/spa_unit/src/com/android/settings/network/telephony/WifiCallingPreferenceControllerTest.kt create mode 100644 tests/spa_unit/src/com/android/settings/network/telephony/ims/ImsMmTelRepositoryTest.kt delete mode 100644 tests/unit/src/com/android/settings/network/telephony/WifiCallingPreferenceControllerTest.java diff --git a/src/com/android/settings/network/telephony/CallingPreferenceCategoryController.java b/src/com/android/settings/network/telephony/CallingPreferenceCategoryController.java deleted file mode 100644 index f8364158d58..00000000000 --- a/src/com/android/settings/network/telephony/CallingPreferenceCategoryController.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settings.network.telephony; - -import android.content.Context; - -import com.android.settings.widget.PreferenceCategoryController; - -/** - * Preference controller for "Calling" category - */ -public class CallingPreferenceCategoryController extends PreferenceCategoryController { - - public CallingPreferenceCategoryController(Context context, String key) { - super(context, key); - } -} diff --git a/src/com/android/settings/network/telephony/CallingPreferenceCategoryController.kt b/src/com/android/settings/network/telephony/CallingPreferenceCategoryController.kt new file mode 100644 index 00000000000..5356a417235 --- /dev/null +++ b/src/com/android/settings/network/telephony/CallingPreferenceCategoryController.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.network.telephony + +import android.content.Context +import androidx.preference.Preference +import androidx.preference.PreferenceScreen +import com.android.settings.core.BasePreferenceController + +/** + * Preference controller for "Calling" category + */ +class CallingPreferenceCategoryController(context: Context, key: String) : + BasePreferenceController(context, key) { + + private val visibleChildren = mutableSetOf() + private var preference: Preference? = null + + override fun getAvailabilityStatus() = AVAILABLE + + override fun displayPreference(screen: PreferenceScreen) { + // Not call super here, to avoid preference.isVisible changed unexpectedly + preference = screen.findPreference(preferenceKey) + } + + fun updateChildVisible(key: String, isVisible: Boolean) { + if (isVisible) { + visibleChildren.add(key) + } else { + visibleChildren.remove(key) + } + preference?.isVisible = visibleChildren.isNotEmpty() + } +} diff --git a/src/com/android/settings/network/telephony/MobileNetworkSettings.java b/src/com/android/settings/network/telephony/MobileNetworkSettings.java index 0812ccc83cd..16b04aa356d 100644 --- a/src/com/android/settings/network/telephony/MobileNetworkSettings.java +++ b/src/com/android/settings/network/telephony/MobileNetworkSettings.java @@ -269,8 +269,10 @@ public class MobileNetworkSettings extends AbstractMobileNetworkSettings impleme use(Enable2gPreferenceController.class).init(mSubId); use(CarrierWifiTogglePreferenceController.class).init(getLifecycle(), mSubId); - final WifiCallingPreferenceController wifiCallingPreferenceController = - use(WifiCallingPreferenceController.class).init(mSubId); + final CallingPreferenceCategoryController callingPreferenceCategoryController = + use(CallingPreferenceCategoryController.class); + use(WifiCallingPreferenceController.class) + .init(mSubId, callingPreferenceCategoryController); final OpenNetworkSelectPagePreferenceController openNetworkSelectPagePreferenceController = use(OpenNetworkSelectPagePreferenceController.class).init(mSubId); @@ -286,9 +288,8 @@ public class MobileNetworkSettings extends AbstractMobileNetworkSettings impleme mCdmaSubscriptionPreferenceController.init(getPreferenceManager(), mSubId); final VideoCallingPreferenceController videoCallingPreferenceController = - use(VideoCallingPreferenceController.class).init(mSubId); - use(CallingPreferenceCategoryController.class).setChildren( - Arrays.asList(wifiCallingPreferenceController, videoCallingPreferenceController)); + use(VideoCallingPreferenceController.class) + .init(mSubId, callingPreferenceCategoryController); use(Enhanced4gLtePreferenceController.class).init(mSubId) .addListener(videoCallingPreferenceController); use(Enhanced4gCallingPreferenceController.class).init(mSubId) diff --git a/src/com/android/settings/network/telephony/VideoCallingPreferenceController.java b/src/com/android/settings/network/telephony/VideoCallingPreferenceController.java index 1519bf01940..5810510a8b1 100644 --- a/src/com/android/settings/network/telephony/VideoCallingPreferenceController.java +++ b/src/com/android/settings/network/telephony/VideoCallingPreferenceController.java @@ -54,6 +54,7 @@ public class VideoCallingPreferenceController extends TelephonyTogglePreferenceC @VisibleForTesting Integer mCallState; private MobileDataEnabledListener mDataContentObserver; + private CallingPreferenceCategoryController mCallingPreferenceCategoryController; public VideoCallingPreferenceController(Context context, String key) { super(context, key); @@ -97,6 +98,8 @@ public class VideoCallingPreferenceController extends TelephonyTogglePreferenceC final TwoStatePreference switchPreference = (TwoStatePreference) preference; final boolean videoCallEnabled = isVideoCallEnabled(mSubId); switchPreference.setVisible(videoCallEnabled); + mCallingPreferenceCategoryController + .updateChildVisible(getPreferenceKey(), videoCallEnabled); if (videoCallEnabled) { final boolean videoCallEditable = queryVoLteState(mSubId).isEnabledByUser() && queryImsState(mSubId).isAllowUserControl(); @@ -136,8 +139,13 @@ public class VideoCallingPreferenceController extends TelephonyTogglePreferenceC PackageManager.FEATURE_TELEPHONY_IMS); } - public VideoCallingPreferenceController init(int subId) { + /** + * Init instance of VideoCallingPreferenceController. + */ + public VideoCallingPreferenceController init( + int subId, CallingPreferenceCategoryController callingPreferenceCategoryController) { mSubId = subId; + mCallingPreferenceCategoryController = callingPreferenceCategoryController; return this; } diff --git a/src/com/android/settings/network/telephony/WifiCallingPreferenceController.java b/src/com/android/settings/network/telephony/WifiCallingPreferenceController.java deleted file mode 100644 index 5503e95854d..00000000000 --- a/src/com/android/settings/network/telephony/WifiCallingPreferenceController.java +++ /dev/null @@ -1,230 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settings.network.telephony; - -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.os.PersistableBundle; -import android.provider.Settings; -import android.telecom.PhoneAccountHandle; -import android.telecom.TelecomManager; -import android.telephony.CarrierConfigManager; -import android.telephony.SubscriptionManager; -import android.telephony.TelephonyCallback; -import android.telephony.TelephonyManager; -import android.telephony.ims.ImsMmTelManager; -import android.util.Log; - -import androidx.annotation.VisibleForTesting; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; - -import com.android.settings.R; -import com.android.settings.network.ims.WifiCallingQueryImsState; -import com.android.settingslib.core.lifecycle.LifecycleObserver; -import com.android.settingslib.core.lifecycle.events.OnStart; -import com.android.settingslib.core.lifecycle.events.OnStop; - -import java.util.List; - -/** - * Preference controller for "Wifi Calling" - */ -//TODO: Remove the class once Provider Model is always enabled in the future. -public class WifiCallingPreferenceController extends TelephonyBasePreferenceController implements - LifecycleObserver, OnStart, OnStop { - - private static final String TAG = "WifiCallingPreference"; - - @VisibleForTesting - Integer mCallState; - @VisibleForTesting - CarrierConfigManager mCarrierConfigManager; - private ImsMmTelManager mImsMmTelManager; - @VisibleForTesting - PhoneAccountHandle mSimCallManager; - private PhoneTelephonyCallback mTelephonyCallback; - private Preference mPreference; - private boolean mHasException; - - public WifiCallingPreferenceController(Context context, String key) { - super(context, key); - mCarrierConfigManager = context.getSystemService(CarrierConfigManager.class); - mTelephonyCallback = new PhoneTelephonyCallback(); - } - - @Override - public int getAvailabilityStatus(int subId) { - return SubscriptionManager.isValidSubscriptionId(subId) - && MobileNetworkUtils.isWifiCallingEnabled(mContext, subId, null) - ? AVAILABLE - : UNSUPPORTED_ON_DEVICE; - } - - @Override - public void onStart() { - mTelephonyCallback.register(mContext, mSubId); - } - - @Override - public void onStop() { - mTelephonyCallback.unregister(); - } - - @Override - public void displayPreference(PreferenceScreen screen) { - super.displayPreference(screen); - mPreference = screen.findPreference(getPreferenceKey()); - final Intent intent = mPreference.getIntent(); - if (intent != null) { - intent.putExtra(Settings.EXTRA_SUB_ID, mSubId); - } - } - - @Override - public void updateState(Preference preference) { - super.updateState(preference); - if ((mCallState == null) || (preference == null)) { - Log.d(TAG, "Skip update under mCallState=" + mCallState); - return; - } - mHasException = false; - CharSequence summaryText = null; - if (mSimCallManager != null) { - final Intent intent = MobileNetworkUtils.buildPhoneAccountConfigureIntent(mContext, - mSimCallManager); - if (intent == null) { - // Do nothing in this case since preference is invisible - return; - } - final PackageManager pm = mContext.getPackageManager(); - final List resolutions = pm.queryIntentActivities(intent, 0); - preference.setTitle(resolutions.get(0).loadLabel(pm)); - preference.setIntent(intent); - } else { - final String title = SubscriptionManager.getResourcesForSubId(mContext, mSubId) - .getString(R.string.wifi_calling_settings_title); - preference.setTitle(title); - summaryText = getResourceIdForWfcMode(mSubId); - } - preference.setSummary(summaryText); - preference.setEnabled(mCallState == TelephonyManager.CALL_STATE_IDLE && !mHasException); - } - - private CharSequence getResourceIdForWfcMode(int subId) { - int resId = com.android.internal.R.string.wifi_calling_off_summary; - if (queryImsState(subId).isEnabledByUser()) { - boolean useWfcHomeModeForRoaming = false; - if (mCarrierConfigManager != null) { - final PersistableBundle carrierConfig = - mCarrierConfigManager.getConfigForSubId(subId); - if (carrierConfig != null) { - useWfcHomeModeForRoaming = carrierConfig.getBoolean( - CarrierConfigManager - .KEY_USE_WFC_HOME_NETWORK_MODE_IN_ROAMING_NETWORK_BOOL); - } - } - final boolean isRoaming = getTelephonyManager(mContext, subId) - .isNetworkRoaming(); - int wfcMode = ImsMmTelManager.WIFI_MODE_UNKNOWN; - try { - wfcMode = (isRoaming && !useWfcHomeModeForRoaming) - ? mImsMmTelManager.getVoWiFiRoamingModeSetting() : - mImsMmTelManager.getVoWiFiModeSetting(); - } catch (IllegalArgumentException e) { - mHasException = true; - Log.e(TAG, "getResourceIdForWfcMode: Exception", e); - } - - switch (wfcMode) { - case ImsMmTelManager.WIFI_MODE_WIFI_ONLY: - resId = com.android.internal.R.string.wfc_mode_wifi_only_summary; - break; - case ImsMmTelManager.WIFI_MODE_CELLULAR_PREFERRED: - resId = com.android.internal.R.string - .wfc_mode_cellular_preferred_summary; - break; - case ImsMmTelManager.WIFI_MODE_WIFI_PREFERRED: - resId = com.android.internal.R.string.wfc_mode_wifi_preferred_summary; - break; - default: - break; - } - } - return SubscriptionManager.getResourcesForSubId(mContext, subId).getText(resId); - } - - public WifiCallingPreferenceController init(int subId) { - mSubId = subId; - mImsMmTelManager = getImsMmTelManager(mSubId); - mSimCallManager = mContext.getSystemService(TelecomManager.class) - .getSimCallManagerForSubscription(mSubId); - - return this; - } - - @VisibleForTesting - WifiCallingQueryImsState queryImsState(int subId) { - return new WifiCallingQueryImsState(mContext, subId); - } - - protected ImsMmTelManager getImsMmTelManager(int subId) { - if (!SubscriptionManager.isValidSubscriptionId(subId)) { - return null; - } - return ImsMmTelManager.createForSubscriptionId(subId); - } - - @VisibleForTesting - TelephonyManager getTelephonyManager(Context context, int subId) { - final TelephonyManager telephonyMgr = context.getSystemService(TelephonyManager.class); - if (!SubscriptionManager.isValidSubscriptionId(subId)) { - return telephonyMgr; - } - final TelephonyManager subscriptionTelephonyMgr = - telephonyMgr.createForSubscriptionId(subId); - return (subscriptionTelephonyMgr == null) ? telephonyMgr : subscriptionTelephonyMgr; - } - - - private class PhoneTelephonyCallback extends TelephonyCallback implements - TelephonyCallback.CallStateListener { - - private TelephonyManager mTelephonyManager; - - @Override - public void onCallStateChanged(int state) { - mCallState = state; - updateState(mPreference); - } - - public void register(Context context, int subId) { - mTelephonyManager = getTelephonyManager(context, subId); - // assign current call state so that it helps to show correct preference state even - // before first onCallStateChanged() by initial registration. - mCallState = mTelephonyManager.getCallStateForSubscription(); - mTelephonyManager.registerTelephonyCallback(context.getMainExecutor(), this); - } - - public void unregister() { - mCallState = null; - mTelephonyManager.unregisterTelephonyCallback(this); - } - } -} diff --git a/src/com/android/settings/network/telephony/WifiCallingPreferenceController.kt b/src/com/android/settings/network/telephony/WifiCallingPreferenceController.kt new file mode 100644 index 00000000000..e7b83189f86 --- /dev/null +++ b/src/com/android/settings/network/telephony/WifiCallingPreferenceController.kt @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.network.telephony + +import android.content.Context +import android.provider.Settings +import android.telecom.TelecomManager +import android.telephony.SubscriptionManager +import android.telephony.TelephonyManager +import android.telephony.ims.ImsMmTelManager +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.preference.Preference +import androidx.preference.PreferenceScreen +import com.android.settings.R +import com.android.settings.network.telephony.ims.ImsMmTelRepository +import com.android.settings.network.telephony.ims.ImsMmTelRepositoryImpl +import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** + * Preference controller for "Wifi Calling". + * + * TODO: Remove the class once Provider Model is always enabled in the future. + */ +open class WifiCallingPreferenceController @JvmOverloads constructor( + context: Context, + key: String, + private val callStateFlowFactory: (subId: Int) -> Flow = context::callStateFlow, + private val imsMmTelRepositoryFactory: (subId: Int) -> ImsMmTelRepository = { subId -> + ImsMmTelRepositoryImpl(context, subId) + }, +) : TelephonyBasePreferenceController(context, key) { + + private lateinit var preference: Preference + private lateinit var callingPreferenceCategoryController: CallingPreferenceCategoryController + + private val resourcesForSub by lazy { + SubscriptionManager.getResourcesForSubId(mContext, mSubId) + } + + fun init( + subId: Int, + callingPreferenceCategoryController: CallingPreferenceCategoryController, + ): WifiCallingPreferenceController { + mSubId = subId + this.callingPreferenceCategoryController = callingPreferenceCategoryController + return this + } + + /** + * Note: Visibility also controlled by [onViewCreated]. + */ + override fun getAvailabilityStatus(subId: Int) = + if (SubscriptionManager.isValidSubscriptionId(subId)) AVAILABLE + else CONDITIONALLY_UNAVAILABLE + + override fun displayPreference(screen: PreferenceScreen) { + // Not call super here, to avoid preference.isVisible changed unexpectedly + preference = screen.findPreference(preferenceKey)!! + preference.intent?.putExtra(Settings.EXTRA_SUB_ID, mSubId) + } + + override fun onViewCreated(viewLifecycleOwner: LifecycleOwner) { + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + val isVisible = withContext(Dispatchers.Default) { + MobileNetworkUtils.isWifiCallingEnabled(mContext, mSubId, null) + } + preference.isVisible = isVisible + callingPreferenceCategoryController.updateChildVisible(preferenceKey, isVisible) + } + } + + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + update() + } + } + + callStateFlowFactory(mSubId).collectLatestWithLifecycle(viewLifecycleOwner) { + preference.isEnabled = (it == TelephonyManager.CALL_STATE_IDLE) + } + } + + private suspend fun update() { + val simCallManager = mContext.getSystemService(TelecomManager::class.java) + ?.getSimCallManagerForSubscription(mSubId) + if (simCallManager != null) { + val intent = withContext(Dispatchers.Default) { + MobileNetworkUtils.buildPhoneAccountConfigureIntent(mContext, simCallManager) + } ?: return // Do nothing in this case since preference is invisible + val title = withContext(Dispatchers.Default) { + mContext.packageManager.resolveActivity(intent, 0) + ?.loadLabel(mContext.packageManager) + } ?: return + preference.intent = intent + preference.title = title + preference.summary = null + } else { + preference.title = resourcesForSub.getString(R.string.wifi_calling_settings_title) + preference.summary = withContext(Dispatchers.Default) { getSummaryForWfcMode() } + } + } + + private fun getSummaryForWfcMode(): String { + val resId = when (imsMmTelRepositoryFactory(mSubId).getWiFiCallingMode()) { + ImsMmTelManager.WIFI_MODE_WIFI_ONLY -> + com.android.internal.R.string.wfc_mode_wifi_only_summary + + ImsMmTelManager.WIFI_MODE_CELLULAR_PREFERRED -> + com.android.internal.R.string.wfc_mode_cellular_preferred_summary + + ImsMmTelManager.WIFI_MODE_WIFI_PREFERRED -> + com.android.internal.R.string.wfc_mode_wifi_preferred_summary + + else -> com.android.internal.R.string.wifi_calling_off_summary + } + return resourcesForSub.getString(resId) + } +} diff --git a/src/com/android/settings/network/telephony/ims/ImsMmTelRepository.kt b/src/com/android/settings/network/telephony/ims/ImsMmTelRepository.kt new file mode 100644 index 00000000000..44f09d13e4d --- /dev/null +++ b/src/com/android/settings/network/telephony/ims/ImsMmTelRepository.kt @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.network.telephony.ims + +import android.content.Context +import android.telephony.CarrierConfigManager +import android.telephony.CarrierConfigManager.KEY_USE_WFC_HOME_NETWORK_MODE_IN_ROAMING_NETWORK_BOOL +import android.telephony.TelephonyManager +import android.telephony.ims.ImsManager +import android.telephony.ims.ImsMmTelManager +import android.telephony.ims.ImsMmTelManager.WiFiCallingMode + +interface ImsMmTelRepository { + @WiFiCallingMode + fun getWiFiCallingMode(): Int +} + +class ImsMmTelRepositoryImpl( + context: Context, + private val subId: Int, + private val imsMmTelManager: ImsMmTelManager = ImsManager(context).getImsMmTelManager(subId), +) : ImsMmTelRepository { + + private val telephonyManager = context.getSystemService(TelephonyManager::class.java)!! + .createForSubscriptionId(subId) + + private val carrierConfigManager = context.getSystemService(CarrierConfigManager::class.java)!! + + @WiFiCallingMode + override fun getWiFiCallingMode(): Int = when { + !imsMmTelManager.isVoWiFiSettingEnabled -> ImsMmTelManager.WIFI_MODE_UNKNOWN + + telephonyManager.isNetworkRoaming && !useWfcHomeModeForRoaming() -> + imsMmTelManager.getVoWiFiRoamingModeSetting() + + else -> imsMmTelManager.getVoWiFiModeSetting() + } + + private fun useWfcHomeModeForRoaming(): Boolean = + carrierConfigManager + .getConfigForSubId(subId, KEY_USE_WFC_HOME_NETWORK_MODE_IN_ROAMING_NETWORK_BOOL) + .getBoolean(KEY_USE_WFC_HOME_NETWORK_MODE_IN_ROAMING_NETWORK_BOOL) +} diff --git a/tests/robotests/src/com/android/settings/network/telephony/VideoCallingPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/network/telephony/VideoCallingPreferenceControllerTest.java index 9b44faa9295..da8958dff2e 100644 --- a/tests/robotests/src/com/android/settings/network/telephony/VideoCallingPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/network/telephony/VideoCallingPreferenceControllerTest.java @@ -86,7 +86,8 @@ public class VideoCallingPreferenceControllerTest { mPreference = new SwitchPreference(mContext); mController = spy(new VideoCallingPreferenceController(mContext, "wifi_calling")); - mController.init(SUB_ID); + mController.init( + SUB_ID, new CallingPreferenceCategoryController(mContext, "calling_category")); doReturn(mQueryImsState).when(mController).queryImsState(anyInt()); doReturn(mQueryVoLteState).when(mController).queryVoLteState(anyInt()); doReturn(true).when(mController).isImsSupported(); diff --git a/tests/spa_unit/src/com/android/settings/network/telephony/CallingPreferenceCategoryControllerTest.kt b/tests/spa_unit/src/com/android/settings/network/telephony/CallingPreferenceCategoryControllerTest.kt new file mode 100644 index 00000000000..81d17d2ea3b --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/network/telephony/CallingPreferenceCategoryControllerTest.kt @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.network.telephony + +import android.content.Context +import androidx.preference.PreferenceManager +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settings.spa.preference.ComposePreference +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class CallingPreferenceCategoryControllerTest { + + private val context: Context = ApplicationProvider.getApplicationContext() + + private val preference = ComposePreference(context).apply { key = TEST_KEY } + private val preferenceScreen = PreferenceManager(context).createPreferenceScreen(context) + + private val controller = CallingPreferenceCategoryController(context, TEST_KEY) + + @Before + fun setUp() { + preferenceScreen.addPreference(preference) + controller.displayPreference(preferenceScreen) + } + + @Test + fun updateChildVisible_singleChildVisible_categoryVisible() { + controller.updateChildVisible(CHILD_A_KEY, true) + + assertThat(preference.isVisible).isTrue() + } + + @Test + fun updateChildVisible_singleChildNotVisible_categoryNotVisible() { + controller.updateChildVisible(CHILD_A_KEY, false) + + assertThat(preference.isVisible).isFalse() + } + + @Test + fun updateChildVisible_oneChildVisible_categoryVisible() { + controller.updateChildVisible(CHILD_A_KEY, true) + controller.updateChildVisible(CHILD_B_KEY, false) + + assertThat(preference.isVisible).isTrue() + } + + @Test + fun updateChildVisible_nonChildNotVisible_categoryNotVisible() { + controller.updateChildVisible(CHILD_A_KEY, false) + controller.updateChildVisible(CHILD_B_KEY, false) + + assertThat(preference.isVisible).isFalse() + } + + private companion object { + const val TEST_KEY = "test_key" + const val CHILD_A_KEY = "a" + const val CHILD_B_KEY = "b" + } +} diff --git a/tests/spa_unit/src/com/android/settings/network/telephony/WifiCallingPreferenceControllerTest.kt b/tests/spa_unit/src/com/android/settings/network/telephony/WifiCallingPreferenceControllerTest.kt new file mode 100644 index 00000000000..fc53049ee2a --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/network/telephony/WifiCallingPreferenceControllerTest.kt @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.network.telephony + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.provider.Settings +import android.telecom.PhoneAccountHandle +import android.telecom.TelecomManager +import android.telephony.TelephonyManager +import android.telephony.ims.ImsMmTelManager +import androidx.lifecycle.testing.TestLifecycleOwner +import androidx.preference.Preference +import androidx.preference.PreferenceManager +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settings.network.telephony.ims.ImsMmTelRepository +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.spy +import org.mockito.kotlin.stub + +@RunWith(AndroidJUnit4::class) +class WifiCallingPreferenceControllerTest { + private val mockTelecomManager = mock() + + private val context: Context = spy(ApplicationProvider.getApplicationContext()) { + on { getSystemService(TelecomManager::class.java) } doReturn mockTelecomManager + } + + private val preferenceIntent = Intent() + + private val preference = Preference(context).apply { + key = TEST_KEY + intent = preferenceIntent + } + private val preferenceScreen = PreferenceManager(context).createPreferenceScreen(context) + + private var callState = TelephonyManager.CALL_STATE_IDLE + + private object FakeImsMmTelRepository : ImsMmTelRepository { + var wiFiMode = ImsMmTelManager.WIFI_MODE_UNKNOWN + override fun getWiFiCallingMode() = wiFiMode + } + + private val callingPreferenceCategoryController = + CallingPreferenceCategoryController(context, "calling_category") + + private val controller = WifiCallingPreferenceController( + context = context, + key = TEST_KEY, + callStateFlowFactory = { flowOf(callState) }, + imsMmTelRepositoryFactory = { FakeImsMmTelRepository }, + ).init(subId = SUB_ID, callingPreferenceCategoryController) + + @Before + fun setUp() { + preferenceScreen.addPreference(preference) + controller.displayPreference(preferenceScreen) + } + + @Test + fun summary_noSimCallManager_setCorrectSummary() = runBlocking { + mockTelecomManager.stub { + on { getSimCallManagerForSubscription(SUB_ID) } doReturn null + } + FakeImsMmTelRepository.wiFiMode = ImsMmTelManager.WIFI_MODE_WIFI_ONLY + + controller.onViewCreated(TestLifecycleOwner()) + delay(100) + + assertThat(preference.summary) + .isEqualTo(context.getString(com.android.internal.R.string.wfc_mode_wifi_only_summary)) + } + + @Test + fun summary_hasSimCallManager_summaryIsNull() = runBlocking { + mockTelecomManager.stub { + on { getSimCallManagerForSubscription(SUB_ID) } doReturn + PhoneAccountHandle(ComponentName("", ""), "") + } + + controller.onViewCreated(TestLifecycleOwner()) + delay(100) + + assertThat(preference.summary).isNull() + } + + @Test + fun isEnabled_callIdle_enabled() = runBlocking { + callState = TelephonyManager.CALL_STATE_IDLE + + controller.onViewCreated(TestLifecycleOwner()) + delay(100) + + assertThat(preference.isEnabled).isTrue() + } + + @Test + fun isEnabled_notCallIdle_disabled() = runBlocking { + callState = TelephonyManager.CALL_STATE_RINGING + + controller.onViewCreated(TestLifecycleOwner()) + delay(100) + + assertThat(preference.isEnabled).isFalse() + } + + @Test + fun displayPreference_setsSubscriptionIdOnIntent() = runBlocking { + assertThat(preference.intent!!.getIntExtra(Settings.EXTRA_SUB_ID, 0)).isEqualTo(SUB_ID) + } + + private companion object { + const val TEST_KEY = "test_key" + const val SUB_ID = 2 + } +} diff --git a/tests/spa_unit/src/com/android/settings/network/telephony/ims/ImsMmTelRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/network/telephony/ims/ImsMmTelRepositoryTest.kt new file mode 100644 index 00000000000..eba44eddcfa --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/network/telephony/ims/ImsMmTelRepositoryTest.kt @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.network.telephony.ims + +import android.content.Context +import android.telephony.CarrierConfigManager +import android.telephony.CarrierConfigManager.KEY_USE_WFC_HOME_NETWORK_MODE_IN_ROAMING_NETWORK_BOOL +import android.telephony.TelephonyManager +import android.telephony.ims.ImsMmTelManager +import androidx.core.os.persistableBundleOf +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.spy +import org.mockito.kotlin.stub + +@RunWith(AndroidJUnit4::class) +class ImsMmTelRepositoryTest { + private val mockTelephonyManager = mock { + on { createForSubscriptionId(SUB_ID) } doReturn mock + } + + private val mockCarrierConfigManager = mock() + + private val context: Context = spy(ApplicationProvider.getApplicationContext()) { + on { getSystemService(TelephonyManager::class.java) } doReturn mockTelephonyManager + on { getSystemService(CarrierConfigManager::class.java) } doReturn mockCarrierConfigManager + } + + private val mockImsMmTelManager = mock { + on { isVoWiFiSettingEnabled } doReturn true + on { getVoWiFiRoamingModeSetting() } doReturn ImsMmTelManager.WIFI_MODE_WIFI_PREFERRED + on { getVoWiFiModeSetting() } doReturn ImsMmTelManager.WIFI_MODE_CELLULAR_PREFERRED + } + + private val repository = ImsMmTelRepositoryImpl(context, SUB_ID, mockImsMmTelManager) + + @Test + fun getWiFiCallingMode_voWiFiSettingNotEnabled_returnUnknown() { + mockImsMmTelManager.stub { + on { isVoWiFiSettingEnabled } doReturn false + } + + val wiFiCallingMode = repository.getWiFiCallingMode() + + assertThat(wiFiCallingMode).isEqualTo(ImsMmTelManager.WIFI_MODE_UNKNOWN) + } + + @Test + fun getWiFiCallingMode_roamingAndNotUseWfcHomeModeForRoaming_returnRoamingSetting() { + mockTelephonyManager.stub { + on { isNetworkRoaming } doReturn true + } + mockUseWfcHomeModeForRoaming(false) + + val wiFiCallingMode = repository.getWiFiCallingMode() + + assertThat(wiFiCallingMode).isEqualTo(mockImsMmTelManager.getVoWiFiRoamingModeSetting()) + } + + @Test + fun getWiFiCallingMode_roamingAndUseWfcHomeModeForRoaming_returnHomeSetting() { + mockTelephonyManager.stub { + on { isNetworkRoaming } doReturn true + } + mockUseWfcHomeModeForRoaming(true) + + val wiFiCallingMode = repository.getWiFiCallingMode() + + assertThat(wiFiCallingMode).isEqualTo(mockImsMmTelManager.getVoWiFiModeSetting()) + } + + @Test + fun getWiFiCallingMode_notRoaming_returnHomeSetting() { + mockTelephonyManager.stub { + on { isNetworkRoaming } doReturn false + } + + val wiFiCallingMode = repository.getWiFiCallingMode() + + assertThat(wiFiCallingMode).isEqualTo(mockImsMmTelManager.getVoWiFiModeSetting()) + } + + private fun mockUseWfcHomeModeForRoaming(config: Boolean) { + mockCarrierConfigManager.stub { + on { + getConfigForSubId(SUB_ID, KEY_USE_WFC_HOME_NETWORK_MODE_IN_ROAMING_NETWORK_BOOL) + } doReturn persistableBundleOf( + KEY_USE_WFC_HOME_NETWORK_MODE_IN_ROAMING_NETWORK_BOOL to config, + ) + } + } + + private companion object { + const val SUB_ID = 1 + } +} diff --git a/tests/unit/src/com/android/settings/network/telephony/WifiCallingPreferenceControllerTest.java b/tests/unit/src/com/android/settings/network/telephony/WifiCallingPreferenceControllerTest.java deleted file mode 100644 index 58275169f85..00000000000 --- a/tests/unit/src/com/android/settings/network/telephony/WifiCallingPreferenceControllerTest.java +++ /dev/null @@ -1,227 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settings.network.telephony; - -import static com.google.common.truth.Truth.assertThat; - -import static org.junit.Assert.assertNull; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.when; - -import android.content.Context; -import android.content.Intent; -import android.os.PersistableBundle; -import android.provider.Settings; -import android.telecom.PhoneAccountHandle; -import android.telephony.CarrierConfigManager; -import android.telephony.SubscriptionManager; -import android.telephony.TelephonyManager; -import android.telephony.ims.ImsMmTelManager; - -import androidx.preference.Preference; -import androidx.preference.PreferenceManager; -import androidx.preference.PreferenceScreen; -import androidx.test.annotation.UiThreadTest; -import androidx.test.core.app.ApplicationProvider; -import androidx.test.ext.junit.runners.AndroidJUnit4; - -import com.android.internal.R; -import com.android.settings.core.BasePreferenceController; -import com.android.settings.network.ims.MockWifiCallingQueryImsState; -import com.android.settings.network.ims.WifiCallingQueryImsState; - -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -@RunWith(AndroidJUnit4.class) -public class WifiCallingPreferenceControllerTest { - private static final int SUB_ID = 2; - @Mock - private SubscriptionManager mSubscriptionManager; - @Mock - private CarrierConfigManager mCarrierConfigManager; - @Mock - private TelephonyManager mTelephonyManager; - @Mock - private ImsMmTelManager mImsMmTelManager; - - private PreferenceScreen mScreen; - private PreferenceManager mPreferenceManager; - - private MockWifiCallingQueryImsState mQueryImsState; - - private TestWifiCallingPreferenceController mController; - private Preference mPreference; - private Context mContext; - private PersistableBundle mCarrierConfig; - - @Before - @UiThreadTest - public void setUp() { - MockitoAnnotations.initMocks(this); - - mContext = spy(ApplicationProvider.getApplicationContext()); - when(mContext.getSystemService(SubscriptionManager.class)).thenReturn(mSubscriptionManager); - - mQueryImsState = new MockWifiCallingQueryImsState(mContext, SUB_ID); - mQueryImsState.setIsEnabledByUser(true); - mQueryImsState.setIsProvisionedOnDevice(true); - - mController = new TestWifiCallingPreferenceController(mContext, "wifi_calling"); - mController.mCarrierConfigManager = mCarrierConfigManager; - mController.init(SUB_ID); - mController.mCallState = TelephonyManager.CALL_STATE_IDLE; - mCarrierConfig = new PersistableBundle(); - when(mCarrierConfigManager.getConfigForSubId(SUB_ID)).thenReturn(mCarrierConfig); - - mPreferenceManager = new PreferenceManager(mContext); - mScreen = mPreferenceManager.createPreferenceScreen(mContext); - mPreference = new Preference(mContext); - mPreference.setKey(mController.getPreferenceKey()); - mScreen.addPreference(mPreference); - } - - @Test - @UiThreadTest - public void updateState_noSimCallManager_setCorrectSummary() { - mController.mSimCallManager = null; - mQueryImsState.setIsEnabledByUser(true); - when(mImsMmTelManager.getVoWiFiRoamingModeSetting()).thenReturn( - ImsMmTelManager.WIFI_MODE_WIFI_ONLY); - when(mImsMmTelManager.getVoWiFiModeSetting()).thenReturn( - ImsMmTelManager.WIFI_MODE_WIFI_ONLY); - - mController.updateState(mPreference); - - assertThat(mPreference.getSummary()).isEqualTo( - mContext.getString(com.android.internal.R.string.wfc_mode_wifi_only_summary)); - } - - @Test - @UiThreadTest - public void updateState_notCallIdle_disable() { - mController.mCallState = TelephonyManager.CALL_STATE_RINGING; - - mController.updateState(mPreference); - - assertThat(mPreference.isEnabled()).isFalse(); - } - - @Test - @UiThreadTest - public void updateState_invalidPhoneAccountHandle_shouldNotCrash() { - mController.mSimCallManager = new PhoneAccountHandle(null /* invalid */, ""); - - //Should not crash - mController.updateState(mPreference); - } - - @Test - @UiThreadTest - public void updateState_wfcNonRoamingByConfig() { - assertNull(mController.mSimCallManager); - mCarrierConfig.putBoolean( - CarrierConfigManager.KEY_USE_WFC_HOME_NETWORK_MODE_IN_ROAMING_NETWORK_BOOL, true); - mController.init(SUB_ID); - - when(mImsMmTelManager.getVoWiFiRoamingModeSetting()).thenReturn( - ImsMmTelManager.WIFI_MODE_WIFI_PREFERRED); - when(mImsMmTelManager.getVoWiFiModeSetting()).thenReturn( - ImsMmTelManager.WIFI_MODE_CELLULAR_PREFERRED); - mQueryImsState.setIsEnabledByUser(true); - when(mTelephonyManager.isNetworkRoaming()).thenReturn(true); - - mController.updateState(mPreference); - assertThat(mPreference.getSummary()) - .isEqualTo(mContext.getString(R.string.wfc_mode_cellular_preferred_summary)); - } - - @Test - @UiThreadTest - public void updateState_wfcRoamingByConfig() { - assertNull(mController.mSimCallManager); - // useWfcHomeModeForRoaming is false by default. In order to check wfc in roaming mode. We - // need the device roaming, and not using home mode in roaming network. - when(mImsMmTelManager.getVoWiFiRoamingModeSetting()).thenReturn( - ImsMmTelManager.WIFI_MODE_WIFI_PREFERRED); - when(mImsMmTelManager.getVoWiFiModeSetting()).thenReturn( - ImsMmTelManager.WIFI_MODE_CELLULAR_PREFERRED); - mQueryImsState.setIsEnabledByUser(true); - when(mTelephonyManager.isNetworkRoaming()).thenReturn(true); - - mController.updateState(mPreference); - assertThat(mPreference.getSummary()) - .isEqualTo(mContext.getString(R.string.wfc_mode_wifi_preferred_summary)); - } - - @Test - @UiThreadTest - public void displayPreference_notAvailable_setPreferenceInvisible() { - mController.init(SubscriptionManager.INVALID_SUBSCRIPTION_ID); - when(mSubscriptionManager.getActiveSubscriptionInfoList()).thenReturn(null); - - mController.displayPreference(mScreen); - - assertThat(mController.getPreferenceKey()).isEqualTo("wifi_calling"); - assertThat(mScreen.findPreference(mController.getPreferenceKey()).isVisible()).isFalse(); - } - - @Test - @Ignore - public void displayPreference_available_setsSubscriptionIdOnIntent() { - final Intent intent = new Intent(); - mPreference.setIntent(intent); - mController.displayPreference(mScreen); - assertThat(intent.getIntExtra(Settings.EXTRA_SUB_ID, - SubscriptionManager.INVALID_SUBSCRIPTION_ID)).isEqualTo(SUB_ID); - } - - @Test - @UiThreadTest - public void getAvailabilityStatus_noWiFiCalling_shouldReturnUnsupported() { - mController.init(SubscriptionManager.INVALID_SUBSCRIPTION_ID); - when(mSubscriptionManager.getActiveSubscriptionInfoList()).thenReturn(null); - - assertThat(mController.getAvailabilityStatus()).isEqualTo( - BasePreferenceController.UNSUPPORTED_ON_DEVICE); - } - - private class TestWifiCallingPreferenceController extends WifiCallingPreferenceController { - TestWifiCallingPreferenceController(Context context, String preferenceKey) { - super(context, preferenceKey); - } - - @Override - protected ImsMmTelManager getImsMmTelManager(int subId) { - return mImsMmTelManager; - } - - @Override - protected TelephonyManager getTelephonyManager(Context context, int subId) { - return mTelephonyManager; - } - - @Override - protected WifiCallingQueryImsState queryImsState(int subId) { - return mQueryImsState; - } - } -} From 882058de81261a6b890c450d1acf9c6f75d73e4f Mon Sep 17 00:00:00 2001 From: Pajace Chen Date: Fri, 29 Dec 2023 13:48:34 +0000 Subject: [PATCH 5/8] [Shadow] Switch the button for defend battery tips Align the button position of defender settings tips with new design Bug: 299403437 Test: Manual Test Flag: None Change-Id: Ieac9cd34ba2e14581040a87a1eb7382decbe1212 --- .../batterytip/tips/BatteryDefenderTip.java | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/com/android/settings/fuelgauge/batterytip/tips/BatteryDefenderTip.java b/src/com/android/settings/fuelgauge/batterytip/tips/BatteryDefenderTip.java index b5ec522b707..047bf135741 100644 --- a/src/com/android/settings/fuelgauge/batterytip/tips/BatteryDefenderTip.java +++ b/src/com/android/settings/fuelgauge/batterytip/tips/BatteryDefenderTip.java @@ -83,17 +83,8 @@ public class BatteryDefenderTip extends BatteryTip { } cardPreference.setSelectable(false); - cardPreference.setPrimaryButtonText( - context.getString(R.string.battery_tip_charge_to_full_button)); + cardPreference.setPrimaryButtonText(context.getString(R.string.learn_more)); cardPreference.setPrimaryButtonClickListener( - unused -> { - resumeCharging(context); - preference.setVisible(false); - }); - cardPreference.setPrimaryButtonVisible(mIsPluggedIn); - - cardPreference.setSecondaryButtonText(context.getString(R.string.learn_more)); - cardPreference.setSecondaryButtonClickListener( button -> button.startActivityForResult( HelpUtils.getHelpIntent( @@ -101,10 +92,19 @@ public class BatteryDefenderTip extends BatteryTip { context.getString(R.string.help_url_battery_defender), /* backupContext */ ""), /* requestCode */ 0)); - cardPreference.setSecondaryButtonVisible(true); - cardPreference.setSecondaryButtonContentDescription( + cardPreference.setPrimaryButtonVisible(true); + cardPreference.setPrimaryButtonContentDescription( context.getString( R.string.battery_tip_limited_temporarily_sec_button_content_description)); + + cardPreference.setSecondaryButtonText( + context.getString(R.string.battery_tip_charge_to_full_button)); + cardPreference.setSecondaryButtonClickListener( + unused -> { + resumeCharging(context); + preference.setVisible(false); + }); + cardPreference.setSecondaryButtonVisible(mIsPluggedIn); } private void resumeCharging(Context context) { From 9c06e522bcb22f2bc65eae2e195dbc213664e4bd Mon Sep 17 00:00:00 2001 From: Zaiyue Xue Date: Tue, 2 Jan 2024 11:55:02 +0800 Subject: [PATCH 6/8] Fix battery usage chart was unexpectedly cleared Bug: 318308397 Fix: 318308397 Test: manual Change-Id: Ia00c0680a563eaffb3609c1372336a6b9ed7fa17 --- .../batteryusage/BootBroadcastReceiver.java | 2 +- .../fuelgauge/batteryusage/DatabaseUtils.java | 81 +++++++++++++------ 2 files changed, 56 insertions(+), 27 deletions(-) diff --git a/src/com/android/settings/fuelgauge/batteryusage/BootBroadcastReceiver.java b/src/com/android/settings/fuelgauge/batteryusage/BootBroadcastReceiver.java index dd4848393fa..299ba09ff45 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/BootBroadcastReceiver.java +++ b/src/com/android/settings/fuelgauge/batteryusage/BootBroadcastReceiver.java @@ -71,7 +71,7 @@ public final class BootBroadcastReceiver extends BroadcastReceiver { break; case Intent.ACTION_TIME_CHANGED: Log.d(TAG, "refresh job and clear all data from action=" + action); - DatabaseUtils.clearDataAfterTimeChangedIfNeeded(context); + DatabaseUtils.clearDataAfterTimeChangedIfNeeded(context, intent); break; default: Log.w(TAG, "receive unsupported action=" + action); diff --git a/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java b/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java index ee0e449a1e4..d489252bb5c 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java +++ b/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java @@ -16,6 +16,8 @@ package com.android.settings.fuelgauge.batteryusage; +import static android.content.Intent.FLAG_RECEIVER_REPLACE_PENDING; + import static com.android.settings.fuelgauge.batteryusage.ConvertUtils.utcToLocalTimeForLogging; import android.app.usage.IUsageStatsManager; @@ -150,6 +152,7 @@ public final class DatabaseUtils { .authority(AUTHORITY) .appendPath(BATTERY_USAGE_SLOT_TABLE) .build(); + /** A list of level record event types to access battery usage data. */ public static final List BATTERY_LEVEL_RECORD_EVENTS = List.of(BatteryEventType.FULL_CHARGED, BatteryEventType.EVEN_HOUR); @@ -454,32 +457,58 @@ public final class DatabaseUtils { } /** Clears all data and jobs if current timestamp is out of the range of last recorded job. */ - public static void clearDataAfterTimeChangedIfNeeded(Context context) { - AsyncTask.execute(() -> { - try { - final List batteryLevelRecordEvents = - DatabaseUtils.getBatteryEvents(context, Calendar.getInstance(), - getLastFullChargeTime(context), BATTERY_LEVEL_RECORD_EVENTS); - final long lastRecordTimestamp = batteryLevelRecordEvents.isEmpty() - ? INVALID_TIMESTAMP : batteryLevelRecordEvents.get(0).getTimestamp(); - final long nextRecordTimestamp = - TimestampUtils.getNextEvenHourTimestamp(lastRecordTimestamp); - final long currentTime = System.currentTimeMillis(); - final boolean isOutOfTimeRange = lastRecordTimestamp == INVALID_TIMESTAMP - || currentTime < lastRecordTimestamp || currentTime > nextRecordTimestamp; - final String logInfo = String.format(Locale.ENGLISH, - "clear database = %b, current time = %d, last record time = %d", - isOutOfTimeRange, currentTime, lastRecordTimestamp); - Log.d(TAG, logInfo); - BatteryUsageLogUtils.writeLog(context, Action.TIME_UPDATED, logInfo); - if (isOutOfTimeRange) { - DatabaseUtils.clearAll(context); - PeriodicJobManager.getInstance(context).refreshJob(/* fromBoot= */ false); - } - } catch (RuntimeException e) { - Log.e(TAG, "refreshDataAndJobIfNeededAfterTimeChanged() failed", e); - } - }); + public static void clearDataAfterTimeChangedIfNeeded(Context context, Intent intent) { + AsyncTask.execute( + () -> { + try { + if ((intent.getFlags() & FLAG_RECEIVER_REPLACE_PENDING) != 0) { + BatteryUsageLogUtils.writeLog( + context, + Action.TIME_UPDATED, + "Database is not cleared because the time change intent is only" + + " for the existing pending receiver."); + return; + } + final List batteryLevelRecordEvents = + DatabaseUtils.getBatteryEvents( + context, + Calendar.getInstance(), + getLastFullChargeTime(context), + BATTERY_LEVEL_RECORD_EVENTS); + final long lastRecordTimestamp = + batteryLevelRecordEvents.isEmpty() + ? INVALID_TIMESTAMP + : batteryLevelRecordEvents.get(0).getTimestamp(); + final long nextRecordTimestamp = + TimestampUtils.getNextEvenHourTimestamp(lastRecordTimestamp); + final long currentTime = System.currentTimeMillis(); + final boolean isOutOfTimeRange = + lastRecordTimestamp == INVALID_TIMESTAMP + || currentTime < lastRecordTimestamp + || currentTime > nextRecordTimestamp; + final String logInfo = + String.format( + Locale.ENGLISH, + "clear database = %b, current time = %d, " + + "last record time = %d", + isOutOfTimeRange, + currentTime, + lastRecordTimestamp); + Log.d(TAG, logInfo); + BatteryUsageLogUtils.writeLog(context, Action.TIME_UPDATED, logInfo); + if (isOutOfTimeRange) { + DatabaseUtils.clearAll(context); + PeriodicJobManager.getInstance(context) + .refreshJob(/* fromBoot= */ false); + } + } catch (RuntimeException e) { + Log.e(TAG, "refreshDataAndJobIfNeededAfterTimeChanged() failed", e); + BatteryUsageLogUtils.writeLog( + context, + Action.TIME_UPDATED, + "refreshDataAndJobIfNeededAfterTimeChanged() failed" + e); + } + }); } /** Returns the timestamp for 00:00 6 days before the calendar date. */ From cccb5e57469a19434048ddddd6812ba8b3972503 Mon Sep 17 00:00:00 2001 From: Pajace Chen Date: Tue, 2 Jan 2024 05:21:25 +0000 Subject: [PATCH 7/8] Revert "[Shadow] Switch the button for defend battery tips" This reverts commit 882058de81261a6b890c450d1acf9c6f75d73e4f. Reason for revert: Need update the test case Change-Id: I9bb35941541408f04fb414e2b119619749e28cbf --- .../batterytip/tips/BatteryDefenderTip.java | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/com/android/settings/fuelgauge/batterytip/tips/BatteryDefenderTip.java b/src/com/android/settings/fuelgauge/batterytip/tips/BatteryDefenderTip.java index 047bf135741..b5ec522b707 100644 --- a/src/com/android/settings/fuelgauge/batterytip/tips/BatteryDefenderTip.java +++ b/src/com/android/settings/fuelgauge/batterytip/tips/BatteryDefenderTip.java @@ -83,8 +83,17 @@ public class BatteryDefenderTip extends BatteryTip { } cardPreference.setSelectable(false); - cardPreference.setPrimaryButtonText(context.getString(R.string.learn_more)); + cardPreference.setPrimaryButtonText( + context.getString(R.string.battery_tip_charge_to_full_button)); cardPreference.setPrimaryButtonClickListener( + unused -> { + resumeCharging(context); + preference.setVisible(false); + }); + cardPreference.setPrimaryButtonVisible(mIsPluggedIn); + + cardPreference.setSecondaryButtonText(context.getString(R.string.learn_more)); + cardPreference.setSecondaryButtonClickListener( button -> button.startActivityForResult( HelpUtils.getHelpIntent( @@ -92,19 +101,10 @@ public class BatteryDefenderTip extends BatteryTip { context.getString(R.string.help_url_battery_defender), /* backupContext */ ""), /* requestCode */ 0)); - cardPreference.setPrimaryButtonVisible(true); - cardPreference.setPrimaryButtonContentDescription( + cardPreference.setSecondaryButtonVisible(true); + cardPreference.setSecondaryButtonContentDescription( context.getString( R.string.battery_tip_limited_temporarily_sec_button_content_description)); - - cardPreference.setSecondaryButtonText( - context.getString(R.string.battery_tip_charge_to_full_button)); - cardPreference.setSecondaryButtonClickListener( - unused -> { - resumeCharging(context); - preference.setVisible(false); - }); - cardPreference.setSecondaryButtonVisible(mIsPluggedIn); } private void resumeCharging(Context context) { From b02f9f38e460668d75f7d371cbfb5d61ee732278 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Budnik?= Date: Fri, 29 Dec 2023 17:05:05 +0000 Subject: [PATCH 8/8] Inject mocks in RemoteVolumeControllerTest via constructor This fix avoids the test wrongfully initializing real components before the tests can modify mockable fields, which causes NPEs. Bug: 318078730 Test: atest RemoteVolumeGroupControllerTest Change-Id: I350e3e2e45eadb0f9737ba5d4b45c9f80e3355fb --- .../RemoteVolumeGroupController.java | 17 +++++++++++++++++ .../RemoteVolumeGroupControllerTest.java | 6 +++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/com/android/settings/notification/RemoteVolumeGroupController.java b/src/com/android/settings/notification/RemoteVolumeGroupController.java index 50f9ed524da..919b6d01d23 100644 --- a/src/com/android/settings/notification/RemoteVolumeGroupController.java +++ b/src/com/android/settings/notification/RemoteVolumeGroupController.java @@ -16,6 +16,8 @@ package com.android.settings.notification; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.Context; import android.content.Intent; import android.media.MediaRouter2Manager; @@ -53,6 +55,7 @@ public class RemoteVolumeGroupController extends BasePreferenceController implem @VisibleForTesting static final String SWITCHER_PREFIX = "OUTPUT_SWITCHER"; + @Nullable private PreferenceCategory mPreferenceCategory; private final List mRoutingSessionInfos = new ArrayList<>(); @@ -61,6 +64,7 @@ public class RemoteVolumeGroupController extends BasePreferenceController implem @VisibleForTesting MediaRouter2Manager mRouterManager; + // Called via reflection from BasePreferenceController#createInstance(). public RemoteVolumeGroupController(Context context, String preferenceKey) { super(context, preferenceKey); if (mLocalMediaManager == null) { @@ -71,6 +75,19 @@ public class RemoteVolumeGroupController extends BasePreferenceController implem mRouterManager = MediaRouter2Manager.getInstance(context); } + @VisibleForTesting + /* package */ RemoteVolumeGroupController( + @NonNull Context context, + @NonNull String preferenceKey, + @NonNull LocalMediaManager localMediaManager, + @NonNull MediaRouter2Manager mediaRouter2Manager) { + super(context, preferenceKey); + mLocalMediaManager = localMediaManager; + mRouterManager = mediaRouter2Manager; + mLocalMediaManager.registerCallback(this); + mLocalMediaManager.startScan(); + } + @Override public int getAvailabilityStatus() { if (mRoutingSessionInfos.isEmpty()) { diff --git a/tests/robotests/src/com/android/settings/notification/RemoteVolumeGroupControllerTest.java b/tests/robotests/src/com/android/settings/notification/RemoteVolumeGroupControllerTest.java index 1e42e18bbc7..06bd90b38f4 100644 --- a/tests/robotests/src/com/android/settings/notification/RemoteVolumeGroupControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/RemoteVolumeGroupControllerTest.java @@ -103,9 +103,9 @@ public class RemoteVolumeGroupControllerTest { mContext = spy(RuntimeEnvironment.application); doReturn(mMediaSessionManager).when(mContext).getSystemService( Context.MEDIA_SESSION_SERVICE); - mController = new RemoteVolumeGroupController(mContext, KEY_REMOTE_VOLUME_GROUP); - mController.mLocalMediaManager = mLocalMediaManager; - mController.mRouterManager = mRouterManager; + mController = + new RemoteVolumeGroupController( + mContext, KEY_REMOTE_VOLUME_GROUP, mLocalMediaManager, mRouterManager); mPreferenceCategory = spy(new PreferenceCategory(mContext)); mPreferenceCategory.setKey(mController.getPreferenceKey());