From 9a8b50baea32516e2a0919cbb6c4f4c9df0a5134 Mon Sep 17 00:00:00 2001 From: Angela Wang Date: Wed, 19 Jun 2024 06:56:52 +0000 Subject: [PATCH 01/18] Show message when no preset info is obtained from the remote device Display message when hearing aid has no presets configured. Previously, the preset item was grayed out with no explanation, causing confusion. Now, a clear message informs users that presets are not available on their device. Bug: 345112286 Test: atest BluetoothDetailsHearingAidsPresetsControllerTest Flag: EXEMPT bugfix Change-Id: Ie1ece8f08933eb28a5947e2a030888a6bc49bc9f --- res/values/strings.xml | 2 ++ ...luetoothDetailsHearingAidsPresetsController.java | 13 ++++++++----- ...oothDetailsHearingAidsPresetsControllerTest.java | 3 +++ 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index f92fd2aca84..ee06f9bef10 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -156,6 +156,8 @@ Shortcut, hearing aid compatibility Preset + + There are no presets programmed by your audiologist Couldn\u2019t update preset diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsHearingAidsPresetsController.java b/src/com/android/settings/bluetooth/BluetoothDetailsHearingAidsPresetsController.java index 564e1384779..12f904b6ae2 100644 --- a/src/com/android/settings/bluetooth/BluetoothDetailsHearingAidsPresetsController.java +++ b/src/com/android/settings/bluetooth/BluetoothDetailsHearingAidsPresetsController.java @@ -159,19 +159,22 @@ public class BluetoothDetailsHearingAidsPresetsController extends mPreference.setEnabled(mCachedDevice.isConnectedHapClientDevice()); loadAllPresetInfo(); + mPreference.setSummary(null); if (mPreference.getEntries().length == 0) { - if (DEBUG) { - Log.w(TAG, "Disable the preference since preset info size = 0"); + if (mPreference.isEnabled()) { + if (DEBUG) { + Log.w(TAG, "Disable the preference since preset info size = 0"); + } + mPreference.setEnabled(false); + mPreference.setSummary(mContext.getString( + R.string.bluetooth_hearing_aids_presets_empty_list_message)); } - mPreference.setEnabled(false); } else { int activePresetIndex = mHapClientProfile.getActivePresetIndex( mCachedDevice.getDevice()); if (activePresetIndex != BluetoothHapClient.PRESET_INDEX_UNAVAILABLE) { mPreference.setValue(Integer.toString(activePresetIndex)); mPreference.setSummary(mPreference.getEntry()); - } else { - mPreference.setSummary(null); } } } diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingAidsPresetsControllerTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingAidsPresetsControllerTest.java index cf80a871416..36838852b8a 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingAidsPresetsControllerTest.java +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingAidsPresetsControllerTest.java @@ -38,6 +38,7 @@ import android.bluetooth.BluetoothHapPresetInfo; import androidx.preference.ListPreference; import androidx.preference.PreferenceCategory; +import com.android.settings.R; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.HapClientProfile; import com.android.settingslib.bluetooth.LocalBluetoothManager; @@ -215,6 +216,8 @@ public class BluetoothDetailsHearingAidsPresetsControllerTest extends assertThat(mController.getPreference()).isNotNull(); assertThat(mController.getPreference().isEnabled()).isFalse(); + assertThat(String.valueOf(mController.getPreference().getSummary())).isEqualTo( + mContext.getString(R.string.bluetooth_hearing_aids_presets_empty_list_message)); } @Test From e8288512ee87c410f8a972130ebf7cc307085589 Mon Sep 17 00:00:00 2001 From: Roy Chou Date: Wed, 19 Jun 2024 05:05:05 +0000 Subject: [PATCH 02/18] chore(brightness suw): adjust auto brightness detail page footer content description Ref to the bug, s2s and talkback pages' footer content descriptions are prefixed with "About XXX" for talkbalk info announcement. Therefore, for auto brightness detail page in SUW, we also prefix "About adaptive brightness" to the footer preference content description, to improve the consistency with other accessiblity feature suw pages. Bug: 347859318 Flag: com.android.settings.accessibility.add_brightness_settings_in_suw Test: manually atest AutoBrightnessPreferenceFragmentForSetupWizardTest Change-Id: Ieda4bcffb4f4e11ea68c961beee5c2fff1b29f2c --- res/values/strings.xml | 3 + ...tnessPreferenceFragmentForSetupWizard.java | 17 +++ ...sPreferenceFragmentForSetupWizardTest.java | 104 +++++++++++------- 3 files changed, 84 insertions(+), 40 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index f92fd2aca84..0711156add1 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -2753,6 +2753,9 @@ Brightness level Adaptive brightness + + + About adaptive brightness Your screen brightness will automatically adjust to your environment and activities. You can move the slider manually to help adaptive brightness learn your preferences. diff --git a/src/com/android/settings/accessibility/AutoBrightnessPreferenceFragmentForSetupWizard.java b/src/com/android/settings/accessibility/AutoBrightnessPreferenceFragmentForSetupWizard.java index ad1ae96b03b..19db2668df9 100644 --- a/src/com/android/settings/accessibility/AutoBrightnessPreferenceFragmentForSetupWizard.java +++ b/src/com/android/settings/accessibility/AutoBrightnessPreferenceFragmentForSetupWizard.java @@ -27,11 +27,13 @@ import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.preference.PreferenceScreen; import androidx.recyclerview.widget.RecyclerView; import com.android.settings.R; import com.android.settings.display.AutoBrightnessSettings; import com.android.settingslib.Utils; +import com.android.settingslib.widget.FooterPreference; import com.google.android.setupcompat.template.FooterBarMixin; import com.google.android.setupdesign.GlifPreferenceLayout; @@ -41,10 +43,14 @@ import com.google.android.setupdesign.GlifPreferenceLayout; */ public class AutoBrightnessPreferenceFragmentForSetupWizard extends AutoBrightnessSettings { + private static final String FOOTER_PREFERENCE_KEY = "auto_brightness_footer"; + @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); + updateFooterContentDescription(); + if (view instanceof GlifPreferenceLayout) { final GlifPreferenceLayout layout = (GlifPreferenceLayout) view; final String title = getContext().getString( @@ -78,4 +84,15 @@ public class AutoBrightnessPreferenceFragmentForSetupWizard extends AutoBrightne public int getMetricsCategory() { return SettingsEnums.SUW_ACCESSIBILITY_AUTO_BRIGHTNESS; } + + private void updateFooterContentDescription() { + final PreferenceScreen screen = getPreferenceScreen(); + final FooterPreference footerPreference = screen.findPreference(FOOTER_PREFERENCE_KEY); + if (footerPreference != null) { + String title = getString(R.string.auto_brightness_content_description_title); + final StringBuilder sb = new StringBuilder(); + sb.append(title).append("\n\n").append(footerPreference.getTitle()); + footerPreference.setContentDescription(sb); + } + } } diff --git a/tests/robotests/src/com/android/settings/accessibility/AutoBrightnessPreferenceFragmentForSetupWizardTest.java b/tests/robotests/src/com/android/settings/accessibility/AutoBrightnessPreferenceFragmentForSetupWizardTest.java index 1e6e068cae6..c0b9dbd2104 100644 --- a/tests/robotests/src/com/android/settings/accessibility/AutoBrightnessPreferenceFragmentForSetupWizardTest.java +++ b/tests/robotests/src/com/android/settings/accessibility/AutoBrightnessPreferenceFragmentForSetupWizardTest.java @@ -18,66 +18,97 @@ package com.android.settings.accessibility; import static com.google.common.truth.Truth.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - import android.app.settings.SettingsEnums; -import android.content.Context; +import android.os.Bundle; +import android.view.View; +import android.widget.Button; -import androidx.lifecycle.LifecycleOwner; -import androidx.test.core.app.ApplicationProvider; +import androidx.fragment.app.FragmentFactory; +import androidx.fragment.app.testing.FragmentScenario; +import androidx.lifecycle.Lifecycle; +import androidx.preference.Preference; import com.android.settings.R; +import com.android.settingslib.widget.FooterPreference; import com.google.android.setupcompat.template.FooterBarMixin; +import com.google.android.setupdesign.GlifLayout; import com.google.android.setupdesign.GlifPreferenceLayout; +import org.junit.After; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.Spy; -import org.mockito.junit.MockitoJUnit; -import org.mockito.junit.MockitoRule; import org.robolectric.RobolectricTestRunner; /** Tests for {@link AutoBrightnessPreferenceFragmentForSetupWizard}. */ @RunWith(RobolectricTestRunner.class) public class AutoBrightnessPreferenceFragmentForSetupWizardTest { - @Rule - public final MockitoRule mMockito = MockitoJUnit.rule(); + // Same as AutoBrightnessPreferenceFragmentForSetupWizard#FOOTER_PREFERENCE_KEY + private static final String FOOTER_PREFERENCE_KEY = "auto_brightness_footer"; + + private FragmentScenario mFragmentScenario; - @Spy - private final Context mContext = ApplicationProvider.getApplicationContext(); - @Mock - private GlifPreferenceLayout mGlifLayoutView; - @Mock - private FooterBarMixin mFooterBarMixin; private AutoBrightnessPreferenceFragmentForSetupWizard mFragment; + private GlifLayout mGlifLayout; @Before public void setUp() { - mFragment = spy(new AutoBrightnessPreferenceFragmentForSetupWizard()); - doReturn(mock(LifecycleOwner.class)).when(mFragment).getViewLifecycleOwner(); - doReturn(mContext).when(mFragment).getContext(); - when(mGlifLayoutView.getMixin(eq(FooterBarMixin.class))).thenReturn(mFooterBarMixin); + mFragmentScenario = FragmentScenario + .launch( + AutoBrightnessPreferenceFragmentForSetupWizard.class, + /* fragmentArgs= */ (Bundle) null, + R.style.GlifTheme, + /* factory= */ (FragmentFactory) null) + .moveToState(Lifecycle.State.RESUMED); + mFragmentScenario.onFragment(fragment -> mFragment = fragment); + + View view = mFragment.getView(); + assertThat(view).isInstanceOf(GlifPreferenceLayout.class); + mGlifLayout = (GlifLayout) view; + } + + @After + public void tearDown() { + mFragmentScenario.close(); } @Test - public void setHeaderText_onViewCreated_verifyAction() { - final String title = "title"; - doReturn(title).when(mContext).getString(R.string.auto_brightness_title); + public void onViewCreated_verifyGlifHerderText() { + assertThat(mGlifLayout.getHeaderText()) + .isEqualTo(mFragment.getString(R.string.auto_brightness_title)); + } - mFragment.onViewCreated(mGlifLayoutView, null); + @Test + public void onViewCreated_verifyGlifFooter() { + FooterBarMixin footerMixin = mGlifLayout.getMixin(FooterBarMixin.class); + assertThat(footerMixin).isNotNull(); - verify(mGlifLayoutView).setHeaderText(title); + Button footerButton = footerMixin.getPrimaryButtonView(); + assertThat(footerButton).isNotNull(); + assertThat(footerButton.getText().toString()).isEqualTo(mFragment.getString(R.string.done)); + + footerButton.performClick(); + assertThat(mFragment.getActivity().isFinishing()).isTrue(); + } + + @Test + public void onViewCreated_verifyFooterPreference() { + Preference pref = mFragment.findPreference(FOOTER_PREFERENCE_KEY); + assertThat(pref).isInstanceOf(FooterPreference.class); + + FooterPreference footerPref = (FooterPreference) pref; + String exactTitle = footerPref.getTitle().toString(); + assertThat(exactTitle).isEqualTo(mFragment.getString(R.string.auto_brightness_description)); + + // Ensure that footer content description has "About XXX" prefix for consistency with other + // accessibility suw pages + String expectedContentDescription = + mFragment.getString(R.string.auto_brightness_content_description_title) + + "\n\n" + exactTitle; + assertThat(footerPref.getContentDescription().toString()) + .isEqualTo(expectedContentDescription); } @Test @@ -85,11 +116,4 @@ public class AutoBrightnessPreferenceFragmentForSetupWizardTest { assertThat(mFragment.getMetricsCategory()).isEqualTo( SettingsEnums.SUW_ACCESSIBILITY_AUTO_BRIGHTNESS); } - - @Test - public void onViewCreated_verifyAction() { - mFragment.onViewCreated(mGlifLayoutView, null); - - verify(mFooterBarMixin).setPrimaryButton(any()); - } } From 2b78c17e84267aea414fa49ac2a521d86cc88f42 Mon Sep 17 00:00:00 2001 From: Angela Wang Date: Wed, 19 Jun 2024 08:10:09 +0000 Subject: [PATCH 03/18] Remove unavailable preset info option Bug: 347134589 Test: atest BluetoothDetailsHearingAidsPresetsControllerTest Flag: EXEMPT bugfix Change-Id: Iabdbe675a08fcd172617ef31dd0b8fbe8dccbb89 --- ...thDetailsHearingAidsPresetsController.java | 3 +- ...tailsHearingAidsPresetsControllerTest.java | 28 ++++++++++++++++--- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsHearingAidsPresetsController.java b/src/com/android/settings/bluetooth/BluetoothDetailsHearingAidsPresetsController.java index 12f904b6ae2..f7ccc610870 100644 --- a/src/com/android/settings/bluetooth/BluetoothDetailsHearingAidsPresetsController.java +++ b/src/com/android/settings/bluetooth/BluetoothDetailsHearingAidsPresetsController.java @@ -276,7 +276,8 @@ public class BluetoothDetailsHearingAidsPresetsController extends return; } List infoList = mHapClientProfile.getAllPresetInfo( - mCachedDevice.getDevice()); + mCachedDevice.getDevice()).stream().filter( + BluetoothHapPresetInfo::isAvailable).toList(); CharSequence[] presetNames = new CharSequence[infoList.size()]; CharSequence[] presetIndexes = new CharSequence[infoList.size()]; for (int i = 0; i < infoList.size(); i++) { diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingAidsPresetsControllerTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingAidsPresetsControllerTest.java index 36838852b8a..7c865f340a6 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingAidsPresetsControllerTest.java +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingAidsPresetsControllerTest.java @@ -222,7 +222,7 @@ public class BluetoothDetailsHearingAidsPresetsControllerTest extends @Test public void refresh_validPresetInfo_preferenceEnabled() { - BluetoothHapPresetInfo info = getTestPresetInfo(); + BluetoothHapPresetInfo info = getTestPresetInfo(true); when(mHapClientProfile.getAllPresetInfo(mDevice)).thenReturn(List.of(info)); mController.refresh(); @@ -233,7 +233,7 @@ public class BluetoothDetailsHearingAidsPresetsControllerTest extends @Test public void refresh_invalidActivePresetIndex_summaryIsNull() { - BluetoothHapPresetInfo info = getTestPresetInfo(); + BluetoothHapPresetInfo info = getTestPresetInfo(true); when(mHapClientProfile.getAllPresetInfo(mDevice)).thenReturn(List.of(info)); when(mHapClientProfile.getActivePresetIndex(mDevice)).thenReturn(PRESET_INDEX_UNAVAILABLE); @@ -245,7 +245,7 @@ public class BluetoothDetailsHearingAidsPresetsControllerTest extends @Test public void refresh_validActivePresetIndex_summaryIsNotNull() { - BluetoothHapPresetInfo info = getTestPresetInfo(); + BluetoothHapPresetInfo info = getTestPresetInfo(true); when(mHapClientProfile.getAllPresetInfo(mDevice)).thenReturn(List.of(info)); when(mHapClientProfile.getActivePresetIndex(mDevice)).thenReturn(TEST_PRESET_INDEX); @@ -265,10 +265,30 @@ public class BluetoothDetailsHearingAidsPresetsControllerTest extends verify(mHapClientProfile).selectPreset(mDevice, TEST_PRESET_INDEX); } - private BluetoothHapPresetInfo getTestPresetInfo() { + @Test + public void loadAllPresetInfo_unavailablePreset_notAddedToEntries() { + BluetoothHapPresetInfo info = getTestPresetInfo(false); + when(mHapClientProfile.getAllPresetInfo(mDevice)).thenReturn(List.of(info)); + + mController.refresh(); + + assertThat(mController.getPreference().getEntries().length).isEqualTo(0); + } + + @Test + public void loadAllPresetInfo_availablePreset_addedToEntries() { + BluetoothHapPresetInfo info = getTestPresetInfo(true); + when(mHapClientProfile.getAllPresetInfo(mDevice)).thenReturn(List.of(info)); + + mController.refresh(); + + assertThat(mController.getPreference().getEntries().length).isEqualTo(1); + } + private BluetoothHapPresetInfo getTestPresetInfo(boolean available) { BluetoothHapPresetInfo info = mock(BluetoothHapPresetInfo.class); when(info.getName()).thenReturn(TEST_PRESET_NAME); when(info.getIndex()).thenReturn(TEST_PRESET_INDEX); + when(info.isAvailable()).thenReturn(available); return info; } From f9424c6231d67a6891ab6aa7312d7c1455ac06ac Mon Sep 17 00:00:00 2001 From: Yiyi Shen Date: Tue, 25 Jun 2024 17:59:02 +0800 Subject: [PATCH 04/18] [Audiosharing] Avoid start sharing dialog for single device. Issue - for LEA device with two BT addresses, when the second bud connected with incorrect group id, we wrongly show up the start audio sharing dialog. Fix - check there are two connected LEA valid groups before show up start audio sharing dialog. Bug: 347655885 Test: atest Flag: com.android.settingslib.flags.enable_le_audio_sharing Change-Id: Icd86ce2cfa4312c10c14906f46df324357c56990 --- .../AudioSharingDialogHandler.java | 15 ++++++-- .../AudioSharingDialogHandlerTest.java | 34 +++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogHandler.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogHandler.java index 15e3de90e7a..753daaf33bb 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogHandler.java +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogHandler.java @@ -258,6 +258,8 @@ public class AudioSharingDialogHandler { boolean userTriggered) { Map> groupedDevices = AudioSharingUtils.fetchConnectedDevicesByGroupId(mLocalBtManager); + BluetoothDevice btDevice = cachedDevice.getDevice(); + String deviceAddress = btDevice == null ? "" : btDevice.getAnonymizedAddress(); if (isBroadcasting) { // If another device within the same is already in the sharing session, add source to // the device automatically. @@ -271,10 +273,10 @@ public class AudioSharingDialogHandler { Log.d( TAG, "Automatically add another device within the same group to the sharing: " - + cachedDevice.getDevice().getAnonymizedAddress()); + + deviceAddress); if (mAssistant != null && mBroadcast != null) { mAssistant.addSource( - cachedDevice.getDevice(), + btDevice, mBroadcast.getLatestBluetoothLeBroadcastMetadata(), /* isGroupOp= */ false); } @@ -313,6 +315,7 @@ public class AudioSharingDialogHandler { cachedDevice, listener, eventData); + Log.d(TAG, "Show disconnect dialog, device = " + deviceAddress); }); } else { // Show audio sharing join dialog when the first or second eligible (LE audio) @@ -343,9 +346,11 @@ public class AudioSharingDialogHandler { cachedDevice, listener, eventData); + Log.d(TAG, "Show join dialog, device = " + deviceAddress); }); } } else { + // Build a list of AudioSharingDeviceItem for connected devices other than cachedDevice. List deviceItems = new ArrayList<>(); for (List devices : groupedDevices.values()) { // Use random device in the group within the sharing session to represent the group. @@ -358,7 +363,7 @@ public class AudioSharingDialogHandler { } // Show audio sharing join dialog when the second eligible (LE audio) remote // device connect and no sharing session. - if (deviceItems.size() == 1) { + if (groupedDevices.size() == 2 && deviceItems.size() == 1) { AudioSharingJoinDialogFragment.DialogEventListener listener = new AudioSharingJoinDialogFragment.DialogEventListener() { @Override @@ -396,9 +401,13 @@ public class AudioSharingDialogHandler { closeOpeningDialogsOtherThan(AudioSharingJoinDialogFragment.tag()); AudioSharingJoinDialogFragment.show( mHostFragment, deviceItems, cachedDevice, listener, eventData); + Log.d(TAG, "Show start dialog, device = " + deviceAddress); }); } else if (userTriggered) { cachedDevice.setActive(); + Log.d(TAG, "Set active device = " + deviceAddress); + } else { + Log.d(TAG, "Fail to handle LE audio device connected, device = " + deviceAddress); } } } diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogHandlerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogHandlerTest.java index a7e6f5698d2..53c214be43a 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogHandlerTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogHandlerTest.java @@ -248,6 +248,23 @@ public class AudioSharingDialogHandlerTest { verify(mCachedDevice1).setActive(); } + @Test + public void handleUserTriggeredLeaDeviceConnected_noSharingLeaDeviceInErrorState_setActive() { + setUpBroadcast(false); + when(mCachedDevice1.getGroupId()).thenReturn(-1); + when(mLeAudioProfile.getGroupId(mDevice1)).thenReturn(-1); + ImmutableList deviceList = ImmutableList.of(mDevice1, mDevice3); + when(mAssistant.getDevicesMatchingConnectionStates( + new int[] {BluetoothProfile.STATE_CONNECTED})) + .thenReturn(deviceList); + when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of()); + mHandler.handleDeviceConnected(mCachedDevice1, /* userTriggered= */ true); + shadowOf(Looper.getMainLooper()).idle(); + List childFragments = mParentFragment.getChildFragmentManager().getFragments(); + assertThat(childFragments).isEmpty(); + verify(mCachedDevice1).setActive(); + } + @Test public void handleUserTriggeredLeaDeviceConnected_noSharingTwoLeaDevices_showJoinDialog() { setUpBroadcast(false); @@ -451,6 +468,23 @@ public class AudioSharingDialogHandlerTest { verify(mCachedDevice1, never()).setActive(); } + @Test + public void handleLeaDeviceConnected_noSharingLeaDeviceInErrorState_doNothing() { + setUpBroadcast(false); + when(mCachedDevice1.getGroupId()).thenReturn(-1); + when(mLeAudioProfile.getGroupId(mDevice1)).thenReturn(-1); + ImmutableList deviceList = ImmutableList.of(mDevice1, mDevice3); + when(mAssistant.getDevicesMatchingConnectionStates( + new int[] {BluetoothProfile.STATE_CONNECTED})) + .thenReturn(deviceList); + when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of()); + mHandler.handleDeviceConnected(mCachedDevice1, /* userTriggered= */ false); + shadowOf(Looper.getMainLooper()).idle(); + List childFragments = mParentFragment.getChildFragmentManager().getFragments(); + assertThat(childFragments).isEmpty(); + verify(mCachedDevice1, never()).setActive(); + } + @Test public void handleLeaDeviceConnected_noSharingTwoLeaDevices_showJoinDialog() { setUpBroadcast(false); From 477ebd25f2f688adc647200f5896c7fe9fa3e5dc Mon Sep 17 00:00:00 2001 From: Julia Reynolds Date: Tue, 25 Jun 2024 16:46:41 -0400 Subject: [PATCH 05/18] Move 'show hidden channels' option Make it more prominent to make it more clear what's happening when you unblock an app Test: DeletedChannelsPreferenceControllerTest Test: ShowMorePreferenceControllerTest Flag: com.android.server.notification.notification_hide_unused_channels Bug: 322536537 Change-Id: I745b2037b4dc907a4307fa7f70ecc3a4c9db2dd2 --- res/values/strings.xml | 3 + res/xml/app_notification_settings.xml | 5 + .../notification/NotificationBackend.java | 6 + .../app/AppNotificationSettings.java | 40 +------ .../app/ChannelListPreferenceController.java | 8 +- .../DeletedChannelsPreferenceController.java | 5 + .../app/ShowMorePreferenceController.java | 75 ++++++++++++ ...letedChannelsPreferenceControllerTest.java | 17 +++ .../app/ShowMorePreferenceControllerTest.java | 113 ++++++++++++++++++ 9 files changed, 227 insertions(+), 45 deletions(-) create mode 100644 src/com/android/settings/notification/app/ShowMorePreferenceController.java create mode 100644 tests/robotests/src/com/android/settings/notification/app/ShowMorePreferenceControllerTest.java diff --git a/res/values/strings.xml b/res/values/strings.xml index 50556de6e0b..21ab9179933 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -8935,6 +8935,9 @@ This app has not posted any notifications + + Show unused categories + Additional settings in the app diff --git a/res/xml/app_notification_settings.xml b/res/xml/app_notification_settings.xml index f96a375224e..091de7536da 100644 --- a/res/xml/app_notification_settings.xml +++ b/res/xml/app_notification_settings.xml @@ -55,6 +55,11 @@ android:key="channels" android:layout="@layout/empty_view" /> + + sentByChannel; public NotificationsSentState sentByApp; + public boolean showAllChannels = true; } } diff --git a/src/com/android/settings/notification/app/AppNotificationSettings.java b/src/com/android/settings/notification/app/AppNotificationSettings.java index 89756b7b839..3d3f3429e9a 100644 --- a/src/com/android/settings/notification/app/AppNotificationSettings.java +++ b/src/com/android/settings/notification/app/AppNotificationSettings.java @@ -16,16 +16,10 @@ package com.android.settings.notification.app; -import static com.android.server.notification.Flags.notificationHideUnusedChannels; - - import android.app.settings.SettingsEnums; import android.content.Context; import android.text.TextUtils; import android.util.Log; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; import com.android.internal.widget.LockPatternUtils; import com.android.settings.R; @@ -107,38 +101,8 @@ public class AppNotificationSettings extends NotificationSettings { mControllers.add(new BubbleSummaryPreferenceController(context, mBackend)); mControllers.add(new NotificationsOffPreferenceController(context)); mControllers.add(new DeletedChannelsPreferenceController(context, mBackend)); + mControllers.add(new ShowMorePreferenceController( + context, mDependentFieldListener, mBackend)); return new ArrayList<>(mControllers); } - - private final int SHOW_ALL_CHANNELS = 1; - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - if (notificationHideUnusedChannels()) { - menu.add(Menu.NONE, SHOW_ALL_CHANNELS, Menu.NONE, - mShowAll ? R.string.hide_unused_channels : R.string.show_unused_channels); - } - super.onCreateOptionsMenu(menu, inflater); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (!notificationHideUnusedChannels()) { - return super.onOptionsItemSelected(item); - } - switch (item.getItemId()) { - case SHOW_ALL_CHANNELS: - mShowAll = !mShowAll; - item.setTitle(mShowAll - ? R.string.hide_unused_channels - : R.string.show_unused_channels); - ChannelListPreferenceController list = - use(ChannelListPreferenceController.class); - list.setShowAll(mShowAll); - list.updateState(findPreference(list.getPreferenceKey())); - return true; - default: - return super.onOptionsItemSelected(item); - } - } } diff --git a/src/com/android/settings/notification/app/ChannelListPreferenceController.java b/src/com/android/settings/notification/app/ChannelListPreferenceController.java index 70775926e9b..b8dfb6a7069 100644 --- a/src/com/android/settings/notification/app/ChannelListPreferenceController.java +++ b/src/com/android/settings/notification/app/ChannelListPreferenceController.java @@ -59,8 +59,6 @@ public class ChannelListPreferenceController extends NotificationPreferenceContr private List mChannelGroupList; private PreferenceCategory mPreference; - private boolean mShowAll; - public ChannelListPreferenceController(Context context, NotificationBackend backend) { super(context, backend); } @@ -100,7 +98,7 @@ public class ChannelListPreferenceController extends NotificationPreferenceContr @Override protected Void doInBackground(Void... unused) { if (notificationHideUnusedChannels()) { - if (mShowAll) { + if (mAppRow.showAllChannels) { mChannelGroupList = mBackend.getGroups(mAppRow.pkg, mAppRow.uid).getList(); } else { mChannelGroupList = mBackend.getGroupsWithRecentBlockedFilter(mAppRow.pkg, @@ -123,10 +121,6 @@ public class ChannelListPreferenceController extends NotificationPreferenceContr }.execute(); } - protected void setShowAll(boolean showAll) { - mShowAll = showAll; - } - /** * Update the preferences group to match the * @param groupPrefsList diff --git a/src/com/android/settings/notification/app/DeletedChannelsPreferenceController.java b/src/com/android/settings/notification/app/DeletedChannelsPreferenceController.java index 6a1d4cb1247..07b7fdab01d 100644 --- a/src/com/android/settings/notification/app/DeletedChannelsPreferenceController.java +++ b/src/com/android/settings/notification/app/DeletedChannelsPreferenceController.java @@ -16,6 +16,8 @@ package com.android.settings.notification.app; +import static com.android.server.notification.Flags.notificationHideUnusedChannels; + import android.content.Context; import androidx.preference.Preference; @@ -44,6 +46,9 @@ public class DeletedChannelsPreferenceController extends NotificationPreferenceC if (!super.isAvailable()) { return false; } + if (notificationHideUnusedChannels()) { + return false; + } // only visible on app screen if (mChannel != null || hasValidGroup()) { return false; diff --git a/src/com/android/settings/notification/app/ShowMorePreferenceController.java b/src/com/android/settings/notification/app/ShowMorePreferenceController.java new file mode 100644 index 00000000000..dbc279a6c8b --- /dev/null +++ b/src/com/android/settings/notification/app/ShowMorePreferenceController.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2024 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.notification.app; + +import static com.android.server.notification.Flags.notificationHideUnusedChannels; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.settings.notification.NotificationBackend; + +import org.jetbrains.annotations.NotNull; + +public class ShowMorePreferenceController extends NotificationPreferenceController { + + private static final String KEY = "more"; + private NotificationSettings.DependentFieldListener mDependentFieldListener; + + public ShowMorePreferenceController(Context context, + NotificationSettings.DependentFieldListener dependentFieldListener, + NotificationBackend backend) { + super(context, backend); + mDependentFieldListener = dependentFieldListener; + } + + @Override + public String getPreferenceKey() { + return KEY; + } + + @Override + public boolean isAvailable() { + if (!notificationHideUnusedChannels()) { + return false; + } + if (mAppRow == null) { + return false; + } + if (mAppRow.banned || mAppRow.showAllChannels) { + return false; + } + return true; + } + + @Override + boolean isIncludedInFilter() { + return false; + } + + @Override + public void updateState(Preference preference) { + preference.setOnPreferenceClickListener(preference1 -> { + mAppRow.showAllChannels = true; + mDependentFieldListener.onFieldValueChanged(); + return true; + }); + } +} diff --git a/tests/robotests/src/com/android/settings/notification/app/DeletedChannelsPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/app/DeletedChannelsPreferenceControllerTest.java index 5c9de7cb30b..267b8d74616 100644 --- a/tests/robotests/src/com/android/settings/notification/app/DeletedChannelsPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/app/DeletedChannelsPreferenceControllerTest.java @@ -31,12 +31,17 @@ import android.app.NotificationChannelGroup; import android.app.NotificationManager; import android.content.Context; import android.os.UserManager; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import androidx.preference.Preference; +import com.android.server.notification.Flags; import com.android.settings.notification.NotificationBackend; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -60,6 +65,8 @@ public class DeletedChannelsPreferenceControllerTest { private UserManager mUm; private DeletedChannelsPreferenceController mController; + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @Before public void setUp() { @@ -109,6 +116,16 @@ public class DeletedChannelsPreferenceControllerTest { } @Test + @EnableFlags(Flags.FLAG_NOTIFICATION_HIDE_UNUSED_CHANNELS) + public void isAvailable_notIfFlagEnabled() { + when(mBackend.getDeletedChannelCount(any(), anyInt())).thenReturn(1); + mController.onResume( + new NotificationBackend.AppRow(), null, null, null, null, null, new ArrayList<>()); + assertFalse(mController.isAvailable()); + } + + @Test + @DisableFlags(Flags.FLAG_NOTIFICATION_HIDE_UNUSED_CHANNELS) public void isAvailable_appScreen() { when(mBackend.getDeletedChannelCount(any(), anyInt())).thenReturn(1); mController.onResume( diff --git a/tests/robotests/src/com/android/settings/notification/app/ShowMorePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/app/ShowMorePreferenceControllerTest.java new file mode 100644 index 00000000000..611c80a48f1 --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/app/ShowMorePreferenceControllerTest.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2024 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.notification.app; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertFalse; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import android.app.NotificationChannelGroup; +import android.app.NotificationManager; +import android.content.Context; +import android.os.UserManager; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; + +import androidx.preference.Preference; + +import com.android.server.notification.Flags; +import com.android.settings.notification.NotificationBackend; + +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.RuntimeEnvironment; +import org.robolectric.shadows.ShadowApplication; + +@RunWith(RobolectricTestRunner.class) +@EnableFlags(Flags.FLAG_NOTIFICATION_HIDE_UNUSED_CHANNELS) +public class ShowMorePreferenceControllerTest { + + private Context mContext; + @Mock + private NotificationBackend mBackend; + @Mock + private NotificationManager mNm; + @Mock + private UserManager mUm; + @Mock + private NotificationSettings.DependentFieldListener mDependentFieldListener; + + private ShowMorePreferenceController mController; + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + ShadowApplication shadowApplication = ShadowApplication.getInstance(); + shadowApplication.setSystemService(Context.NOTIFICATION_SERVICE, mNm); + shadowApplication.setSystemService(Context.USER_SERVICE, mUm); + mContext = RuntimeEnvironment.application; + mController = new ShowMorePreferenceController(mContext, mDependentFieldListener, mBackend); + } + + @Test + public void noCrashIfNoOnResume() { + mController.isAvailable(); + mController.updateState(mock(Preference.class)); + } + + @Test + public void isAvailable_notIfAppBlocked() { + NotificationBackend.AppRow appRow = new NotificationBackend.AppRow(); + appRow.banned = true; + appRow.showAllChannels = false; + mController.onResume(appRow, null, null, null, null, null, null); + assertFalse(mController.isAvailable()); + } + + @Test + public void isAvailable_notIfShowingAll() { + NotificationBackend.AppRow appRow = new NotificationBackend.AppRow(); + mController.onResume(appRow, null, mock(NotificationChannelGroup.class), null, null, null, + null); + assertFalse(mController.isAvailable()); + } + + @Test + public void updateState() { + NotificationBackend.AppRow appRow = new NotificationBackend.AppRow(); + appRow.banned = false; + appRow.showAllChannels = false; + mController.onResume(appRow, null, null, null, null, null, null); + + Preference pref = new Preference(mContext); + mController.updateState(pref); + + pref.performClick(); + + verify(mDependentFieldListener).onFieldValueChanged(); + assertThat(appRow.showAllChannels).isTrue(); + } +} From 16f973b8364171116ce16b951113d64beefe506c Mon Sep 17 00:00:00 2001 From: Joshua Mccloskey Date: Tue, 25 Jun 2024 21:34:37 +0000 Subject: [PATCH 06/18] Revert "Restart fingerprint auth on cancel." This reverts commit 4efd4c16f9373569b26cdc2752727faf90b5c9c0. Reason for revert: Breaks auth behavior Fixes: 347858844 Change-Id: Ie1b8be9dfae2cba7e2b37187f08cf1360aedf29f --- .../fingerprint/FingerprintSettings.java | 5 ++++ .../FingerprintSettingsFragmentTest.java | 30 +------------------ 2 files changed, 6 insertions(+), 29 deletions(-) diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java b/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java index 109ae4f22fb..f0415286a6d 100644 --- a/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java +++ b/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java @@ -352,6 +352,11 @@ public class FingerprintSettings extends SubSettings { */ protected void handleError(int errMsgId, CharSequence msg) { switch (errMsgId) { + case FingerprintManager.FINGERPRINT_ERROR_CANCELED: + case FingerprintManager.FINGERPRINT_ERROR_USER_CANCELED: + // Only happens if we get preempted by another activity, or canceled by the + // user (e.g. swipe up to home). Ignored. + return; case FingerprintManager.FINGERPRINT_ERROR_LOCKOUT: mInFingerprintLockout = true; // We've been locked out. Reset after 30s. diff --git a/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsFragmentTest.java b/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsFragmentTest.java index 6407f648acb..58e7e2d4003 100644 --- a/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsFragmentTest.java +++ b/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsFragmentTest.java @@ -17,7 +17,6 @@ package com.android.settings.biometrics.fingerprint; import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_POWER_BUTTON; -import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_REAR; import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFPS_OPTICAL; import static com.android.settings.biometrics.fingerprint.FingerprintSettings.FingerprintSettingsFragment; @@ -34,16 +33,13 @@ import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; import android.content.Context; import android.content.Intent; import android.content.pm.UserInfo; import android.hardware.biometrics.ComponentInfoInternal; import android.hardware.biometrics.SensorProperties; -import android.hardware.fingerprint.Fingerprint; import android.hardware.fingerprint.FingerprintManager; import android.hardware.fingerprint.FingerprintSensorProperties; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; @@ -84,7 +80,6 @@ import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; import java.util.ArrayList; -import java.util.List; @RunWith(RobolectricTestRunner.class) @Config(shadows = {ShadowSettingsPreferenceFragment.class, ShadowUtils.class, ShadowFragment.class, @@ -152,6 +147,7 @@ public class FingerprintSettingsFragmentTest { public void testCancellationSignalLifeCycle() { setUpFragment(false); + mFingerprintAuthenticateSidecar.setFingerprintManager(mFingerprintManager); doNothing().when(mFingerprintManager).authenticate(any(), mCancellationSignalArgumentCaptor.capture(), @@ -217,7 +213,6 @@ public class FingerprintSettingsFragmentTest { doReturn(fragmentManager).when(mActivity).getSupportFragmentManager(); mFingerprintAuthenticateSidecar = new FingerprintAuthenticateSidecar(); - mFingerprintAuthenticateSidecar.setFingerprintManager(mFingerprintManager); doReturn(mFingerprintAuthenticateSidecar).when(fragmentManager).findFragmentByTag( "authenticate_sidecar"); @@ -251,27 +246,4 @@ public class FingerprintSettingsFragmentTest { true /* resetLockoutRequiresHardwareAuthToken */)); doReturn(props).when(mFingerprintManager).getSensorPropertiesInternal(); } - - @Test - public void testAuthOnFragmentSetup() { - doReturn(List.of(new Fingerprint("Finger 1", 1, 2, 3))) - .when(mFingerprintManager).getEnrolledFingerprints(anyInt()); - setUpFragment(false, 1, TYPE_REAR); - - verify(mFingerprintManager).authenticate(any(), any(), - any(), any(), anyInt()); - } - - @Test - public void testErrorCancelledRestartsAuth() { - doReturn(List.of(new Fingerprint("Finger 1", 1, 2, 3))) - .when(mFingerprintManager).getEnrolledFingerprints(anyInt()); - setUpFragment(false, 1, TYPE_REAR); - - // When we receive a cancel, we should restart auth. - mFragment.handleError(FingerprintManager.FINGERPRINT_ERROR_CANCELED, "blah"); - - verify(mFingerprintManager, times(2)).authenticate(any(), any(), - any(), any(), anyInt()); - } } From a80e4c070b223c4d9585b8d3c76c9777ea526f93 Mon Sep 17 00:00:00 2001 From: Bill Yi Date: Tue, 25 Jun 2024 15:19:35 -0700 Subject: [PATCH 07/18] Import translations. DO NOT MERGE ANYWHERE Auto-generated-cl: translation import Change-Id: Ide9ec08e53c696dba6970cae1ea0600641ae3eb2 --- res/values-fa/arrays.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/res/values-fa/arrays.xml b/res/values-fa/arrays.xml index 53b5cd3d31b..f2418170dd6 100644 --- a/res/values-fa/arrays.xml +++ b/res/values-fa/arrays.xml @@ -195,9 +195,9 @@ "فوکوس صدا" "میزان کنترل" "میزان صدا" - "حجم حلقه" + "صدای زنگ" "میزان صدای رسانه" - "میزان صدای زنگ ساعت" + "صدای زنگ هشدار" "میزان صدای اعلان" "میزان صدای بلوتوث" "بیدار باش" @@ -262,9 +262,9 @@ "فوکوس صدا" "میزان صدای اصلی" "میزان صدای مکالمه" - "میزان صدای زنگ" + "صدای زنگ" "میزان صدای رسانه" - "میزان صدای زنگ ساعت" + "صدای زنگ هشدار" "میزان صدای اعلان" "میزان صدای بلوتوث" "بیدار باش" From 75509bd06bce47f22bcee2ea05ae986b049282ea Mon Sep 17 00:00:00 2001 From: tomhsu Date: Mon, 24 Jun 2024 05:55:34 +0000 Subject: [PATCH 08/18] Avoid to change preference UI content from tapping outside of dialog. Flag: EXEMPT bug fix Fix: 335763360 Test: atest passed. Test: Manual test passed. Change-Id: Iec5e98f74f0009ab2d3bc21bc590229514192f93 --- .../spa/network/SimOnboardingLabelSim.kt | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/com/android/settings/spa/network/SimOnboardingLabelSim.kt b/src/com/android/settings/spa/network/SimOnboardingLabelSim.kt index 64667318d5a..f78808f5628 100644 --- a/src/com/android/settings/spa/network/SimOnboardingLabelSim.kt +++ b/src/com/android/settings/spa/network/SimOnboardingLabelSim.kt @@ -81,24 +81,28 @@ private fun LabelSimPreference( onboardingService: SimOnboardingService, subInfo: SubscriptionInfo, ) { - val originalSimCarrierName = subInfo.displayName.toString() - var titleSimName by remember { - mutableStateOf(onboardingService.getSubscriptionInfoDisplayName(subInfo)) + val currentSimName = onboardingService.getSubscriptionInfoDisplayName(subInfo) + var prefTitle by remember { + mutableStateOf(currentSimName) + } + var dialogInputContent by remember { + mutableStateOf(currentSimName) } val phoneNumber = phoneNumber(subInfo) val alertDialogPresenter = rememberAlertDialogPresenter( confirmButton = AlertDialogButton( stringResource(R.string.mobile_network_sim_name_rename), - titleSimName.isNotBlank() + dialogInputContent.isNotBlank() ) { onboardingService.addItemForRenaming( - subInfo, if (titleSimName.isEmpty()) originalSimCarrierName else titleSimName + subInfo, dialogInputContent ) + prefTitle = dialogInputContent }, dismissButton = AlertDialogButton( stringResource(R.string.cancel), ) { - titleSimName = onboardingService.getSubscriptionInfoDisplayName(subInfo) + // Do nothing }, title = stringResource(R.string.sim_onboarding_label_sim_dialog_title), text = { @@ -107,17 +111,19 @@ private fun LabelSimPreference( modifier = Modifier.padding(bottom = SettingsDimension.itemPaddingVertical) ) SettingsOutlinedTextField( - value = titleSimName, + value = dialogInputContent, label = stringResource(R.string.sim_onboarding_label_sim_dialog_label), - placeholder = {Text(text = originalSimCarrierName)}, - modifier = Modifier.fillMaxWidth().testTag("contentInput") + placeholder = {Text(text = subInfo.displayName.toString())}, + modifier = Modifier + .fillMaxWidth() + .testTag("contentInput") ) { - titleSimName = it + dialogInputContent = it } }, ) Preference(object : PreferenceModel { - override val title = titleSimName + override val title = prefTitle override val summary = { phoneNumber.value ?: "" } override val onClick = alertDialogPresenter::open }) From edc72a9b0fc2825cf5cb2ee87a1c1d25e9647ba0 Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Tue, 25 Jun 2024 11:42:22 +0800 Subject: [PATCH 09/18] SubscriptionRepository.activeSubscriptionIdListFlow Bug: 328293508 Flag: EXEMPT refactor Test: manual - on Mobile Settings Test: unit test Change-Id: I63a86569f4fa3a27bd38d9853f6141890d26b881 --- .../network/telephony/CallStateRepository.kt | 15 ++++--- .../telephony/SubscriptionRepository.kt | 43 ++++++++++++------- .../wificalling/CrossSimCallingViewModel.kt | 10 ++--- .../telephony/CallStateRepositoryTest.kt | 17 +++----- .../telephony/SubscriptionRepositoryTest.kt | 15 ++++++- 5 files changed, 59 insertions(+), 41 deletions(-) diff --git a/src/com/android/settings/network/telephony/CallStateRepository.kt b/src/com/android/settings/network/telephony/CallStateRepository.kt index 4b6cdc83975..e5a21bf2292 100644 --- a/src/com/android/settings/network/telephony/CallStateRepository.kt +++ b/src/com/android/settings/network/telephony/CallStateRepository.kt @@ -25,14 +25,17 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.onEach @OptIn(ExperimentalCoroutinesApi::class) -class CallStateRepository(private val context: Context) { - private val subscriptionManager = context.requireSubscriptionManager() +class CallStateRepository( + private val context: Context, + private val subscriptionRepository: SubscriptionRepository = SubscriptionRepository(context), +) { /** Flow for call state of given [subId]. */ fun callStateFlow(subId: Int): Flow = context.telephonyCallbackFlow(subId) { @@ -48,9 +51,8 @@ class CallStateRepository(private val context: Context) { * * @return true if any active subscription's call state is not idle. */ - fun isInCallFlow(): Flow = context.subscriptionsChangedFlow() - .flatMapLatest { - val subIds = subscriptionManager.activeSubscriptionIdList + fun isInCallFlow(): Flow = subscriptionRepository.activeSubscriptionIdListFlow() + .flatMapLatest { subIds -> if (subIds.isEmpty()) { flowOf(false) } else { @@ -59,9 +61,10 @@ class CallStateRepository(private val context: Context) { } } } + .distinctUntilChanged() .conflate() - .flowOn(Dispatchers.Default) .onEach { Log.d(TAG, "isInCallFlow: $it") } + .flowOn(Dispatchers.Default) private companion object { private const val TAG = "CallStateRepository" diff --git a/src/com/android/settings/network/telephony/SubscriptionRepository.kt b/src/com/android/settings/network/telephony/SubscriptionRepository.kt index 3ee854843fc..c95231041d0 100644 --- a/src/com/android/settings/network/telephony/SubscriptionRepository.kt +++ b/src/com/android/settings/network/telephony/SubscriptionRepository.kt @@ -29,6 +29,7 @@ import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn @@ -68,6 +69,30 @@ class SubscriptionRepository(private val context: Context) { } fun canDisablePhysicalSubscription() = subscriptionManager.canDisablePhysicalSubscription() + + /** Flow for subscriptions changes. */ + fun subscriptionsChangedFlow() = callbackFlow { + val listener = object : SubscriptionManager.OnSubscriptionsChangedListener() { + override fun onSubscriptionsChanged() { + trySend(Unit) + } + } + + subscriptionManager.addOnSubscriptionsChangedListener( + Dispatchers.Default.asExecutor(), + listener, + ) + + awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(listener) } + }.conflate().onEach { Log.d(TAG, "subscriptions changed") }.flowOn(Dispatchers.Default) + + /** Flow of active subscription ids. */ + fun activeSubscriptionIdListFlow(): Flow> = context.subscriptionsChangedFlow() + .map { subscriptionManager.activeSubscriptionIdList.sorted() } + .distinctUntilChanged() + .conflate() + .onEach { Log.d(TAG, "activeSubscriptionIdList: $it") } + .flowOn(Dispatchers.Default) } val Context.subscriptionManager: SubscriptionManager? @@ -79,22 +104,8 @@ fun Context.phoneNumberFlow(subscriptionInfo: SubscriptionInfo) = subscriptionsC SubscriptionUtil.getBidiFormattedPhoneNumber(this, subscriptionInfo) }.filterNot { it.isNullOrEmpty() }.flowOn(Dispatchers.Default) -fun Context.subscriptionsChangedFlow() = callbackFlow { - val subscriptionManager = requireSubscriptionManager() - - val listener = object : SubscriptionManager.OnSubscriptionsChangedListener() { - override fun onSubscriptionsChanged() { - trySend(Unit) - } - } - - subscriptionManager.addOnSubscriptionsChangedListener( - Dispatchers.Default.asExecutor(), - listener, - ) - - awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(listener) } -}.conflate().onEach { Log.d(TAG, "subscriptions changed") }.flowOn(Dispatchers.Default) +fun Context.subscriptionsChangedFlow(): Flow = + SubscriptionRepository(this).subscriptionsChangedFlow() /** * Return a list of subscriptions that are available and visible to the user. diff --git a/src/com/android/settings/network/telephony/wificalling/CrossSimCallingViewModel.kt b/src/com/android/settings/network/telephony/wificalling/CrossSimCallingViewModel.kt index fb0bd82a7d4..170af548ad1 100644 --- a/src/com/android/settings/network/telephony/wificalling/CrossSimCallingViewModel.kt +++ b/src/com/android/settings/network/telephony/wificalling/CrossSimCallingViewModel.kt @@ -25,10 +25,9 @@ import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import com.android.settings.R import com.android.settings.network.telephony.MobileDataRepository +import com.android.settings.network.telephony.SubscriptionRepository import com.android.settings.network.telephony.ims.ImsMmTelRepositoryImpl -import com.android.settings.network.telephony.requireSubscriptionManager import com.android.settings.network.telephony.safeGetConfig -import com.android.settings.network.telephony.subscriptionsChangedFlow import com.android.settings.network.telephony.telephonyManager import com.android.settings.overlay.FeatureFactory.Companion.featureFactory import kotlinx.coroutines.Dispatchers @@ -48,7 +47,7 @@ class CrossSimCallingViewModel( private val application: Application, ) : AndroidViewModel(application) { - private val subscriptionManager = application.requireSubscriptionManager() + private val subscriptionRepository = SubscriptionRepository(application) private val carrierConfigManager = application.getSystemService(CarrierConfigManager::class.java)!! private val scope = viewModelScope + Dispatchers.Default @@ -59,9 +58,8 @@ class CrossSimCallingViewModel( init { val resources = application.resources if (resources.getBoolean(R.bool.config_auto_data_switch_enables_cross_sim_calling)) { - application.subscriptionsChangedFlow() - .flatMapLatest { - val activeSubIds = subscriptionManager.activeSubscriptionIdList.toList() + subscriptionRepository.activeSubscriptionIdListFlow() + .flatMapLatest { activeSubIds -> merge( activeSubIds.anyMobileDataEnableChangedFlow(), updateChannel.receiveAsFlow(), diff --git a/tests/spa_unit/src/com/android/settings/network/telephony/CallStateRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/network/telephony/CallStateRepositoryTest.kt index 55d520fd214..d192eb490b1 100644 --- a/tests/spa_unit/src/com/android/settings/network/telephony/CallStateRepositoryTest.kt +++ b/tests/spa_unit/src/com/android/settings/network/telephony/CallStateRepositoryTest.kt @@ -17,7 +17,6 @@ package com.android.settings.network.telephony import android.content.Context -import android.telephony.SubscriptionManager import android.telephony.TelephonyCallback import android.telephony.TelephonyManager import androidx.test.core.app.ApplicationProvider @@ -27,6 +26,7 @@ import com.android.settingslib.spa.testutils.toListWithTimeout import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.async import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking import org.junit.Test import org.junit.runner.RunWith @@ -49,20 +49,15 @@ class CallStateRepositoryTest { } } - private val mockSubscriptionManager = mock { - on { activeSubscriptionIdList } doReturn intArrayOf(SUB_ID) - on { addOnSubscriptionsChangedListener(any(), any()) } doAnswer { - val listener = it.arguments[1] as SubscriptionManager.OnSubscriptionsChangedListener - listener.onSubscriptionsChanged() - } + private val mockSubscriptionRepository = mock { + on { activeSubscriptionIdListFlow() } doReturn flowOf(listOf(SUB_ID)) } private val context: Context = spy(ApplicationProvider.getApplicationContext()) { on { getSystemService(TelephonyManager::class.java) } doReturn mockTelephonyManager - on { subscriptionManager } doReturn mockSubscriptionManager } - private val repository = CallStateRepository(context) + private val repository = CallStateRepository(context, mockSubscriptionRepository) @Test fun callStateFlow_initial_sendInitialState() = runBlocking { @@ -89,8 +84,8 @@ class CallStateRepositoryTest { @Test fun isInCallFlow_noActiveSubscription() = runBlocking { - mockSubscriptionManager.stub { - on { activeSubscriptionIdList } doReturn intArrayOf() + mockSubscriptionRepository.stub { + on { activeSubscriptionIdListFlow() } doReturn flowOf(emptyList()) } val isInCall = repository.isInCallFlow().firstWithTimeoutOrNull() diff --git a/tests/spa_unit/src/com/android/settings/network/telephony/SubscriptionRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/network/telephony/SubscriptionRepositoryTest.kt index e233fa428a5..75c9aa14456 100644 --- a/tests/spa_unit/src/com/android/settings/network/telephony/SubscriptionRepositoryTest.kt +++ b/tests/spa_unit/src/com/android/settings/network/telephony/SubscriptionRepositoryTest.kt @@ -77,7 +77,7 @@ class SubscriptionRepositoryTest { @Test fun subscriptionsChangedFlow_hasInitialValue() = runBlocking { - val initialValue = context.subscriptionsChangedFlow().firstWithTimeoutOrNull() + val initialValue = repository.subscriptionsChangedFlow().firstWithTimeoutOrNull() assertThat(initialValue).isSameInstanceAs(Unit) } @@ -85,7 +85,7 @@ class SubscriptionRepositoryTest { @Test fun subscriptionsChangedFlow_changed() = runBlocking { val listDeferred = async { - context.subscriptionsChangedFlow().toListWithTimeout() + repository.subscriptionsChangedFlow().toListWithTimeout() } delay(100) @@ -94,6 +94,17 @@ class SubscriptionRepositoryTest { assertThat(listDeferred.await()).hasSize(2) } + @Test + fun activeSubscriptionIdListFlow(): Unit = runBlocking { + mockSubscriptionManager.stub { + on { activeSubscriptionIdList } doReturn intArrayOf(SUB_ID_IN_SLOT_0) + } + + val activeSubIds = repository.activeSubscriptionIdListFlow().firstWithTimeoutOrNull() + + assertThat(activeSubIds).containsExactly(SUB_ID_IN_SLOT_0) + } + @Test fun getSelectableSubscriptionInfoList_sortedBySimSlotIndex() { mockSubscriptionManager.stub { From b70c80571790df6f58e3fecaeb0b7c7ecc0b656f Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Mon, 24 Jun 2024 16:52:01 +0800 Subject: [PATCH 10/18] Fix unable to erase eSIM Before this change, - eSIM will be erased twice, one with result callback and one without result callback. - During reset, ResetNetworkConfirm could interrupted by subscription invalid event, which happens during reset. After this change, - eSIM will be erased only once, result callback is registered separately. - Explicit exit the page when reset finish, and ignore the subscription invalid event after reset started. Bug: 328293508 Flag: EXEMPT bug fix Test: manual - dry run the reset Test: ResetNetworkConfirmTest Change-Id: I51395a556b1c8775192d5897a87f13046c042578 --- src/com/android/settings/ResetNetwork.java | 1 + .../android/settings/ResetNetworkConfirm.java | 247 ------------------ .../settings/ResetSubscriptionContract.java | 157 ----------- .../network/ResetNetworkOperationBuilder.java | 27 +- .../system/reset/ResetNetworkConfirm.kt | 217 +++++++++++++++ .../settings/ResetNetworkConfirmTest.java | 124 --------- .../system/reset/ResetNetworkConfirmTest.kt | 79 ++++++ .../ResetSubscriptionContractTest.java | 109 -------- 8 files changed, 312 insertions(+), 649 deletions(-) delete mode 100644 src/com/android/settings/ResetNetworkConfirm.java delete mode 100644 src/com/android/settings/ResetSubscriptionContract.java create mode 100644 src/com/android/settings/system/reset/ResetNetworkConfirm.kt delete mode 100644 tests/robotests/src/com/android/settings/ResetNetworkConfirmTest.java create mode 100644 tests/spa_unit/src/com/android/settings/system/reset/ResetNetworkConfirmTest.kt delete mode 100644 tests/unit/src/com/android/settings/ResetSubscriptionContractTest.java diff --git a/src/com/android/settings/ResetNetwork.java b/src/com/android/settings/ResetNetwork.java index c91ef5cdb93..c1e3494d621 100644 --- a/src/com/android/settings/ResetNetwork.java +++ b/src/com/android/settings/ResetNetwork.java @@ -52,6 +52,7 @@ import com.android.settings.network.SubscriptionUtil; import com.android.settings.network.telephony.EuiccRacConnectivityDialogActivity; import com.android.settings.password.ChooseLockSettingsHelper; import com.android.settings.password.ConfirmLockPattern; +import com.android.settings.system.reset.ResetNetworkConfirm; import com.android.settingslib.development.DevelopmentSettingsEnabler; import java.util.ArrayList; diff --git a/src/com/android/settings/ResetNetworkConfirm.java b/src/com/android/settings/ResetNetworkConfirm.java deleted file mode 100644 index c707b96a328..00000000000 --- a/src/com/android/settings/ResetNetworkConfirm.java +++ /dev/null @@ -1,247 +0,0 @@ -/* - * Copyright (C) 2015 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; - -import android.app.Activity; -import android.app.ProgressDialog; -import android.app.settings.SettingsEnums; -import android.content.Context; -import android.os.AsyncTask; -import android.os.Bundle; -import android.os.Looper; -import android.telephony.SubscriptionManager; -import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.TextView; -import android.widget.Toast; - -import androidx.annotation.VisibleForTesting; -import androidx.appcompat.app.AlertDialog; - -import com.android.settings.core.InstrumentedFragment; -import com.android.settings.network.ResetNetworkOperationBuilder; -import com.android.settings.network.ResetNetworkRestrictionViewBuilder; - -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * Confirm and execute a reset of the network settings to a clean "just out of the box" - * state. Multiple confirmations are required: first, a general "are you sure - * you want to do this?" prompt, followed by a keyguard pattern trace if the user - * has defined one, followed by a final strongly-worded "THIS WILL RESET EVERYTHING" - * prompt. If at any time the phone is allowed to go to sleep, is - * locked, et cetera, then the confirmation sequence is abandoned. - * - * This is the confirmation screen. - */ -public class ResetNetworkConfirm extends InstrumentedFragment { - private static final String TAG = "ResetNetworkConfirm"; - - @VisibleForTesting View mContentView; - @VisibleForTesting ResetNetworkTask mResetNetworkTask; - @VisibleForTesting Activity mActivity; - @VisibleForTesting ResetNetworkRequest mResetNetworkRequest; - private ProgressDialog mProgressDialog; - private AlertDialog mAlertDialog; - @VisibleForTesting ResetSubscriptionContract mResetSubscriptionContract; - private OnSubscriptionsChangedListener mSubscriptionsChangedListener; - - /** - * Async task used to do all reset task. If error happens during - * erasing eSIM profiles or timeout, an error msg is shown. - */ - private class ResetNetworkTask extends AsyncTask { - private static final String TAG = "ResetNetworkTask"; - - private final Context mContext; - - ResetNetworkTask(Context context) { - mContext = context; - } - - @Override - protected Boolean doInBackground(Void... params) { - final AtomicBoolean resetEsimSuccess = new AtomicBoolean(true); - - String resetEsimPackageName = mResetNetworkRequest.getResetEsimPackageName(); - ResetNetworkOperationBuilder builder = mResetNetworkRequest - .toResetNetworkOperationBuilder(mContext, Looper.getMainLooper()); - if (resetEsimPackageName != null) { - // Override reset eSIM option for the result of reset operation - builder = builder.resetEsim(resetEsimPackageName, - success -> { resetEsimSuccess.set(success); } - ); - } - builder.build().run(); - - boolean isResetSucceed = resetEsimSuccess.get(); - Log.d(TAG, "network factoryReset complete. succeeded: " - + String.valueOf(isResetSucceed)); - return isResetSucceed; - } - - @Override - protected void onPostExecute(Boolean succeeded) { - if (mProgressDialog != null && mProgressDialog.isShowing()) { - mProgressDialog.dismiss(); - } - - if (succeeded) { - Toast.makeText(mContext, R.string.reset_network_complete_toast, Toast.LENGTH_SHORT) - .show(); - } else { - mAlertDialog = new AlertDialog.Builder(mContext) - .setTitle(R.string.reset_esim_error_title) - .setMessage(R.string.reset_esim_error_msg) - .setPositiveButton(android.R.string.ok, null /* listener */) - .show(); - } - } - } - - /** - * The user has gone through the multiple confirmation, so now we go ahead - * and reset the network settings to its factory-default state. - */ - @VisibleForTesting - Button.OnClickListener mFinalClickListener = new Button.OnClickListener() { - - @Override - public void onClick(View v) { - if (Utils.isMonkeyRunning()) { - return; - } - - // abandon execution if subscription no longer active - Integer subId = mResetSubscriptionContract.getAnyMissingSubscriptionId(); - if (subId != null) { - Log.w(TAG, "subId " + subId + " no longer active"); - getActivity().onBackPressed(); - return; - } - - // Should dismiss the progress dialog firstly if it is showing - // Or not the progress dialog maybe not dismissed in fast clicking. - if (mProgressDialog != null && mProgressDialog.isShowing()) { - mProgressDialog.dismiss(); - } - - mProgressDialog = getProgressDialog(mActivity); - mProgressDialog.show(); - - mResetNetworkTask = new ResetNetworkTask(mActivity); - mResetNetworkTask.execute(); - } - }; - - private ProgressDialog getProgressDialog(Context context) { - final ProgressDialog progressDialog = new ProgressDialog(context); - progressDialog.setIndeterminate(true); - progressDialog.setCancelable(false); - progressDialog.setMessage( - context.getString(R.string.main_clear_progress_text)); - return progressDialog; - } - - /** - * Configure the UI for the final confirmation interaction - */ - private void establishFinalConfirmationState() { - mContentView.findViewById(R.id.execute_reset_network) - .setOnClickListener(mFinalClickListener); - } - - @VisibleForTesting - void setSubtitle() { - if (mResetNetworkRequest.getResetEsimPackageName() != null) { - ((TextView) mContentView.findViewById(R.id.reset_network_confirm)) - .setText(R.string.reset_network_final_desc_esim); - } - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - View view = (new ResetNetworkRestrictionViewBuilder(mActivity)).build(); - if (view != null) { - mResetSubscriptionContract.close(); - Log.w(TAG, "Access deny."); - return view; - } - mContentView = inflater.inflate(R.layout.reset_network_confirm, null); - establishFinalConfirmationState(); - setSubtitle(); - return mContentView; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - Bundle args = getArguments(); - if (args == null) { - args = savedInstanceState; - } - mResetNetworkRequest = new ResetNetworkRequest(args); - - mActivity = getActivity(); - - mResetSubscriptionContract = new ResetSubscriptionContract(getContext(), - mResetNetworkRequest) { - @Override - public void onSubscriptionInactive(int subscriptionId) { - // close UI if subscription no longer active - Log.w(TAG, "subId " + subscriptionId + " no longer active."); - getActivity().onBackPressed(); - } - }; - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - mResetNetworkRequest.writeIntoBundle(outState); - } - - @Override - public void onDestroy() { - if (mResetNetworkTask != null) { - mResetNetworkTask.cancel(true /* mayInterruptIfRunning */); - mResetNetworkTask = null; - } - if (mResetSubscriptionContract != null) { - mResetSubscriptionContract.close(); - mResetSubscriptionContract = null; - } - if (mProgressDialog != null) { - mProgressDialog.dismiss(); - } - if (mAlertDialog != null) { - mAlertDialog.dismiss(); - } - super.onDestroy(); - } - - @Override - public int getMetricsCategory() { - return SettingsEnums.RESET_NETWORK_CONFIRM; - } -} diff --git a/src/com/android/settings/ResetSubscriptionContract.java b/src/com/android/settings/ResetSubscriptionContract.java deleted file mode 100644 index 528a16def5a..00000000000 --- a/src/com/android/settings/ResetSubscriptionContract.java +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright (C) 2022 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; - -import android.content.Context; -import android.telephony.SubscriptionManager; -import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener; -import android.util.Log; - -import androidx.annotation.VisibleForTesting; - -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.stream.IntStream; - -/** - * A Class monitoring the availability of subscription IDs provided within reset request. - * - * This is to detect the situation when user changing SIM card during the presenting of - * confirmation UI. - */ -public class ResetSubscriptionContract implements AutoCloseable { - private static final String TAG = "ResetSubscriptionContract"; - - private final Context mContext; - private ExecutorService mExecutorService; - private final int [] mResetSubscriptionIds; - @VisibleForTesting - protected OnSubscriptionsChangedListener mSubscriptionsChangedListener; - private AtomicBoolean mSubscriptionsUpdateNotify = new AtomicBoolean(); - - /** - * Constructor - * @param context Context - * @param resetRequest the request object for perform network reset operation. - */ - public ResetSubscriptionContract(Context context, ResetNetworkRequest resetRequest) { - mContext = context; - // Only keeps specific subscription ID required to perform reset operation - IntStream subIdStream = IntStream.of( - resetRequest.getResetTelephonyAndNetworkPolicyManager(), - resetRequest.getResetApnSubId(), resetRequest.getResetImsSubId()); - mResetSubscriptionIds = subIdStream.sorted().distinct() - .filter(id -> SubscriptionManager.isUsableSubscriptionId(id)) - .toArray(); - - if (mResetSubscriptionIds.length <= 0) { - return; - } - - // Monitoring callback through background thread - mExecutorService = Executors.newSingleThreadExecutor(); - startMonitorSubscriptionChange(); - } - - /** - * A method for detecting if there's any subscription under monitor no longer active. - * @return subscription ID which is no longer active. - */ - public Integer getAnyMissingSubscriptionId() { - if (mResetSubscriptionIds.length <= 0) { - return null; - } - SubscriptionManager mgr = getSubscriptionManager(); - if (mgr == null) { - Log.w(TAG, "Fail to access subscription manager"); - return mResetSubscriptionIds[0]; - } - for (int idx = 0; idx < mResetSubscriptionIds.length; idx++) { - int subId = mResetSubscriptionIds[idx]; - if (mgr.getActiveSubscriptionInfo(subId) == null) { - Log.w(TAG, "SubId " + subId + " no longer active."); - return subId; - } - } - return null; - } - - /** - * Async callback when detecting if there's any subscription under monitor no longer active. - * @param subscriptionId subscription ID which is no longer active. - */ - public void onSubscriptionInactive(int subscriptionId) {} - - @VisibleForTesting - protected SubscriptionManager getSubscriptionManager() { - return mContext.getSystemService(SubscriptionManager.class); - } - - @VisibleForTesting - protected OnSubscriptionsChangedListener getChangeListener() { - return new OnSubscriptionsChangedListener() { - @Override - public void onSubscriptionsChanged() { - /** - * Reducing the processing time on main UI thread through a flag. - * Once flag get into false, which means latest callback has been - * processed. - */ - mSubscriptionsUpdateNotify.set(true); - - // Back to main UI thread - mContext.getMainExecutor().execute(() -> { - // Remove notifications and perform checking. - if (mSubscriptionsUpdateNotify.getAndSet(false)) { - Integer subId = getAnyMissingSubscriptionId(); - if (subId != null) { - onSubscriptionInactive(subId); - } - } - }); - } - }; - } - - private void startMonitorSubscriptionChange() { - SubscriptionManager mgr = getSubscriptionManager(); - if (mgr == null) { - return; - } - // update monitor listener - mSubscriptionsChangedListener = getChangeListener(); - - mgr.addOnSubscriptionsChangedListener( - mExecutorService, mSubscriptionsChangedListener); - } - - // Implementation of AutoCloseable - public void close() { - if (mExecutorService == null) { - return; - } - // Stop monitoring subscription change - SubscriptionManager mgr = getSubscriptionManager(); - if (mgr != null) { - mgr.removeOnSubscriptionsChangedListener(mSubscriptionsChangedListener); - } - // Release Executor - mExecutorService.shutdownNow(); - mExecutorService = null; - } -} diff --git a/src/com/android/settings/network/ResetNetworkOperationBuilder.java b/src/com/android/settings/network/ResetNetworkOperationBuilder.java index 47c06d4480d..dfcca520255 100644 --- a/src/com/android/settings/network/ResetNetworkOperationBuilder.java +++ b/src/com/android/settings/network/ResetNetworkOperationBuilder.java @@ -65,6 +65,8 @@ public class ResetNetworkOperationBuilder { private Context mContext; private List mResetSequence = new ArrayList(); + @Nullable + private Consumer mResetEsimResultCallback = null; /** * Constructor of builder. @@ -129,31 +131,32 @@ public class ResetNetworkOperationBuilder { } /** - * Append a step of resetting E-SIM. - * @param callerPackage package name of caller + * Append a result callback of resetting E-SIM. + * @param resultCallback a callback dealing with result of resetting eSIM * @return this */ - public ResetNetworkOperationBuilder resetEsim(String callerPackage) { - resetEsim(callerPackage, null); + public ResetNetworkOperationBuilder resetEsimResultCallback(Consumer resultCallback) { + mResetEsimResultCallback = resultCallback; return this; } /** * Append a step of resetting E-SIM. * @param callerPackage package name of caller - * @param resultCallback a Consumer dealing with result of resetting eSIM * @return this */ - public ResetNetworkOperationBuilder resetEsim(String callerPackage, - Consumer resultCallback) { + public ResetNetworkOperationBuilder resetEsim(String callerPackage) { Runnable runnable = () -> { long startTime = SystemClock.elapsedRealtime(); - if (!DRY_RUN) { - Boolean wipped = RecoverySystem.wipeEuiccData(mContext, callerPackage); - if (resultCallback != null) { - resultCallback.accept(wipped); - } + boolean wipped; + if (DRY_RUN) { + wipped = true; + } else { + wipped = RecoverySystem.wipeEuiccData(mContext, callerPackage); + } + if (mResetEsimResultCallback != null) { + mResetEsimResultCallback.accept(wipped); } long endTime = SystemClock.elapsedRealtime(); diff --git a/src/com/android/settings/system/reset/ResetNetworkConfirm.kt b/src/com/android/settings/system/reset/ResetNetworkConfirm.kt new file mode 100644 index 00000000000..34b9909e55e --- /dev/null +++ b/src/com/android/settings/system/reset/ResetNetworkConfirm.kt @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2024 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.system.reset + +import android.app.ProgressDialog +import android.app.settings.SettingsEnums +import android.os.Bundle +import android.os.Looper +import android.telephony.SubscriptionManager +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import android.widget.Toast +import androidx.annotation.VisibleForTesting +import androidx.appcompat.app.AlertDialog +import androidx.lifecycle.lifecycleScope +import com.android.settings.R +import com.android.settings.ResetNetworkRequest +import com.android.settings.Utils +import com.android.settings.core.InstrumentedFragment +import com.android.settings.network.ResetNetworkRestrictionViewBuilder +import com.android.settings.network.telephony.SubscriptionRepository +import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** + * Confirm and execute a reset of the network settings to a clean "just out of the box" state. + * Multiple confirmations are required: first, a general "are you sure you want to do this?" prompt, + * followed by a keyguard pattern trace if the user has defined one, followed by a final + * strongly-worded "THIS WILL RESET EVERYTHING" prompt. If at any time the phone is allowed to go to + * sleep, is locked, et cetera, then the confirmation sequence is abandoned. + * + * This is the confirmation screen. + */ +class ResetNetworkConfirm : InstrumentedFragment() { + @VisibleForTesting lateinit var resetNetworkRequest: ResetNetworkRequest + private var progressDialog: ProgressDialog? = null + private var alertDialog: AlertDialog? = null + private var resetStarted = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + Log.d(TAG, "onCreate: $arguments") + resetNetworkRequest = ResetNetworkRequest(arguments) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + val view = ResetNetworkRestrictionViewBuilder(requireActivity()).build() + if (view != null) { + Log.w(TAG, "Access deny.") + return view + } + return inflater.inflate(R.layout.reset_network_confirm, null).apply { + establishFinalConfirmationState() + setSubtitle() + } + } + + /** Configure the UI for the final confirmation interaction */ + private fun View.establishFinalConfirmationState() { + requireViewById(R.id.execute_reset_network).setOnClickListener { + if (!Utils.isMonkeyRunning() && !resetStarted) { + resetStarted = true + viewLifecycleOwner.lifecycleScope.launch { onResetClicked() } + } + } + } + + private fun View.setSubtitle() { + if (resetNetworkRequest.resetEsimPackageName != null) { + requireViewById(R.id.reset_network_confirm) + .setText(R.string.reset_network_final_desc_esim) + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + invalidSubIdFlow().collectLatestWithLifecycle(viewLifecycleOwner) { invalidSubId -> + // Reset process could triage this callback, so if reset has started, ignore the event. + if (!resetStarted) { + Log.w(TAG, "subId $invalidSubId no longer active.") + requireActivity().finish() + } + } + } + + /** + * Monitor the sub ids in the request, if any sub id becomes inactive, the request is abandoned. + */ + private fun invalidSubIdFlow(): Flow { + val subIdsInRequest = + listOf( + resetNetworkRequest.resetTelephonyAndNetworkPolicyManager, + resetNetworkRequest.resetApnSubId, + resetNetworkRequest.resetImsSubId, + ) + .distinct() + .filter(SubscriptionManager::isUsableSubscriptionId) + + if (subIdsInRequest.isEmpty()) return emptyFlow() + + return SubscriptionRepository(requireContext()) + .activeSubscriptionIdListFlow() + .mapNotNull { activeSubIds -> subIdsInRequest.firstOrNull { it !in activeSubIds } } + .conflate() + .flowOn(Dispatchers.Default) + } + + /** + * The user has gone through the multiple confirmation, so now we go ahead and reset the network + * settings to its factory-default state. + */ + @VisibleForTesting + suspend fun onResetClicked() { + showProgressDialog() + resetNetwork() + } + + private fun showProgressDialog() { + progressDialog = + ProgressDialog(requireContext()).apply { + isIndeterminate = true + setCancelable(false) + setMessage(requireContext().getString(R.string.main_clear_progress_text)) + show() + } + } + + private fun dismissProgressDialog() { + progressDialog?.let { progressDialog -> + if (progressDialog.isShowing) { + progressDialog.dismiss() + } + } + } + + /** + * Do all reset task. + * + * If error happens during erasing eSIM profiles or timeout, an error msg is shown. + */ + private suspend fun resetNetwork() { + var resetEsimSuccess = true + + withContext(Dispatchers.Default) { + val builder = + resetNetworkRequest.toResetNetworkOperationBuilder( + requireContext(), Looper.getMainLooper()) + resetNetworkRequest.resetEsimPackageName?.let { resetEsimPackageName -> + builder.resetEsim(resetEsimPackageName) + builder.resetEsimResultCallback { resetEsimSuccess = it } + } + builder.build().run() + } + + Log.d(TAG, "network factoryReset complete. succeeded: $resetEsimSuccess") + onResetFinished(resetEsimSuccess) + } + + private fun onResetFinished(resetEsimSuccess: Boolean) { + dismissProgressDialog() + val activity = requireActivity() + + if (!resetEsimSuccess) { + alertDialog = + AlertDialog.Builder(activity) + .setTitle(R.string.reset_esim_error_title) + .setMessage(R.string.reset_esim_error_msg) + .setPositiveButton(android.R.string.ok, /* listener= */ null) + .show() + } else { + Toast.makeText(activity, R.string.reset_network_complete_toast, Toast.LENGTH_SHORT) + .show() + } + activity.finish() + } + + override fun onDestroy() { + progressDialog?.dismiss() + alertDialog?.dismiss() + super.onDestroy() + } + + override fun getMetricsCategory() = SettingsEnums.RESET_NETWORK_CONFIRM + + private companion object { + const val TAG = "ResetNetworkConfirm" + } +} diff --git a/tests/robotests/src/com/android/settings/ResetNetworkConfirmTest.java b/tests/robotests/src/com/android/settings/ResetNetworkConfirmTest.java deleted file mode 100644 index ea6559c7416..00000000000 --- a/tests/robotests/src/com/android/settings/ResetNetworkConfirmTest.java +++ /dev/null @@ -1,124 +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; - -import static com.google.common.truth.Truth.assertThat; - -import static org.mockito.Mockito.spy; - -import android.view.LayoutInflater; -import android.widget.TextView; - -import androidx.fragment.app.FragmentActivity; - -import com.android.settings.testutils.shadow.ShadowBluetoothAdapter; -import com.android.settings.testutils.shadow.ShadowRecoverySystem; - -import org.junit.After; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnit; -import org.mockito.junit.MockitoRule; -import org.robolectric.Robolectric; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.android.util.concurrent.PausedExecutorService; -import org.robolectric.annotation.Config; -import org.robolectric.shadows.ShadowLooper; -import org.robolectric.shadows.ShadowPausedAsyncTask; - -@RunWith(RobolectricTestRunner.class) -@Config(shadows = {ShadowRecoverySystem.class, ShadowBluetoothAdapter.class}) -public class ResetNetworkConfirmTest { - @Rule - public final MockitoRule mMockitoRule = MockitoJUnit.rule(); - - private static final String TEST_PACKAGE = "com.android.settings"; - - private FragmentActivity mActivity; - - @Mock - private ResetNetworkConfirm mResetNetworkConfirm; - private PausedExecutorService mExecutorService; - - @Before - public void setUp() { - mExecutorService = new PausedExecutorService(); - ShadowPausedAsyncTask.overrideExecutor(mExecutorService); - mResetNetworkConfirm = new ResetNetworkConfirm(); - mActivity = spy(Robolectric.setupActivity(FragmentActivity.class)); - mResetNetworkConfirm.mActivity = mActivity; - } - - @After - public void tearDown() { - ShadowRecoverySystem.reset(); - } - - @Test - public void testResetNetworkData_notResetEsim() { - mResetNetworkConfirm.mResetNetworkRequest = - new ResetNetworkRequest(ResetNetworkRequest.RESET_NONE); - mResetNetworkConfirm.mResetSubscriptionContract = - new ResetSubscriptionContract(mActivity, - mResetNetworkConfirm.mResetNetworkRequest) { - @Override - public void onSubscriptionInactive(int subscriptionId) { - mActivity.onBackPressed(); - } - }; - - mResetNetworkConfirm.mFinalClickListener.onClick(null /* View */); - mExecutorService.runAll(); - ShadowLooper.idleMainLooper(); - - assertThat(ShadowRecoverySystem.getWipeEuiccCalledCount()).isEqualTo(0); - } - - @Test - public void setSubtitle_eraseEsim() { - mResetNetworkConfirm.mResetNetworkRequest = - new ResetNetworkRequest(ResetNetworkRequest.RESET_NONE); - mResetNetworkConfirm.mResetNetworkRequest.setResetEsim(TEST_PACKAGE); - - mResetNetworkConfirm.mContentView = - LayoutInflater.from(mActivity).inflate(R.layout.reset_network_confirm, null); - - mResetNetworkConfirm.setSubtitle(); - - assertThat(((TextView) mResetNetworkConfirm.mContentView - .findViewById(R.id.reset_network_confirm)).getText()) - .isEqualTo(mActivity.getString(R.string.reset_network_final_desc_esim)); - } - - @Test - public void setSubtitle_notEraseEsim() { - mResetNetworkConfirm.mResetNetworkRequest = - new ResetNetworkRequest(ResetNetworkRequest.RESET_NONE); - - mResetNetworkConfirm.mContentView = - LayoutInflater.from(mActivity).inflate(R.layout.reset_network_confirm, null); - - mResetNetworkConfirm.setSubtitle(); - - assertThat(((TextView) mResetNetworkConfirm.mContentView - .findViewById(R.id.reset_network_confirm)).getText()) - .isEqualTo(mActivity.getString(R.string.reset_network_final_desc)); - } -} diff --git a/tests/spa_unit/src/com/android/settings/system/reset/ResetNetworkConfirmTest.kt b/tests/spa_unit/src/com/android/settings/system/reset/ResetNetworkConfirmTest.kt new file mode 100644 index 00000000000..4812cfb1545 --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/system/reset/ResetNetworkConfirmTest.kt @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2024 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.system.reset + +import android.content.Context +import android.view.LayoutInflater +import android.widget.TextView +import androidx.fragment.app.testing.launchFragment +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settings.R +import com.android.settings.ResetNetworkRequest +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.runBlocking +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.never +import org.mockito.kotlin.spy +import org.mockito.kotlin.verify + +@RunWith(AndroidJUnit4::class) +class ResetNetworkConfirmTest { + private val context: Context = spy(ApplicationProvider.getApplicationContext()) {} + + private val scenario = launchFragment() + + @Test + fun resetNetworkData_notResetEsim() { + scenario.recreate().onFragment { fragment -> + fragment.resetNetworkRequest = ResetNetworkRequest(ResetNetworkRequest.RESET_NONE) + + runBlocking { fragment.onResetClicked() } + + verify(context, never()).getSystemService(any()) + } + } + + @Test + fun setSubtitle_eraseEsim() { + scenario.onFragment { fragment -> + fragment.resetNetworkRequest = + ResetNetworkRequest(ResetNetworkRequest.RESET_NONE).apply { + setResetEsim(context.packageName) + } + + val view = fragment.onCreateView(LayoutInflater.from(context), null, null) + + assertThat(view.requireViewById(R.id.reset_network_confirm).text) + .isEqualTo(context.getString(R.string.reset_network_final_desc_esim)) + } + } + + @Test + fun setSubtitle_notEraseEsim() { + scenario.onFragment { fragment -> + fragment.resetNetworkRequest = ResetNetworkRequest(ResetNetworkRequest.RESET_NONE) + + val view = fragment.onCreateView(LayoutInflater.from(context), null, null) + + assertThat(view.requireViewById(R.id.reset_network_confirm).text) + .isEqualTo(context.getString(R.string.reset_network_final_desc)) + } + } +} diff --git a/tests/unit/src/com/android/settings/ResetSubscriptionContractTest.java b/tests/unit/src/com/android/settings/ResetSubscriptionContractTest.java deleted file mode 100644 index 4443304d696..00000000000 --- a/tests/unit/src/com/android/settings/ResetSubscriptionContractTest.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright (C) 2022 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; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; - -import android.content.Context; -import android.os.Bundle; -import android.telephony.SubscriptionInfo; -import android.telephony.SubscriptionManager; -import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener; -import androidx.test.core.app.ApplicationProvider; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -@RunWith(AndroidJUnit4.class) -public class ResetSubscriptionContractTest { - - private static final int SUB_ID_1 = 3; - private static final int SUB_ID_2 = 8; - - @Mock - private SubscriptionManager mSubscriptionManager; - @Mock - private OnSubscriptionsChangedListener mOnSubscriptionsChangedListener; - @Mock - private SubscriptionInfo mSubscriptionInfo1; - @Mock - private SubscriptionInfo mSubscriptionInfo2; - - private Context mContext; - private ResetNetworkRequest mRequestArgs; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - - mContext = spy(ApplicationProvider.getApplicationContext()); - mRequestArgs = new ResetNetworkRequest(new Bundle()); - } - - private ResetSubscriptionContract createTestObject() { - return new ResetSubscriptionContract(mContext, mRequestArgs) { - @Override - protected SubscriptionManager getSubscriptionManager() { - return mSubscriptionManager; - } - @Override - protected OnSubscriptionsChangedListener getChangeListener() { - return mOnSubscriptionsChangedListener; - } - }; - } - - @Test - public void getAnyMissingSubscriptionId_returnNull_whenNoSubscriptionChange() { - mRequestArgs.setResetTelephonyAndNetworkPolicyManager(SUB_ID_1); - doReturn(mSubscriptionInfo1).when(mSubscriptionManager) - .getActiveSubscriptionInfo(SUB_ID_1); - mRequestArgs.setResetApn(SUB_ID_2); - doReturn(mSubscriptionInfo2).when(mSubscriptionManager) - .getActiveSubscriptionInfo(SUB_ID_2); - - ResetSubscriptionContract target = createTestObject(); - - verify(mSubscriptionManager).addOnSubscriptionsChangedListener(any(), any()); - - assertNull(target.getAnyMissingSubscriptionId()); - } - - @Test - public void getAnyMissingSubscriptionId_returnSubId_whenSubscriptionNotActive() { - mRequestArgs.setResetTelephonyAndNetworkPolicyManager(SUB_ID_1); - doReturn(mSubscriptionInfo1).when(mSubscriptionManager) - .getActiveSubscriptionInfo(SUB_ID_1); - mRequestArgs.setResetApn(SUB_ID_2); - doReturn(null).when(mSubscriptionManager) - .getActiveSubscriptionInfo(SUB_ID_2); - - ResetSubscriptionContract target = createTestObject(); - - verify(mSubscriptionManager).addOnSubscriptionsChangedListener(any(), any()); - - assertEquals(target.getAnyMissingSubscriptionId(), new Integer(SUB_ID_2)); - } -} From bc93a6a1468bc2a9ba36df535c1ff23a50b9134d Mon Sep 17 00:00:00 2001 From: YK Hung Date: Wed, 26 Jun 2024 01:11:02 +0000 Subject: [PATCH 11/18] Reattribute data into other apps as the final result (4/5) Bug: 346706894 Test: atest SettingsRoboTests:com.android.settings.fuelgauge.batteryusage Flag: EXEMPT bug fix Change-Id: I8b8a988df2718b64cd752915205db687ffe9d559 --- .../fuelgauge/PowerUsageFeatureProvider.java | 6 ++++- .../PowerUsageFeatureProviderImpl.java | 6 ++++- .../batteryusage/BatteryUsageDataLoader.java | 3 --- .../batteryusage/DataProcessManager.java | 24 +++++++++++++------ .../batteryusage/DataProcessManagerTest.java | 2 ++ 5 files changed, 29 insertions(+), 12 deletions(-) diff --git a/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java b/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java index e7b9a42cac2..98e1a6e8470 100644 --- a/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java +++ b/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java @@ -23,6 +23,8 @@ import android.os.Bundle; import android.util.ArrayMap; import android.util.SparseIntArray; +import androidx.annotation.NonNull; + import com.android.settings.fuelgauge.batteryusage.BatteryDiffData; import com.android.settings.fuelgauge.batteryusage.DetectRequestSourceType; import com.android.settings.fuelgauge.batteryusage.PowerAnomalyEventList; @@ -162,5 +164,7 @@ public interface PowerUsageFeatureProvider { /** Collect and process battery reattribute data if needed. */ boolean processBatteryReattributeData( - Context context, Map batteryDiffDataMap); + @NonNull Context context, + @NonNull Map batteryDiffDataMap, + final boolean isFromPeriodJob); } diff --git a/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java b/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java index 267c0a3fc60..dc5b2269cf8 100644 --- a/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java +++ b/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java @@ -27,6 +27,8 @@ import android.util.ArrayMap; import android.util.ArraySet; import android.util.SparseIntArray; +import androidx.annotation.NonNull; + import com.android.internal.util.ArrayUtils; import com.android.settings.fuelgauge.batteryusage.BatteryDiffData; import com.android.settings.fuelgauge.batteryusage.DetectRequestSourceType; @@ -250,7 +252,9 @@ public class PowerUsageFeatureProviderImpl implements PowerUsageFeatureProvider @Override public boolean processBatteryReattributeData( - Context context, Map batteryDiffDataMap) { + @NonNull Context context, + @NonNull Map batteryDiffDataMap, + final boolean isFromPeriodJob) { return false; } } diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageDataLoader.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageDataLoader.java index 7ef4615e744..08369127b6d 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageDataLoader.java +++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageDataLoader.java @@ -128,9 +128,6 @@ public final class BatteryUsageDataLoader { final PowerUsageFeatureProvider featureProvider = FeatureFactory.getFeatureFactory() .getPowerUsageFeatureProvider(); - // Collect and process battery reattribute data. - featureProvider.processBatteryReattributeData( - context, batteryDiffDataMap); DatabaseUtils.sendBatteryUsageSlotData( context, ConvertUtils.convertToBatteryUsageSlotList( diff --git a/src/com/android/settings/fuelgauge/batteryusage/DataProcessManager.java b/src/com/android/settings/fuelgauge/batteryusage/DataProcessManager.java index 719d3bd5362..2b88d34a17a 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/DataProcessManager.java +++ b/src/com/android/settings/fuelgauge/batteryusage/DataProcessManager.java @@ -28,6 +28,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; +import com.android.settings.fuelgauge.PowerUsageFeatureProvider; +import com.android.settings.overlay.FeatureFactory; import java.util.ArrayList; import java.util.Calendar; @@ -78,6 +80,7 @@ public class DataProcessManager { // Raw start timestamp with round to the nearest hour. private final long mRawStartTimestamp; private final long mLastFullChargeTimestamp; + private final boolean mIsFromPeriodJob; private final Context mContext; private final Handler mHandler; private final UserIdsSeries mUserIdsSeries; @@ -122,6 +125,7 @@ public class DataProcessManager { Context context, Handler handler, final UserIdsSeries userIdsSeries, + final boolean isFromPeriodJob, final long rawStartTimestamp, final long lastFullChargeTimestamp, @NonNull final OnBatteryDiffDataMapLoadedListener callbackFunction, @@ -130,6 +134,7 @@ public class DataProcessManager { mContext = context.getApplicationContext(); mHandler = handler; mUserIdsSeries = userIdsSeries; + mIsFromPeriodJob = isFromPeriodJob; mRawStartTimestamp = rawStartTimestamp; mLastFullChargeTimestamp = lastFullChargeTimestamp; mCallbackFunction = callbackFunction; @@ -147,6 +152,7 @@ public class DataProcessManager { mHandler = handler; mUserIdsSeries = userIdsSeries; mCallbackFunction = callbackFunction; + mIsFromPeriodJob = false; mRawStartTimestamp = 0L; mLastFullChargeTimestamp = 0L; mHourlyBatteryLevelsPerDay = null; @@ -158,14 +164,9 @@ public class DataProcessManager { /** Starts the async tasks to load battery history data and app usage data. */ public void start() { - start(/* isFromPeriodJob= */ false); - } - - /** Starts the async tasks to load battery history data and app usage data. */ - public void start(boolean isFromPeriodJob) { // If we have battery level data, load the battery history map and app usage simultaneously. if (mHourlyBatteryLevelsPerDay != null) { - if (isFromPeriodJob) { + if (mIsFromPeriodJob) { mIsCurrentBatteryHistoryLoaded = true; mIsCurrentAppUsageLoaded = true; mIsBatteryUsageSlotLoaded = true; @@ -514,6 +515,14 @@ public class DataProcessManager { mAppUsagePeriodMap, getSystemAppsPackageNames(), getSystemAppsUids())); + // Process the reattributate data for the following two cases: + // 1) the latest slot for the timestamp "until now" + // 2) walkthrough all BatteryDiffData again to handle "re-compute" case + final PowerUsageFeatureProvider featureProvider = + FeatureFactory.getFeatureFactory() + .getPowerUsageFeatureProvider(); + featureProvider.processBatteryReattributeData( + mContext, batteryDiffDataMap, mIsFromPeriodJob); Log.d( TAG, @@ -683,12 +692,13 @@ public class DataProcessManager { context, handler, userIdsSeries, + isFromPeriodJob, startTimestamp, lastFullChargeTime, onBatteryDiffDataMapLoadedListener, batteryLevelData.getHourlyBatteryLevelsPerDay(), processedBatteryHistoryMap) - .start(isFromPeriodJob); + .start(); return batteryLevelData; } diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessManagerTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessManagerTest.java index 60428014048..2f20b425788 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessManagerTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessManagerTest.java @@ -112,6 +112,7 @@ public final class DataProcessManagerTest { mContext, /* handler= */ null, mUserIdsSeries, + /* isFromPeriodJob= */ false, /* rawStartTimestamp= */ 0L, /* lastFullChargeTimestamp= */ 0L, /* callbackFunction= */ null, @@ -258,6 +259,7 @@ public final class DataProcessManagerTest { mContext, /* handler= */ null, mUserIdsSeries, + /* isFromPeriodJob= */ false, /* rawStartTimestamp= */ 2L, /* lastFullChargeTimestamp= */ 1L, /* callbackFunction= */ null, From 7acebd1b972b03c082a8d0689f2a228e21f5ba11 Mon Sep 17 00:00:00 2001 From: Jason Chang Date: Thu, 20 Jun 2024 11:55:57 +0000 Subject: [PATCH 12/18] Revise the order in the strings to make "Face" in front of "Fingerprint" except for "Choose a screen lock" screen Change the string order to fulfill the UX requirement. Flag: EXEMPT bugfix Bug: 293396928 Test: Build ABTD ROM and check the string order. Change-Id: I4725e22e16ea20ea774383d0c67038479805e6fa --- res-product/values/strings.xml | 18 +++++++++--------- res/values/strings.xml | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/res-product/values/strings.xml b/res-product/values/strings.xml index 987548acf33..83963cbee14 100644 --- a/res-product/values/strings.xml +++ b/res-product/values/strings.xml @@ -338,23 +338,23 @@ A password is required to set up Face Unlock.\n\nA password protects the phone if it\u2019s lost or stolen. - A PIN is required to set up Face Unlock and Fingerprint Unlock.\n\nA PIN protects the tablet if it\u2019s lost or stolen. + A PIN is required to set up Fingerprint Unlock and Face Unlock.\n\nA PIN protects the tablet if it\u2019s lost or stolen. - A pattern is required to set up Face Unlock and Fingerprint Unlock.\n\nA pattern protects the tablet if it\u2019s lost or stolen. + A pattern is required to set up Fingerprint Unlock and Face Unlock.\n\nA pattern protects the tablet if it\u2019s lost or stolen. - A password is required to set up Face Unlock and Fingerprint Unlock.\n\nA password protects the tablet if it\u2019s lost or stolen. + A password is required to set up Fingerprint Unlock and Face Unlock.\n\nA password protects the tablet if it\u2019s lost or stolen. - A PIN is required to set up Face Unlock and Fingerprint Unlock.\n\nA PIN protects the device if it\u2019s lost or stolen. + A PIN is required to set up Fingerprint Unlock and Face Unlock.\n\nA PIN protects the device if it\u2019s lost or stolen. - A pattern is required to set up Face Unlock and Fingerprint Unlock.\n\nA pattern protects the device if it\u2019s lost or stolen. + A pattern is required to set up Fingerprint Unlock and Face Unlock.\n\nA pattern protects the device if it\u2019s lost or stolen. - A password is required to set up Face Unlock and Fingerprint Unlock.\n\nA password protects the device if it\u2019s lost or stolen. + A password is required to set up Fingerprint Unlock and Face Unlock.\n\nA password protects the device if it\u2019s lost or stolen. - A PIN is required to set up Face Unlock and Fingerprint Unlock.\n\nA PIN protects the phone if it\u2019s lost or stolen. + A PIN is required to set up Fingerprint Unlock and Face Unlock.\n\nA PIN protects the phone if it\u2019s lost or stolen. - A pattern is required to set up Face Unlock and Fingerprint Unlock.\n\nA pattern protects the phone if it\u2019s lost or stolen. + A pattern is required to set up Fingerprint Unlock and Face Unlock.\n\nA pattern protects the phone if it\u2019s lost or stolen. - A password is required to set up Face Unlock and Fingerprint Unlock.\n\nA password protects the phone if it\u2019s lost or stolen. + A password is required to set up Fingerprint Unlock and Face Unlock.\n\nA password protects the phone if it\u2019s lost or stolen. This deletes the fingerprint images and model associated with \'%1$s\' that are stored on your phone diff --git a/res/values/strings.xml b/res/values/strings.xml index 075056db64e..b6a83aef405 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -995,7 +995,7 @@ - Fingerprint & Face Unlock + Face & Fingerprint Unlock Face & Fingerprint Unlock for work From 13988cec013a610abe5056d665235b8abf66f307 Mon Sep 17 00:00:00 2001 From: mxyyiyi Date: Wed, 26 Jun 2024 13:08:49 +0800 Subject: [PATCH 13/18] Skip load app name from PackageManager while init the BatteryEntry. - getApplicationLabel is time-consuming. [Before] https://pprof.corp.google.com/?id=bade2601da25f6169b0685a3e2a3e5cc https://screenshot.googleplex.com/86R5DhMgcyQakys [After] https://pprof.corp.google.com/?id=2ccb1e88222e96db1fbcc072470a254c https://screenshot.googleplex.com/7QviDFSpgzoQcg6 Bug: 349120408 Fix: 349120408 Test: atest + pprof Flag: EXEMPT bug fix Change-Id: I1eed60cea9c5bd3f77f38993f7d04906aa811da5 --- .../fuelgauge/batteryusage/BatteryEntry.java | 14 +------------- .../fuelgauge/batteryusage/BatteryEntryTest.java | 6 +++--- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryEntry.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryEntry.java index ddb8ecbe577..fef30563fe2 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/BatteryEntry.java +++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryEntry.java @@ -22,7 +22,6 @@ import android.content.pm.ApplicationInfo; import android.content.pm.IPackageManager; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.UserInfo; import android.graphics.drawable.Drawable; import android.os.BatteryConsumer; @@ -176,18 +175,7 @@ public class BatteryEntry { } } if (mDefaultPackageName != null) { - PackageManager pm = context.getPackageManager(); - try { - ApplicationInfo appInfo = - pm.getApplicationInfo(mDefaultPackageName, 0 /* no flags */); - mName = pm.getApplicationLabel(appInfo).toString(); - } catch (NameNotFoundException e) { - Log.d( - TAG, - "PackageManager failed to retrieve ApplicationInfo for: " - + mDefaultPackageName); - mName = mDefaultPackageName; - } + mName = mDefaultPackageName; } mTimeInForegroundMs = uidBatteryConsumer.getTimeInProcessStateMs( diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryEntryTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryEntryTest.java index 450d058c229..6147778e37a 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryEntryTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryEntryTest.java @@ -132,7 +132,7 @@ public class BatteryEntryTest { createBatteryEntryForApp(null, APP_DEFAULT_PACKAGE_NAME, HIGH_DRAIN_PACKAGE); assertThat(entry.getDefaultPackageName()).isEqualTo(APP_DEFAULT_PACKAGE_NAME); - assertThat(entry.getLabel()).isEqualTo(LABEL_PREFIX + APP_DEFAULT_PACKAGE_NAME); + assertThat(entry.getLabel()).isEqualTo(APP_DEFAULT_PACKAGE_NAME); } @Test @@ -152,7 +152,7 @@ public class BatteryEntryTest { BatteryEntry entry = createBatteryEntryForApp(null, null, HIGH_DRAIN_PACKAGE); - assertThat(entry.getLabel()).isEqualTo(LABEL_PREFIX + HIGH_DRAIN_PACKAGE); + assertThat(entry.getLabel()).isEqualTo(HIGH_DRAIN_PACKAGE); } @Test @@ -163,7 +163,7 @@ public class BatteryEntryTest { null, HIGH_DRAIN_PACKAGE); - assertThat(entry.getLabel()).isEqualTo(LABEL_PREFIX + HIGH_DRAIN_PACKAGE); + assertThat(entry.getLabel()).isEqualTo(HIGH_DRAIN_PACKAGE); } @Test From f22b2668e04488cb6cd800ce7fb6f96d62242be9 Mon Sep 17 00:00:00 2001 From: Jason Chiu Date: Tue, 25 Jun 2024 20:55:35 +0800 Subject: [PATCH 14/18] Guard against exception when reg/unreg content observer When an app injects an entry to Settings with dynamic title/summary, and disables its content provider at runtime, Settings will crash while trying to registering/unregistering the data observer. Fix: 337567627 Test: manual Flag: EXEMPT bugfix Change-Id: I9c7f689c6696d91f0b8e40113a8df10375930ede --- .../settings/dashboard/DashboardFragment.java | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/com/android/settings/dashboard/DashboardFragment.java b/src/com/android/settings/dashboard/DashboardFragment.java index 9abc6c2a0fa..0808da1b4a8 100644 --- a/src/com/android/settings/dashboard/DashboardFragment.java +++ b/src/com/android/settings/dashboard/DashboardFragment.java @@ -649,8 +649,12 @@ public abstract class DashboardFragment extends SettingsPreferenceFragment DynamicDataObserver observer) { Log.d(TAG, "register observer: @" + Integer.toHexString(observer.hashCode()) + ", uri: " + observer.getUri()); - resolver.registerContentObserver(observer.getUri(), false, observer); - mRegisteredObservers.add(observer); + try { + resolver.registerContentObserver(observer.getUri(), false, observer); + mRegisteredObservers.add(observer); + } catch (Exception e) { + Log.w(TAG, "Cannot register observer: " + observer.getUri(), e); + } } private void unregisterDynamicDataObservers(List observers) { @@ -661,8 +665,13 @@ public abstract class DashboardFragment extends SettingsPreferenceFragment observers.forEach(observer -> { Log.d(TAG, "unregister observer: @" + Integer.toHexString(observer.hashCode()) + ", uri: " + observer.getUri()); - mRegisteredObservers.remove(observer); - resolver.unregisterContentObserver(observer); + if (mRegisteredObservers.remove(observer)) { + try { + resolver.unregisterContentObserver(observer); + } catch (Exception e) { + Log.w(TAG, "Cannot unregister observer: " + observer.getUri(), e); + } + } }); } From d1114107dfde8c1372ddbbb706f3c3a807279721 Mon Sep 17 00:00:00 2001 From: pajacechen Date: Wed, 26 Jun 2024 14:56:58 +0800 Subject: [PATCH 15/18] Fix "Dock defend string and tips in settings are incorrectly" issue Symptom: After the dock defend was triggered, the battery tips still show "Future-Bypass" dock defend mode. It should be the "Active" dock defend mode. Root Cause: The original `BatteryInfo.isBatteryDefender` was implemented by using `longlife`, due to the charging limit also reuse `longlife` issue, we replace the implementation of `BatteryInfo.isBatteryDefender` with HAL API call `isTempDefend` and `isDwellDefend`. However, the dock defend also needs `longlife`, the original `BatteryInfo.isBatteryDefender`. So the dock defend checking failed after replacing the implementation of `BatteryInfo.isBatteryDefender` Solution: Add new property isLonglife in BatteryInfo Bug: 348563863 Test: Manual Test and robotest Test: http://ab/I55100010289930405 Flag: EXEMPT bugfix Change-Id: I180cde7a193d75243893471634bab5f354c1623b --- .../settings/fuelgauge/BatteryInfo.java | 7 +-- .../settings/fuelgauge/BatteryUtils.java | 2 +- .../settings/fuelgauge/BatteryInfoTest.java | 43 +++++++++++++++++++ 3 files changed, 48 insertions(+), 4 deletions(-) diff --git a/src/com/android/settings/fuelgauge/BatteryInfo.java b/src/com/android/settings/fuelgauge/BatteryInfo.java index b54801a677a..7cf9e44bd66 100644 --- a/src/com/android/settings/fuelgauge/BatteryInfo.java +++ b/src/com/android/settings/fuelgauge/BatteryInfo.java @@ -53,7 +53,8 @@ public class BatteryInfo { public int batteryStatus; public int pluggedStatus; public boolean discharging = true; - public boolean isBatteryDefender; + public boolean isBatteryDefender = false; + public boolean isLongLife = false; public boolean isFastCharging; public long remainingTimeUs = 0; public long averageTimeToDischarge = EstimateKt.AVERAGE_TIME_TO_DISCHARGE_UNKNOWN; @@ -306,7 +307,7 @@ public class BatteryInfo { info.pluggedStatus = batteryBroadcast.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0); info.mCharging = info.pluggedStatus != 0; info.averageTimeToDischarge = estimate.getAverageDischargeTime(); - info.isBatteryDefender = + info.isLongLife = batteryBroadcast.getIntExtra( BatteryManager.EXTRA_CHARGING_STATUS, BatteryManager.CHARGING_POLICY_DEFAULT) @@ -319,7 +320,7 @@ public class BatteryInfo { info.isFastCharging = BatteryStatus.getChargingSpeed(context, batteryBroadcast) == BatteryStatus.CHARGING_FAST; - if (info.isBatteryDefender) { + if (info.isLongLife) { info.isBatteryDefender = FeatureFactory.getFeatureFactory() .getPowerUsageFeatureProvider() diff --git a/src/com/android/settings/fuelgauge/BatteryUtils.java b/src/com/android/settings/fuelgauge/BatteryUtils.java index 9e08664c901..e5a314c90ff 100644 --- a/src/com/android/settings/fuelgauge/BatteryUtils.java +++ b/src/com/android/settings/fuelgauge/BatteryUtils.java @@ -600,7 +600,7 @@ public class BatteryUtils { context.getContentResolver(), SETTINGS_GLOBAL_DOCK_DEFENDER_BYPASS, 0) == 1) { return DockDefenderMode.TEMPORARILY_BYPASSED; - } else if (batteryInfo.isBatteryDefender + } else if (batteryInfo.isLongLife && FeatureFactory.getFeatureFactory() .getPowerUsageFeatureProvider() .isExtraDefend()) { diff --git a/tests/robotests/src/com/android/settings/fuelgauge/BatteryInfoTest.java b/tests/robotests/src/com/android/settings/fuelgauge/BatteryInfoTest.java index 7bafc6d5198..b7e65906fab 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/BatteryInfoTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/BatteryInfoTest.java @@ -789,6 +789,40 @@ public class BatteryInfoTest { expectedChargeLabel); } + @Test + public void getBatteryInfo_longlife_shouldSetLonglife() { + var batteryIntent = createIntentForLongLifeTest(/* hasLongLife= */ true); + + var batteryInfo = + BatteryInfo.getBatteryInfo( + mContext, + batteryIntent, + mBatteryUsageStats, + /* estimate= */ MOCK_ESTIMATE, + /* elapsedRealtimeUs= */ 0L, + /* shortString= */ false, + /* currentTimeMs= */ 0L); + + assertThat(batteryInfo.isLongLife).isTrue(); + } + + @Test + public void getBatteryInfo_noLonglife_shouldNotLonglife() { + var batteryIntent = createIntentForLongLifeTest(/* hasLongLife= */ false); + + var batteryInfo = + BatteryInfo.getBatteryInfo( + mContext, + batteryIntent, + mBatteryUsageStats, + /* estimate= */ MOCK_ESTIMATE, + /* elapsedRealtimeUs= */ 0L, + /* shortString= */ false, + /* currentTimeMs= */ 0L); + + assertThat(batteryInfo.isLongLife).isFalse(); + } + private enum ChargingSpeed { FAST, REGULAR, @@ -801,6 +835,15 @@ public class BatteryInfoTest { DOCKED } + private Intent createIntentForLongLifeTest(Boolean hasLongLife) { + return new Intent(Intent.ACTION_BATTERY_CHANGED) + .putExtra( + BatteryManager.EXTRA_CHARGING_STATUS, + hasLongLife + ? BatteryManager.CHARGING_POLICY_ADAPTIVE_LONGLIFE + : BatteryManager.CHARGING_POLICY_DEFAULT); + } + private Intent createIntentForGetBatteryInfoTest( ChargingType chargingType, ChargingSpeed chargingSpeed, int batteryLevel) { return createBatteryIntent( From 7ff146a34a89ccb0f3b655b120eebfff64b10ad5 Mon Sep 17 00:00:00 2001 From: mxyyiyi Date: Wed, 26 Jun 2024 16:50:28 +0800 Subject: [PATCH 16/18] Fetch package uid under the corresponding user handle. - Make 3p-app usage entry under work profile selectable: [Before] https://screenshot.googleplex.com/AuD3Q8hrepmxaoS [After] https://screenshot.googleplex.com/ACrXLcV2RYYv9aA Bug: 346982931 Fix: 346982931 Test: atest BatteryDiffEntryTest Flag: EXEMPT bug fix Change-Id: Ib54df4c6d343dd32057e741b448596357ec2c12f --- .../settings/fuelgauge/batteryusage/BatteryDiffEntry.java | 3 ++- .../fuelgauge/batteryusage/BatteryDiffEntryTest.java | 7 +++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffEntry.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffEntry.java index 5b05e347fdd..4d1545a0a89 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffEntry.java +++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffEntry.java @@ -422,7 +422,8 @@ public class BatteryDiffEntry { return; } final boolean isValidPackage = - BatteryUtils.getInstance(mContext).getPackageUid(getPackageName()) + BatteryUtils.getInstance(mContext) + .getPackageUidAsUser(getPackageName(), (int) mUserId) != BatteryUtils.UID_NULL; if (!isValidPackage) { mValidForRestriction = false; diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffEntryTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffEntryTest.java index 4567bc398d5..0e10a15987f 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffEntryTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffEntryTest.java @@ -494,6 +494,7 @@ public final class BatteryDiffEntryTest { final ContentValues values = getContentValuesWithType(ConvertUtils.CONSUMER_TYPE_UID_BATTERY); values.put(BatteryHistEntry.KEY_UID, /*invalid uid*/ 10001); + values.put(BatteryHistEntry.KEY_USER_ID, /*valid userid*/ USER_ID); values.put(BatteryHistEntry.KEY_PACKAGE_NAME, fakePackageName); final BatteryDiffEntry entry = createBatteryDiffEntry(10, new BatteryHistEntry(values)); @@ -503,14 +504,16 @@ public final class BatteryDiffEntryTest { doReturn(BatteryUtils.UID_NULL) .when(mMockPackageManager) - .getPackageUid(entry.getPackageName(), PackageManager.GET_META_DATA); + .getPackageUidAsUser( + entry.getPackageName(), PackageManager.GET_META_DATA, USER_ID); entry.updateRestrictionFlagState(); // Sets false if the app is invalid package name. assertThat(entry.mValidForRestriction).isFalse(); doReturn(1000) .when(mMockPackageManager) - .getPackageUid(entry.getPackageName(), PackageManager.GET_META_DATA); + .getPackageUidAsUser( + entry.getPackageName(), PackageManager.GET_META_DATA, USER_ID); entry.updateRestrictionFlagState(); // Sets false if the app PackageInfo cannot be found. assertThat(entry.mValidForRestriction).isFalse(); From c2d2de085d2282da6f3ab9d062c426139a397489 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Hern=C3=A1ndez?= Date: Tue, 25 Jun 2024 20:29:14 +0200 Subject: [PATCH 17/18] Changes to icon picker for reusability in "add mode" flow * Don't finish the fragment from the controller (ugh!) instead just report the selected icon via a listener. * Highlight the selected icon in the list. * Cache the icon drawables (since we're using selectors for the colors, we don't need to swap them, one per icon resource id is enough). * Improved the tests a bit too. Bug: 333901673 Bug: 326442408 Test: ates Flag: android.app.modes_ui Change-Id: Ib2ec7a7e3ed99b13f9264aa6f7c209ee3f6967a0 --- .../modes_icon_picker_item_background.xml | 25 +++++++ res/color/modes_icon_picker_item_icon.xml | 25 +++++++ .../settings/notification/modes/IconUtil.java | 12 +-- .../notification/modes/ZenModeFragment.java | 11 ++- .../modes/ZenModeFragmentBase.java | 21 +++--- .../modes/ZenModeIconPickerFragment.java | 13 +++- ...odeIconPickerListPreferenceController.java | 75 ++++++++++++++----- .../notification/modes/TestModeBuilder.java | 14 +++- ...conPickerListPreferenceControllerTest.java | 61 +++++++++------ 9 files changed, 190 insertions(+), 67 deletions(-) create mode 100644 res/color/modes_icon_picker_item_background.xml create mode 100644 res/color/modes_icon_picker_item_icon.xml diff --git a/res/color/modes_icon_picker_item_background.xml b/res/color/modes_icon_picker_item_background.xml new file mode 100644 index 00000000000..f9280c60d6c --- /dev/null +++ b/res/color/modes_icon_picker_item_background.xml @@ -0,0 +1,25 @@ + + + + + + + + + diff --git a/res/color/modes_icon_picker_item_icon.xml b/res/color/modes_icon_picker_item_icon.xml new file mode 100644 index 00000000000..8a517d5f474 --- /dev/null +++ b/res/color/modes_icon_picker_item_icon.xml @@ -0,0 +1,25 @@ + + + + + + + + + diff --git a/src/com/android/settings/notification/modes/IconUtil.java b/src/com/android/settings/notification/modes/IconUtil.java index 1e653bf03fe..56967c89d00 100644 --- a/src/com/android/settings/notification/modes/IconUtil.java +++ b/src/com/android/settings/notification/modes/IconUtil.java @@ -50,14 +50,16 @@ class IconUtil { /** * Returns a variant of the supplied {@code icon} to be used in the icon picker. The inner icon - * is 36x36dp and it's contained into a circle of diameter 54dp. + * is 36x36dp and it's contained into a circle of diameter 54dp. It's also set up so that + * selection and pressed states are represented in the color. */ static Drawable makeIconCircle(@NonNull Context context, @NonNull Drawable icon) { ShapeDrawable background = new ShapeDrawable(new OvalShape()); - background.getPaint().setColor(Utils.getColorAttrDefaultColor(context, - com.android.internal.R.attr.materialColorSecondaryContainer)); - icon.setTint(Utils.getColorAttrDefaultColor(context, - com.android.internal.R.attr.materialColorOnSecondaryContainer)); + background.setTintList( + context.getColorStateList(R.color.modes_icon_picker_item_background)); + icon = icon.mutate(); + icon.setTintList( + context.getColorStateList(R.color.modes_icon_picker_item_icon)); LayerDrawable layerDrawable = new LayerDrawable(new Drawable[] { background, icon }); diff --git a/src/com/android/settings/notification/modes/ZenModeFragment.java b/src/com/android/settings/notification/modes/ZenModeFragment.java index 5897c4dd680..63ed8395006 100644 --- a/src/com/android/settings/notification/modes/ZenModeFragment.java +++ b/src/com/android/settings/notification/modes/ZenModeFragment.java @@ -18,13 +18,14 @@ package com.android.settings.notification.modes; import android.app.AlertDialog; import android.app.Application; -import android.app.AutomaticZenRule; import android.app.settings.SettingsEnums; import android.content.Context; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; +import androidx.annotation.NonNull; + import com.android.settings.R; import com.android.settingslib.applications.ApplicationsState; import com.android.settingslib.core.AbstractPreferenceController; @@ -72,11 +73,9 @@ public class ZenModeFragment extends ZenModeFragmentBase { // Set title for the entire screen ZenMode mode = getMode(); - AutomaticZenRule azr = getAZR(); - if (mode == null || azr == null) { - return; + if (mode != null) { + requireActivity().setTitle(mode.getName()); } - getActivity().setTitle(azr.getName()); } @Override @@ -92,7 +91,7 @@ public class ZenModeFragment extends ZenModeFragmentBase { } @Override - protected boolean onOptionsItemSelected(MenuItem item, ZenMode zenMode) { + protected boolean onOptionsItemSelected(MenuItem item, @NonNull ZenMode zenMode) { switch (item.getItemId()) { case DELETE_MODE: new AlertDialog.Builder(mContext) diff --git a/src/com/android/settings/notification/modes/ZenModeFragmentBase.java b/src/com/android/settings/notification/modes/ZenModeFragmentBase.java index d08f7ea0229..b0ad7956a84 100644 --- a/src/com/android/settings/notification/modes/ZenModeFragmentBase.java +++ b/src/com/android/settings/notification/modes/ZenModeFragmentBase.java @@ -18,7 +18,6 @@ package com.android.settings.notification.modes; import static android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID; -import android.app.AutomaticZenRule; import android.content.Context; import android.os.Bundle; import android.util.Log; @@ -34,7 +33,10 @@ import com.android.settings.R; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.notification.modes.ZenMode; +import com.google.common.base.Preconditions; + import java.util.List; +import java.util.function.Consumer; /** * Base class for Settings pages used to configure individual modes. @@ -175,14 +177,15 @@ abstract class ZenModeFragmentBase extends ZenModesFragmentBase { return mZenMode; } - /** - * Get AutomaticZenRule associated with current mode data, or null if it doesn't exist. - */ - @Nullable - public AutomaticZenRule getAZR() { - if (mZenMode == null) { - return null; + protected final boolean saveMode(Consumer updater) { + Preconditions.checkState(mBackend != null); + ZenMode mode = mZenMode; + if (mode == null) { + Log.wtf(TAG, "Cannot save mode, it hasn't been loaded (" + getClass() + ")"); + return false; } - return mZenMode.getRule(); + updater.accept(mode); + mBackend.updateMode(mode); + return true; } } diff --git a/src/com/android/settings/notification/modes/ZenModeIconPickerFragment.java b/src/com/android/settings/notification/modes/ZenModeIconPickerFragment.java index 760b1835f70..43d9dba1b54 100644 --- a/src/com/android/settings/notification/modes/ZenModeIconPickerFragment.java +++ b/src/com/android/settings/notification/modes/ZenModeIconPickerFragment.java @@ -43,7 +43,16 @@ public class ZenModeIconPickerFragment extends ZenModeFragmentBase { return ImmutableList.of( new ZenModeIconPickerIconPreferenceController(context, "current_icon", this, mBackend), - new ZenModeIconPickerListPreferenceController(context, "icon_list", this, - new IconOptionsProviderImpl(mContext), mBackend)); + new ZenModeIconPickerListPreferenceController(context, "icon_list", + mIconPickerListener, new IconOptionsProviderImpl(mContext), mBackend)); } + + private final ZenModeIconPickerListPreferenceController.IconPickerListener mIconPickerListener = + new ZenModeIconPickerListPreferenceController.IconPickerListener() { + @Override + public void onIconSelected(int iconResId) { + saveMode(mode -> mode.getRule().setIconResId(iconResId)); + finish(); + } + }; } diff --git a/src/com/android/settings/notification/modes/ZenModeIconPickerListPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeIconPickerListPreferenceController.java index 85ceafe0870..e663354231e 100644 --- a/src/com/android/settings/notification/modes/ZenModeIconPickerListPreferenceController.java +++ b/src/com/android/settings/notification/modes/ZenModeIconPickerListPreferenceController.java @@ -17,6 +17,7 @@ package com.android.settings.notification.modes; import android.content.Context; +import android.graphics.drawable.Drawable; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -25,31 +26,35 @@ import android.widget.ImageView; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.SimpleItemAnimator; import com.android.settings.R; -import com.android.settings.dashboard.DashboardFragment; import com.android.settingslib.notification.modes.ZenMode; import com.android.settingslib.notification.modes.ZenModesBackend; import com.android.settingslib.widget.LayoutPreference; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; + +import java.util.HashMap; +import java.util.Map; class ZenModeIconPickerListPreferenceController extends AbstractZenModePreferenceController { - private final DashboardFragment mFragment; private final IconOptionsProvider mIconOptionsProvider; + private final IconPickerListener mListener; @Nullable private IconAdapter mAdapter; + private @DrawableRes int mCurrentIconResId; ZenModeIconPickerListPreferenceController(@NonNull Context context, @NonNull String key, - @NonNull DashboardFragment fragment, @NonNull IconOptionsProvider iconOptionsProvider, + @NonNull IconPickerListener listener, @NonNull IconOptionsProvider iconOptionsProvider, @Nullable ZenModesBackend backend) { super(context, key, backend); - mFragment = fragment; + mListener = listener; mIconOptionsProvider = iconOptionsProvider; } @@ -68,20 +73,34 @@ class ZenModeIconPickerListPreferenceController extends AbstractZenModePreferenc recyclerView.setLayoutManager(new AutoFitGridLayoutManager(mContext)); recyclerView.setAdapter(mAdapter); recyclerView.setHasFixedSize(true); - } - - @VisibleForTesting - void onIconSelected(@DrawableRes int resId) { - saveMode(mode -> { - mode.getRule().setIconResId(resId); - return mode; - }); - mFragment.finish(); + if (recyclerView.getItemAnimator() instanceof SimpleItemAnimator animator) { + animator.setSupportsChangeAnimations(true); + } } @Override void updateState(Preference preference, @NonNull ZenMode zenMode) { - // Nothing to do, the current icon is shown in a different preference. + updateIconSelection(zenMode.getRule().getIconResId()); + } + + private void updateIconSelection(@DrawableRes int iconResId) { + if (iconResId != mCurrentIconResId) { + int oldIconResId = mCurrentIconResId; + mCurrentIconResId = iconResId; + if (mAdapter != null) { + mAdapter.notifyIconChanged(oldIconResId); + mAdapter.notifyIconChanged(mCurrentIconResId); + } + } + } + + private void onIconSelected(@DrawableRes int iconResId) { + updateIconSelection(iconResId); + mListener.onIconSelected(iconResId); + } + + interface IconPickerListener { + void onIconSelected(@DrawableRes int iconResId); } private class IconHolder extends RecyclerView.ViewHolder { @@ -93,20 +112,25 @@ class ZenModeIconPickerListPreferenceController extends AbstractZenModePreferenc mImageView = itemView.findViewById(R.id.icon_image_view); } - void bindIcon(IconOptionsProvider.IconInfo icon) { - mImageView.setImageDrawable( - IconUtil.makeIconCircle(itemView.getContext(), icon.resId())); + void bindIcon(IconOptionsProvider.IconInfo icon, Drawable iconDrawable) { + mImageView.setImageDrawable(iconDrawable); itemView.setContentDescription(icon.description()); - itemView.setOnClickListener(v -> onIconSelected(icon.resId())); + itemView.setOnClickListener(v -> { + itemView.setSelected(true); // Immediately, to avoid flicker until we rebind. + onIconSelected(icon.resId()); + }); + itemView.setSelected(icon.resId() == mCurrentIconResId); } } private class IconAdapter extends RecyclerView.Adapter { private final ImmutableList mIconResources; + private final Map mIconCache; private IconAdapter(IconOptionsProvider iconOptionsProvider) { mIconResources = iconOptionsProvider.getIcons(); + mIconCache = new HashMap<>(); } @NonNull @@ -119,13 +143,24 @@ class ZenModeIconPickerListPreferenceController extends AbstractZenModePreferenc @Override public void onBindViewHolder(@NonNull IconHolder holder, int position) { - holder.bindIcon(mIconResources.get(position)); + IconOptionsProvider.IconInfo iconInfo = mIconResources.get(position); + Drawable iconDrawable = mIconCache.computeIfAbsent(iconInfo, + info -> IconUtil.makeIconCircle(mContext, info.resId())); + holder.bindIcon(iconInfo, iconDrawable); } @Override public int getItemCount() { return mIconResources.size(); } + + private void notifyIconChanged(@DrawableRes int iconResId) { + int position = Iterables.indexOf(mIconResources, + iconInfo -> iconInfo.resId() == iconResId); + if (position != -1) { + notifyItemChanged(position); + } + } } private static class AutoFitGridLayoutManager extends GridLayoutManager { diff --git a/tests/robotests/src/com/android/settings/notification/modes/TestModeBuilder.java b/tests/robotests/src/com/android/settings/notification/modes/TestModeBuilder.java index fa12b30590c..26c7fe170c0 100644 --- a/tests/robotests/src/com/android/settings/notification/modes/TestModeBuilder.java +++ b/tests/robotests/src/com/android/settings/notification/modes/TestModeBuilder.java @@ -24,6 +24,7 @@ import android.service.notification.ZenDeviceEffects; import android.service.notification.ZenModeConfig; import android.service.notification.ZenPolicy; +import androidx.annotation.DrawableRes; import androidx.annotation.Nullable; import com.android.settingslib.notification.modes.ZenMode; @@ -70,13 +71,13 @@ class TestModeBuilder { return this; } - public TestModeBuilder setName(String name) { + TestModeBuilder setName(String name) { mRule.setName(name); mConfigZenRule.name = name; return this; } - public TestModeBuilder setPackage(String pkg) { + TestModeBuilder setPackage(String pkg) { mRule.setPackageName(pkg); mConfigZenRule.pkg = pkg; return this; @@ -114,7 +115,7 @@ class TestModeBuilder { return this; } - public TestModeBuilder setEnabled(boolean enabled) { + TestModeBuilder setEnabled(boolean enabled) { mRule.setEnabled(enabled); mConfigZenRule.enabled = enabled; return this; @@ -126,12 +127,17 @@ class TestModeBuilder { return this; } - public TestModeBuilder setTriggerDescription(@Nullable String triggerDescription) { + TestModeBuilder setTriggerDescription(@Nullable String triggerDescription) { mRule.setTriggerDescription(triggerDescription); mConfigZenRule.triggerDescription = triggerDescription; return this; } + TestModeBuilder setIconResId(@DrawableRes int iconResId) { + mRule.setIconResId(iconResId); + return this; + } + TestModeBuilder setActive(boolean active) { if (active) { mConfigZenRule.enabled = true; diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeIconPickerListPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeIconPickerListPreferenceControllerTest.java index 5db7e925eef..e0ca306c71c 100644 --- a/tests/robotests/src/com/android/settings/notification/modes/ZenModeIconPickerListPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeIconPickerListPreferenceControllerTest.java @@ -24,13 +24,15 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.Context; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; import androidx.annotation.NonNull; import androidx.preference.PreferenceScreen; import androidx.recyclerview.widget.RecyclerView; import com.android.settings.R; -import com.android.settings.dashboard.DashboardFragment; import com.android.settingslib.notification.modes.ZenMode; import com.android.settingslib.notification.modes.ZenModesBackend; import com.android.settingslib.widget.LayoutPreference; @@ -40,35 +42,34 @@ import com.google.common.collect.ImmutableList; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; @RunWith(RobolectricTestRunner.class) public class ZenModeIconPickerListPreferenceControllerTest { - private static final ZenMode ZEN_MODE = TestModeBuilder.EXAMPLE; - - private ZenModesBackend mBackend; + private Context mContext; private ZenModeIconPickerListPreferenceController mController; - private PreferenceScreen mPreferenceScreen; + @Mock private PreferenceScreen mPreferenceScreen; + private LayoutPreference mLayoutPreference; private RecyclerView mRecyclerView; + @Mock private ZenModeIconPickerListPreferenceController.IconPickerListener mListener; @Before public void setUp() { - Context context = RuntimeEnvironment.getApplication(); - mBackend = mock(ZenModesBackend.class); + MockitoAnnotations.initMocks(this); + mContext = RuntimeEnvironment.getApplication(); - DashboardFragment fragment = mock(DashboardFragment.class); mController = new ZenModeIconPickerListPreferenceController( - RuntimeEnvironment.getApplication(), "icon_list", fragment, - new TestIconOptionsProvider(), mBackend); + RuntimeEnvironment.getApplication(), "icon_list", mListener, + new TestIconOptionsProvider(), mock(ZenModesBackend.class)); - mRecyclerView = new RecyclerView(context); + mRecyclerView = new RecyclerView(mContext); mRecyclerView.setId(R.id.icon_list); - LayoutPreference layoutPreference = new LayoutPreference(context, mRecyclerView); - mPreferenceScreen = mock(PreferenceScreen.class); - when(mPreferenceScreen.findPreference(eq("icon_list"))).thenReturn(layoutPreference); + mLayoutPreference = new LayoutPreference(mContext, mRecyclerView); + when(mPreferenceScreen.findPreference(eq("icon_list"))).thenReturn(mLayoutPreference); } @Test @@ -80,14 +81,32 @@ public class ZenModeIconPickerListPreferenceControllerTest { } @Test - public void selectIcon_updatesMode() { - mController.setZenMode(ZEN_MODE); + public void updateState_highlightsCurrentIcon() { + ZenMode mode = new TestModeBuilder().setIconResId(R.drawable.ic_hearing).build(); + mController.displayPreference(mPreferenceScreen); - mController.onIconSelected(R.drawable.ic_android); + mController.updateZenMode(mLayoutPreference, mode); - ArgumentCaptor captor = ArgumentCaptor.forClass(ZenMode.class); - verify(mBackend).updateMode(captor.capture()); - assertThat(captor.getValue().getRule().getIconResId()).isEqualTo(R.drawable.ic_android); + assertThat(getItemViewAt(0).isSelected()).isFalse(); + assertThat(getItemViewAt(1).isSelected()).isFalse(); + assertThat(getItemViewAt(2).isSelected()).isTrue(); + } + + @Test + public void performClick_onIconItem_notifiesListener() { + mController.displayPreference(mPreferenceScreen); + + getItemViewAt(1).performClick(); + + verify(mListener).onIconSelected(R.drawable.ic_info); + } + + private View getItemViewAt(int position) { + ViewGroup fakeParent = new FrameLayout(mContext); + RecyclerView.ViewHolder viewHolder = mRecyclerView.getAdapter().onCreateViewHolder( + fakeParent, 0); + mRecyclerView.getAdapter().bindViewHolder(viewHolder, position); + return viewHolder.itemView; } private static class TestIconOptionsProvider implements IconOptionsProvider { From f554e16f45ef9ca186f03be079b070bb269bd7f9 Mon Sep 17 00:00:00 2001 From: songferngwang Date: Tue, 25 Jun 2024 09:32:26 +0000 Subject: [PATCH 18/18] Add more condition to avoid to open the SimOnboardingActivity when the user inserts the psim, showing the sim onboarding for the user to setup the sim switching or the default data subscription in DSDS. Will show dialog for below cases. 1. the psim slot is not active. 2. there are one or more active sim. Bug: 348524643 Test: verify the UI Flag: EXEMPT bugfix Change-Id: I3c782fa2486fde25ac15a69b48ba2f07f90572bd --- .../sim/receivers/SimSlotChangeHandler.java | 37 ++++++++++++++----- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/src/com/android/settings/sim/receivers/SimSlotChangeHandler.java b/src/com/android/settings/sim/receivers/SimSlotChangeHandler.java index 8f934d7e887..9394af16945 100644 --- a/src/com/android/settings/sim/receivers/SimSlotChangeHandler.java +++ b/src/com/android/settings/sim/receivers/SimSlotChangeHandler.java @@ -28,6 +28,7 @@ import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; import android.telephony.UiccCardInfo; +import android.telephony.UiccPortInfo; import android.telephony.UiccSlotInfo; import android.util.Log; @@ -91,10 +92,10 @@ public class SimSlotChangeHandler { Log.e(TAG, "Unable to find the removable slot. Do nothing."); return; } - + Log.i(TAG, "The removableSlotInfo: " + removableSlotInfo); int lastRemovableSlotState = getLastRemovableSimSlotState(mContext); int currentRemovableSlotState = removableSlotInfo.getCardStateInfo(); - Log.i(TAG, + Log.d(TAG, "lastRemovableSlotState: " + lastRemovableSlotState + ",currentRemovableSlotState: " + currentRemovableSlotState); boolean isRemovableSimInserted = @@ -115,8 +116,12 @@ public class SimSlotChangeHandler { if (Flags.isDualSimOnboardingEnabled()) { // ForNewUi, when the user inserts the psim, showing the sim onboarding for the user - // to setup the sim switching or the default data subscription. - handleRemovableSimInsertWhenDsds(); + // to setup the sim switching or the default data subscription in DSDS. + // Will show dialog for below case. + // 1. the psim slot is not active. + // 2. there are one or more active sim. + handleRemovableSimInsertWhenDsds(removableSlotInfo); + return; } else if (!isMultipleEnabledProfilesSupported()) { Log.d(TAG, "The device is already in DSDS mode and no MEP. Do nothing."); return; @@ -124,8 +129,6 @@ public class SimSlotChangeHandler { handleRemovableSimInsertUnderDsdsMep(removableSlotInfo); return; } - Log.d(TAG, "the device is already in DSDS mode and have the DDS. Do nothing."); - return; } if (isRemovableSimInserted) { @@ -258,15 +261,29 @@ public class SimSlotChangeHandler { startChooseSimActivity(false); } - private void handleRemovableSimInsertWhenDsds() { + private boolean hasOtherActiveSubInfo(int psimSubId) { + List activeSubs = SubscriptionUtil.getActiveSubscriptions(mSubMgr); + return activeSubs.stream() + .anyMatch(subscriptionInfo -> subscriptionInfo.getSubscriptionId() != psimSubId); + } + + private boolean hasAnyPortActiveInSlot(UiccSlotInfo removableSlotInfo) { + return removableSlotInfo.getPorts().stream().anyMatch(UiccPortInfo::isActive); + } + + private void handleRemovableSimInsertWhenDsds(UiccSlotInfo removableSlotInfo) { + Log.i(TAG, "ForNewUi: Handle Removable SIM inserted"); List subscriptionInfos = getAvailableRemovableSubscription(); if (subscriptionInfos.isEmpty()) { Log.e(TAG, "Unable to find the removable subscriptionInfo. Do nothing."); return; } - Log.d(TAG, "ForNewUi and getAvailableRemovableSubscription:" - + subscriptionInfos); - startSimConfirmDialogActivity(subscriptionInfos.get(0).getSubscriptionId()); + Log.d(TAG, "getAvailableRemovableSubscription:" + subscriptionInfos); + int psimSubId = subscriptionInfos.get(0).getSubscriptionId(); + if (!hasAnyPortActiveInSlot(removableSlotInfo) || hasOtherActiveSubInfo(psimSubId)) { + Log.d(TAG, "ForNewUi Start Setup flow"); + startSimConfirmDialogActivity(psimSubId); + } } private void handleRemovableSimInsertUnderDsdsMep(UiccSlotInfo removableSlotInfo) {