From c725c5ba5ad44a37bbc997955577d7fdf8015041 Mon Sep 17 00:00:00 2001 From: Sunny Shao Date: Wed, 29 May 2024 09:55:07 +0800 Subject: [PATCH 01/12] Split ChangeReason and decouple it from Observer Observer is flexible to be used for other scenarios. Bug: 325144964 Test: atest SettingsLibDataStoreTest Test: atest com.android.settings.fuelgauge Change-Id: I50c0c6267b29460efa3861e609d64a4d92db7b89 --- .../settings/fuelgauge/BatteryOptimizeUtils.java | 8 ++++---- .../fuelgauge/BatteryOptimizeUtilsTest.java | 14 +++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/com/android/settings/fuelgauge/BatteryOptimizeUtils.java b/src/com/android/settings/fuelgauge/BatteryOptimizeUtils.java index 62e28fe6988..9c7f00701d5 100644 --- a/src/com/android/settings/fuelgauge/BatteryOptimizeUtils.java +++ b/src/com/android/settings/fuelgauge/BatteryOptimizeUtils.java @@ -33,7 +33,7 @@ import androidx.annotation.VisibleForTesting; import com.android.settings.R; import com.android.settings.fuelgauge.BatteryOptimizeHistoricalLogEntry.Action; -import com.android.settingslib.datastore.ChangeReason; +import com.android.settingslib.datastore.DataChangeReason; import com.android.settingslib.fuelgauge.PowerAllowlistBackend; import java.lang.annotation.Retention; @@ -225,7 +225,7 @@ public class BatteryOptimizeUtils { // App preferences are already clear when code reach here, and there may be no // setAppUsageStateInternal call to notifyChange. So always trigger notifyChange here. - BatterySettingsStorage.get(context).notifyChange(ChangeReason.DELETE); + BatterySettingsStorage.get(context).notifyChange(DataChangeReason.DELETE); allowlistBackend.refreshList(); // Resets optimization mode for each application. @@ -371,7 +371,7 @@ public class BatteryOptimizeUtils { getAppOptimizationMode(appStandbyMode, allowListed)); } - private static @ChangeReason int toChangeReason(Action action) { - return action == Action.RESTORE ? ChangeReason.RESTORE : ChangeReason.UPDATE; + private static @DataChangeReason int toChangeReason(Action action) { + return action == Action.RESTORE ? DataChangeReason.RESTORE : DataChangeReason.UPDATE; } } diff --git a/tests/robotests/src/com/android/settings/fuelgauge/BatteryOptimizeUtilsTest.java b/tests/robotests/src/com/android/settings/fuelgauge/BatteryOptimizeUtilsTest.java index 9686709c013..6094208cfaf 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/BatteryOptimizeUtilsTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/BatteryOptimizeUtilsTest.java @@ -49,7 +49,7 @@ import android.os.UserManager; import android.util.ArraySet; import com.android.settings.fuelgauge.BatteryOptimizeHistoricalLogEntry.Action; -import com.android.settingslib.datastore.ChangeReason; +import com.android.settingslib.datastore.DataChangeReason; import com.android.settingslib.datastore.Observer; import com.android.settingslib.fuelgauge.PowerAllowlistBackend; @@ -164,7 +164,7 @@ public class BatteryOptimizeUtilsTest { TimeUnit.SECONDS.sleep(1); verifySetAppOptimizationMode(AppOpsManager.MODE_IGNORED, /* allowListed */ false); - verify(mObserver).onChanged(ChangeReason.UPDATE); + verify(mObserver).onChanged(DataChangeReason.UPDATE); } @Test @@ -178,7 +178,7 @@ public class BatteryOptimizeUtilsTest { TimeUnit.SECONDS.sleep(1); verifySetAppOptimizationMode(AppOpsManager.MODE_ALLOWED, /* allowListed */ true); - verify(mObserver).onChanged(ChangeReason.UPDATE); + verify(mObserver).onChanged(DataChangeReason.UPDATE); } @Test @@ -192,7 +192,7 @@ public class BatteryOptimizeUtilsTest { TimeUnit.SECONDS.sleep(1); verifySetAppOptimizationMode(AppOpsManager.MODE_ALLOWED, /* allowListed */ false); - verify(mObserver).onChanged(ChangeReason.UPDATE); + verify(mObserver).onChanged(DataChangeReason.UPDATE); } @Test @@ -300,7 +300,7 @@ public class BatteryOptimizeUtilsTest { inOrder.verify(mMockBackend).isAllowlisted(PACKAGE_NAME, UID); inOrder.verify(mMockBackend).isSysAllowlisted(PACKAGE_NAME); verifyNoMoreInteractions(mMockBackend); - verify(mObserver).onChanged(ChangeReason.DELETE); + verify(mObserver).onChanged(DataChangeReason.DELETE); } @Test @@ -311,7 +311,7 @@ public class BatteryOptimizeUtilsTest { /* isSystemOrDefaultApp */ false); verifySetAppOptimizationMode(AppOpsManager.MODE_ALLOWED, /* allowListed */ false); - verify(mObserver).onChanged(ChangeReason.DELETE); + verify(mObserver).onChanged(DataChangeReason.DELETE); } @Test @@ -322,7 +322,7 @@ public class BatteryOptimizeUtilsTest { /* isSystemOrDefaultApp */ false); verifySetAppOptimizationMode(AppOpsManager.MODE_ALLOWED, /* allowListed */ false); - verify(mObserver).onChanged(ChangeReason.DELETE); + verify(mObserver).onChanged(DataChangeReason.DELETE); } private void runTestForResetWithMode( From bfdcc82dc76008db5d7cd28dc12e5e56d4f00a2b Mon Sep 17 00:00:00 2001 From: Yiyi Shen Date: Wed, 29 May 2024 15:24:54 +0800 Subject: [PATCH 02/12] [Audiosharing] Add flag to start broadcast with no lea headset connected Allow audio sharing with no connected le audio headset when settings_need_connected_ble_device_for_broadcast is false Bug: 343277847 Test: atest Change-Id: I560338d53ddf290550faf8198cd7173bb734d150 --- .../AudioSharingSwitchBarController.java | 14 +++++++++---- .../AudioSharingSwitchBarControllerTest.java | 20 ++++++++++++++++++- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarController.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarController.java index df49de45d45..475be85a8a3 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarController.java +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarController.java @@ -27,6 +27,7 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.util.FeatureFlagUtils; import android.util.Log; import android.widget.CompoundButton; import android.widget.CompoundButton.OnCheckedChangeListener; @@ -320,10 +321,15 @@ public class AudioSharingSwitchBarController extends BasePreferenceController } return; } - if (mAssistant - .getDevicesMatchingConnectionStates( - new int[] {BluetoothProfile.STATE_CONNECTED}) - .isEmpty()) { + // FeatureFlagUtils.SETTINGS_NEED_CONNECTED_BLE_DEVICE_FOR_BROADCAST is always true in + // prod. We can turn off the flag for debug purpose. + if (FeatureFlagUtils.isEnabled( + mContext, + FeatureFlagUtils.SETTINGS_NEED_CONNECTED_BLE_DEVICE_FOR_BROADCAST) + && mAssistant + .getDevicesMatchingConnectionStates( + new int[] {BluetoothProfile.STATE_CONNECTED}) + .isEmpty()) { // Pop up dialog to ask users to connect at least one lea buds before audio sharing. AudioSharingUtils.postOnMainThread( mContext, diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarControllerTest.java index 0b8f1211b71..0ead2d5d807 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarControllerTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarControllerTest.java @@ -42,6 +42,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.os.Looper; import android.platform.test.flag.junit.SetFlagsRule; +import android.util.FeatureFlagUtils; import android.widget.CompoundButton; import androidx.lifecycle.LifecycleOwner; @@ -322,7 +323,9 @@ public class AudioSharingSwitchBarControllerTest { } @Test - public void onCheckedChangedToChecked_noConnectedLeaDevices_notStartAudioSharing() { + public void onCheckedChangedToChecked_noConnectedLeaDevices_flagOn_notStartAudioSharing() { + FeatureFlagUtils.setEnabled( + mContext, FeatureFlagUtils.SETTINGS_NEED_CONNECTED_BLE_DEVICE_FOR_BROADCAST, true); when(mBtnView.isEnabled()).thenReturn(true); when(mAssistant.getDevicesMatchingConnectionStates( new int[] {BluetoothProfile.STATE_CONNECTED})) @@ -333,8 +336,23 @@ public class AudioSharingSwitchBarControllerTest { verify(mBroadcast, times(0)).startPrivateBroadcast(); } + @Test + public void onCheckedChangedToChecked_noConnectedLeaDevices_flagOff_startAudioSharing() { + FeatureFlagUtils.setEnabled( + mContext, FeatureFlagUtils.SETTINGS_NEED_CONNECTED_BLE_DEVICE_FOR_BROADCAST, false); + when(mBtnView.isEnabled()).thenReturn(true); + when(mAssistant.getDevicesMatchingConnectionStates( + new int[] {BluetoothProfile.STATE_CONNECTED})) + .thenReturn(ImmutableList.of()); + doNothing().when(mBroadcast).startPrivateBroadcast(); + mController.onCheckedChanged(mBtnView, /* isChecked= */ true); + verify(mBroadcast).startPrivateBroadcast(); + } + @Test public void onCheckedChangedToChecked_notSharing_withConnectedLeaDevices_startAudioSharing() { + FeatureFlagUtils.setEnabled( + mContext, FeatureFlagUtils.SETTINGS_NEED_CONNECTED_BLE_DEVICE_FOR_BROADCAST, true); when(mBtnView.isEnabled()).thenReturn(true); when(mAssistant.getDevicesMatchingConnectionStates( new int[] {BluetoothProfile.STATE_CONNECTED})) From f29f44296a15d7363f9e9830c3a1d2e5eb02b20e Mon Sep 17 00:00:00 2001 From: Yuri Lin Date: Wed, 22 May 2024 11:08:13 -0400 Subject: [PATCH 03/12] Add page for choosing calendar & reply for event modes. Also adds the link to set trigger behavior to the main mode page (only for calendar so far), and the switch preference to enable/disable automatic rules (for all but manual DND mode). Removes the "escape hatch" to allow manual mode to also use the new modes pages. Ported from ZenModeEventRuleSettings. Flag: android.app.modes_ui Bug: 332730302 Test: ZenModeSetCalendarPreferenceControllerTest, ZenModeSetTriggerLinkPreferenceControllerTest, manual Change-Id: Ia7a716c66663a21494a6c05711250a5bda87ca8c --- res/values/strings.xml | 12 + res/xml/modes_rule_settings.xml | 9 + res/xml/modes_set_calendar.xml | 44 +++ .../notification/modes/ZenModeFragment.java | 2 + .../modes/ZenModeFragmentBase.java | 1 + .../modes/ZenModeListPreference.java | 24 +- .../modes/ZenModeSetCalendarFragment.java | 52 ++++ ...enModeSetCalendarPreferenceController.java | 264 ++++++++++++++++++ ...odeSetTriggerLinkPreferenceController.java | 96 +++++++ ...deSetCalendarPreferenceControllerTest.java | 151 ++++++++++ ...etTriggerLinkPreferenceControllerTest.java | 170 +++++++++++ 11 files changed, 808 insertions(+), 17 deletions(-) create mode 100644 res/xml/modes_set_calendar.xml create mode 100644 src/com/android/settings/notification/modes/ZenModeSetCalendarFragment.java create mode 100644 src/com/android/settings/notification/modes/ZenModeSetCalendarPreferenceController.java create mode 100644 src/com/android/settings/notification/modes/ZenModeSetTriggerLinkPreferenceController.java create mode 100644 tests/robotests/src/com/android/settings/notification/modes/ZenModeSetCalendarPreferenceControllerTest.java create mode 100644 tests/robotests/src/com/android/settings/notification/modes/ZenModeSetTriggerLinkPreferenceControllerTest.java diff --git a/res/values/strings.xml b/res/values/strings.xml index bc573228030..3b1bec85b43 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -7947,6 +7947,18 @@ Schedule + + Turn on automatically + + + Add a calendar + + + Use your calendar + + + Schedule + Schedule diff --git a/res/xml/modes_rule_settings.xml b/res/xml/modes_rule_settings.xml index df560957036..f2822741bc7 100644 --- a/res/xml/modes_rule_settings.xml +++ b/res/xml/modes_rule_settings.xml @@ -16,6 +16,7 @@ --> + + + + + diff --git a/res/xml/modes_set_calendar.xml b/res/xml/modes_set_calendar.xml new file mode 100644 index 00000000000..02eb26e33af --- /dev/null +++ b/res/xml/modes_set_calendar.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/com/android/settings/notification/modes/ZenModeFragment.java b/src/com/android/settings/notification/modes/ZenModeFragment.java index 1f6ae45c462..7084f51a922 100644 --- a/src/com/android/settings/notification/modes/ZenModeFragment.java +++ b/src/com/android/settings/notification/modes/ZenModeFragment.java @@ -46,6 +46,8 @@ public class ZenModeFragment extends ZenModeFragmentBase { context, "zen_other_settings", mBackend)); prefControllers.add(new ZenModeDisplayLinkPreferenceController( context, "mode_display_settings", mBackend)); + prefControllers.add(new ZenModeSetTriggerLinkPreferenceController(context, + "zen_automatic_trigger_category", mBackend)); return prefControllers; } diff --git a/src/com/android/settings/notification/modes/ZenModeFragmentBase.java b/src/com/android/settings/notification/modes/ZenModeFragmentBase.java index ff75afc756b..5e6cfa5084e 100644 --- a/src/com/android/settings/notification/modes/ZenModeFragmentBase.java +++ b/src/com/android/settings/notification/modes/ZenModeFragmentBase.java @@ -103,6 +103,7 @@ abstract class ZenModeFragmentBase extends ZenModesFragmentBase { if (!reloadMode(id)) { Log.d(TAG, "Mode id=" + id + " not found"); toastAndFinish(); + return; } updateControllers(); } diff --git a/src/com/android/settings/notification/modes/ZenModeListPreference.java b/src/com/android/settings/notification/modes/ZenModeListPreference.java index 0f4728f05de..78bae81481c 100644 --- a/src/com/android/settings/notification/modes/ZenModeListPreference.java +++ b/src/com/android/settings/notification/modes/ZenModeListPreference.java @@ -22,7 +22,6 @@ import android.content.Context; import android.os.Bundle; import com.android.settings.core.SubSettingLauncher; -import com.android.settings.notification.zen.ZenModeSettings; import com.android.settingslib.RestrictedPreference; /** @@ -42,22 +41,13 @@ class ZenModeListPreference extends RestrictedPreference { @Override public void onClick() { - // TODO: b/322373473 - This implementation is a hack that just leads to the old DND page - // for manual only; remove this in favor of the real implementation. - if (mZenMode.isManualDnd()) { - new SubSettingLauncher(mContext) - .setDestination(ZenModeSettings.class.getName()) - .setSourceMetricsCategory(SettingsEnums.NOTIFICATION_ZEN_MODE) - .launch(); - } else { - Bundle bundle = new Bundle(); - bundle.putString(MODE_ID, mZenMode.getId()); - new SubSettingLauncher(mContext) - .setDestination(ZenModeFragment.class.getName()) - .setArguments(bundle) - .setSourceMetricsCategory(SettingsEnums.NOTIFICATION_ZEN_MODE_AUTOMATION) - .launch(); - } + Bundle bundle = new Bundle(); + bundle.putString(MODE_ID, mZenMode.getId()); + new SubSettingLauncher(mContext) + .setDestination(ZenModeFragment.class.getName()) + .setArguments(bundle) + .setSourceMetricsCategory(SettingsEnums.NOTIFICATION_ZEN_MODE_AUTOMATION) + .launch(); } public void setZenMode(ZenMode zenMode) { diff --git a/src/com/android/settings/notification/modes/ZenModeSetCalendarFragment.java b/src/com/android/settings/notification/modes/ZenModeSetCalendarFragment.java new file mode 100644 index 00000000000..f0206ef5dad --- /dev/null +++ b/src/com/android/settings/notification/modes/ZenModeSetCalendarFragment.java @@ -0,0 +1,52 @@ +/* + * 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.modes; + +import android.app.settings.SettingsEnums; +import android.content.Context; + +import com.android.settings.R; +import com.android.settingslib.core.AbstractPreferenceController; + +import java.util.ArrayList; +import java.util.List; + +/** + * Page for choosing calendar and reply type for a scheduled mode that triggers on events. + */ +public class ZenModeSetCalendarFragment extends ZenModeFragmentBase { + + @Override + protected List createPreferenceControllers(Context context) { + List controllers = new ArrayList<>(); + controllers.add( + new ZenModeSetCalendarPreferenceController(context, "zen_mode_event_category", + mBackend)); + return controllers; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.modes_set_calendar; + } + + @Override + public int getMetricsCategory() { + // TODO: b/332937635 - make this the correct metrics category + return SettingsEnums.NOTIFICATION_ZEN_MODE_EVENT_RULE; + } +} diff --git a/src/com/android/settings/notification/modes/ZenModeSetCalendarPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeSetCalendarPreferenceController.java new file mode 100644 index 00000000000..28413091a37 --- /dev/null +++ b/src/com/android/settings/notification/modes/ZenModeSetCalendarPreferenceController.java @@ -0,0 +1,264 @@ +/* + * 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.modes; + +import android.app.Flags; +import android.content.Context; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.os.UserHandle; +import android.os.UserManager; +import android.provider.CalendarContract; +import android.service.notification.SystemZenRules; +import android.service.notification.ZenModeConfig; + +import androidx.annotation.NonNull; +import androidx.preference.DropDownPreference; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.settings.R; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; + +public class ZenModeSetCalendarPreferenceController extends AbstractZenModePreferenceController { + @VisibleForTesting + protected static final String KEY_CALENDAR = "calendar"; + @VisibleForTesting + protected static final String KEY_REPLY = "reply"; + + private DropDownPreference mCalendar; + private DropDownPreference mReply; + + private ZenModeConfig.EventInfo mEvent; + + public ZenModeSetCalendarPreferenceController(Context context, String key, + ZenModesBackend backend) { + super(context, key, backend); + } + + @Override + public void updateState(Preference preference, @NonNull ZenMode zenMode) { + PreferenceCategory cat = (PreferenceCategory) preference; + + // Refresh our understanding of local preferences + mCalendar = cat.findPreference(KEY_CALENDAR); + mReply = cat.findPreference(KEY_REPLY); + + if (mCalendar == null || mReply == null) { + return; + } + + mCalendar.setOnPreferenceChangeListener(mCalendarChangeListener); + + mReply.setEntries(new CharSequence[] { + mContext.getString(R.string.zen_mode_event_rule_reply_any_except_no), + mContext.getString(R.string.zen_mode_event_rule_reply_yes_or_maybe), + mContext.getString(R.string.zen_mode_event_rule_reply_yes), + }); + mReply.setEntryValues(new CharSequence[] { + Integer.toString(ZenModeConfig.EventInfo.REPLY_ANY_EXCEPT_NO), + Integer.toString(ZenModeConfig.EventInfo.REPLY_YES_OR_MAYBE), + Integer.toString(ZenModeConfig.EventInfo.REPLY_YES), + }); + mReply.setOnPreferenceChangeListener(mReplyChangeListener); + + // Parse the zen mode's condition to update our EventInfo object. + mEvent = ZenModeConfig.tryParseEventConditionId(zenMode.getRule().getConditionId()); + if (mEvent != null) { + reloadCalendar(); + updatePrefValues(); + } + } + + private void reloadCalendar() { + List calendars = getCalendars(mContext); + ArrayList entries = new ArrayList<>(); + ArrayList values = new ArrayList<>(); + entries.add(mContext.getString(R.string.zen_mode_event_rule_calendar_any)); + values.add(key(0, null, "")); + final String eventCalendar = mEvent != null ? mEvent.calName : null; + for (CalendarInfo calendar : calendars) { + entries.add(calendar.name); + values.add(key(calendar)); + if (eventCalendar != null && (mEvent.calendarId == null + && eventCalendar.equals(calendar.name))) { + mEvent.calendarId = calendar.calendarId; + } + } + + CharSequence[] entriesArr = entries.toArray(new CharSequence[entries.size()]); + CharSequence[] valuesArr = values.toArray(new CharSequence[values.size()]); + if (!Arrays.equals(mCalendar.getEntries(), entriesArr)) { + mCalendar.setEntries(entriesArr); + } + + if (!Arrays.equals(mCalendar.getEntryValues(), valuesArr)) { + mCalendar.setEntryValues(valuesArr); + } + } + + @VisibleForTesting + protected Function updateEventMode(ZenModeConfig.EventInfo event) { + return (zenMode) -> { + zenMode.getRule().setConditionId(ZenModeConfig.toEventConditionId(event)); + if (Flags.modesApi() && Flags.modesUi()) { + zenMode.getRule().setTriggerDescription( + SystemZenRules.getTriggerDescriptionForScheduleEvent(mContext, event)); + } + return zenMode; + }; + } + + Preference.OnPreferenceChangeListener mCalendarChangeListener = + new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + final String calendarKey = (String) newValue; + if (calendarKey.equals(key(mEvent))) return false; + String[] key = calendarKey.split(":", 3); + mEvent.userId = Integer.parseInt(key[0]); + mEvent.calendarId = key[1].equals("") ? null : Long.parseLong(key[1]); + mEvent.calName = key[2].equals("") ? null : key[2]; + saveMode(updateEventMode(mEvent)); + return true; + } + }; + + Preference.OnPreferenceChangeListener mReplyChangeListener = + new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + final int reply = Integer.parseInt((String) newValue); + if (reply == mEvent.reply) return false; + mEvent.reply = reply; + saveMode(updateEventMode(mEvent)); + return true; + } + }; + + private void updatePrefValues() { + if (!Objects.equals(mCalendar.getValue(), key(mEvent))) { + mCalendar.setValue(key(mEvent)); + } + if (!Objects.equals(mReply.getValue(), Integer.toString(mEvent.reply))) { + mReply.setValue(Integer.toString(mEvent.reply)); + } + } + + private List getCalendars(Context context) { + final List calendars = new ArrayList<>(); + for (UserHandle user : UserManager.get(context).getUserProfiles()) { + final Context userContext = getContextForUser(context, user); + if (userContext != null) { + addCalendars(userContext, calendars); + } + } + Collections.sort(calendars, CALENDAR_NAME); + return calendars; + } + + private static Context getContextForUser(Context context, UserHandle user) { + try { + return context.createPackageContextAsUser(context.getPackageName(), 0, user); + } catch (PackageManager.NameNotFoundException e) { + return null; + } + } + + private void addCalendars(Context context, List outCalendars) { + final String[] projection = + {CalendarContract.Calendars._ID, CalendarContract.Calendars.CALENDAR_DISPLAY_NAME}; + final String selection = CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL + " >= " + + CalendarContract.Calendars.CAL_ACCESS_CONTRIBUTOR + + " AND " + CalendarContract.Calendars.SYNC_EVENTS + " = 1"; + Cursor cursor = null; + try { + cursor = context.getContentResolver().query(CalendarContract.Calendars.CONTENT_URI, + projection, selection, null, null); + if (cursor == null) { + return; + } + while (cursor.moveToNext()) { + addCalendar(cursor.getLong(0), cursor.getString(1), + context.getUserId(), outCalendars); + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + + @VisibleForTesting + protected static void addCalendar(long calendarId, String calName, int userId, + List outCalendars) { + final CalendarInfo ci = new CalendarInfo(); + ci.calendarId = calendarId; + ci.name = calName; + ci.userId = userId; + if (!outCalendars.contains(ci)) { + outCalendars.add(ci); + } + } + + private static String key(CalendarInfo calendar) { + return key(calendar.userId, calendar.calendarId, calendar.name); + } + + private static String key(ZenModeConfig.EventInfo event) { + return key(event.userId, event.calendarId, event.calName); + } + + @VisibleForTesting + protected static String key(int userId, Long calendarId, String displayName) { + return ZenModeConfig.EventInfo.resolveUserId(userId) + ":" + + (calendarId == null ? "" : calendarId) + + ":" + (displayName == null ? "" : displayName); + } + + @VisibleForTesting + protected static final Comparator CALENDAR_NAME = Comparator.comparing( + lhs -> lhs.name); + + public static class CalendarInfo { + public String name; + public int userId; + public Long calendarId; + + @Override + public boolean equals(Object o) { + if (!(o instanceof CalendarInfo)) return false; + if (o == this) return true; + final CalendarInfo other = (CalendarInfo) o; + return Objects.equals(other.name, name) + && Objects.equals(other.calendarId, calendarId); + } + + @Override + public int hashCode() { + return Objects.hash(name, calendarId); + } + } +} diff --git a/src/com/android/settings/notification/modes/ZenModeSetTriggerLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeSetTriggerLinkPreferenceController.java new file mode 100644 index 00000000000..a3bc508cfbb --- /dev/null +++ b/src/com/android/settings/notification/modes/ZenModeSetTriggerLinkPreferenceController.java @@ -0,0 +1,96 @@ +/* + * 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.modes; + +import static android.app.AutomaticZenRule.TYPE_SCHEDULE_CALENDAR; + +import static com.android.settings.notification.modes.ZenModeFragmentBase.MODE_ID; + +import android.content.Context; +import android.os.Bundle; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; + +import com.android.settings.R; +import com.android.settings.core.SubSettingLauncher; +import com.android.settingslib.PrimarySwitchPreference; + +/** + * Preference controller for the link + */ +public class ZenModeSetTriggerLinkPreferenceController extends AbstractZenModePreferenceController { + @VisibleForTesting + protected static final String AUTOMATIC_TRIGGER_PREF_KEY = "zen_automatic_trigger_settings"; + + public ZenModeSetTriggerLinkPreferenceController(Context context, String key, + ZenModesBackend backend) { + super(context, key, backend); + } + + @Override + public boolean isAvailable(@NonNull ZenMode zenMode) { + return !zenMode.isManualDnd(); + } + + @Override + public void updateState(Preference preference, @NonNull ZenMode zenMode) { + // This controller is expected to govern a preference category so that it controls the + // availability of the entire preference category if the mode doesn't have a way to + // automatically trigger (such as manual DND). + Preference switchPref = ((PreferenceCategory) preference).findPreference( + AUTOMATIC_TRIGGER_PREF_KEY); + if (switchPref == null) { + return; + } + ((PrimarySwitchPreference) switchPref).setChecked(zenMode.getRule().isEnabled()); + switchPref.setOnPreferenceChangeListener(mSwitchChangeListener); + + Bundle bundle = new Bundle(); + bundle.putString(MODE_ID, zenMode.getId()); + + // TODO: b/341961712 - direct preference to app-owned intent if available + switch (zenMode.getRule().getType()) { + case TYPE_SCHEDULE_CALENDAR: + switchPref.setTitle(R.string.zen_mode_set_calendar_link); + switchPref.setSummary(zenMode.getRule().getTriggerDescription()); + switchPref.setIntent(new SubSettingLauncher(mContext) + .setDestination(ZenModeSetCalendarFragment.class.getName()) + // TODO: b/332937635 - set correct metrics category + .setSourceMetricsCategory(0) + .setArguments(bundle) + .toIntent()); + break; + default: + // TODO: b/342156843 - change this to allow adding a trigger condition for system + // rules that don't yet have a type selected + switchPref.setTitle("not implemented"); + } + } + + @VisibleForTesting + protected Preference.OnPreferenceChangeListener mSwitchChangeListener = (p, newValue) -> { + final boolean newEnabled = (Boolean) newValue; + return saveMode((zenMode) -> { + if (newEnabled != zenMode.getRule().isEnabled()) { + zenMode.getRule().setEnabled(newEnabled); + } + return zenMode; + }); + }; +} diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeSetCalendarPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeSetCalendarPreferenceControllerTest.java new file mode 100644 index 00000000000..6b24fa21832 --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeSetCalendarPreferenceControllerTest.java @@ -0,0 +1,151 @@ +/* + * 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.modes; + +import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT; +import static android.service.notification.ZenModeConfig.EventInfo.REPLY_YES; + +import static com.android.settings.notification.modes.ZenModeSetCalendarPreferenceController.CALENDAR_NAME; +import static com.android.settings.notification.modes.ZenModeSetCalendarPreferenceController.KEY_CALENDAR; +import static com.android.settings.notification.modes.ZenModeSetCalendarPreferenceController.KEY_REPLY; +import static com.android.settings.notification.modes.ZenModeSetCalendarPreferenceController.addCalendar; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.when; + +import android.app.AutomaticZenRule; +import android.app.Flags; +import android.content.Context; +import android.net.Uri; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; +import android.service.notification.ZenModeConfig; + +import androidx.preference.DropDownPreference; +import androidx.preference.PreferenceCategory; +import androidx.test.core.app.ApplicationProvider; + +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 java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@RunWith(RobolectricTestRunner.class) +public class ZenModeSetCalendarPreferenceControllerTest { + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT); + + @Mock + private ZenModesBackend mBackend; + private Context mContext; + + @Mock + private PreferenceCategory mPrefCategory; + private DropDownPreference mCalendar, mReply; + + private ZenModeSetCalendarPreferenceController mPrefController; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = ApplicationProvider.getApplicationContext(); + + mCalendar = new DropDownPreference(mContext); + mReply = new DropDownPreference(mContext); + when(mPrefCategory.findPreference(KEY_CALENDAR)).thenReturn(mCalendar); + when(mPrefCategory.findPreference(KEY_REPLY)).thenReturn(mReply); + + mPrefController = new ZenModeSetCalendarPreferenceController(mContext, + "zen_mode_event_category", mBackend); + } + + @Test + @EnableFlags({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI}) + public void updateEventMode_updatesConditionAndTriggerDescription() { + ZenMode mode = new ZenMode("id", + new AutomaticZenRule.Builder("name", Uri.parse("condition")).build(), + true); // is active + + // Explicitly update preference controller with mode info first, which will also call + // updateState() + mPrefController.updateZenMode(mPrefCategory, mode); + + ZenModeConfig.EventInfo eventInfo = new ZenModeConfig.EventInfo(); + eventInfo.calendarId = 1L; + eventInfo.calName = "My events"; + + // apply event mode updater to existing mode + ZenMode out = mPrefController.updateEventMode(eventInfo).apply(mode); + + assertThat(out.getRule().getConditionId()).isEqualTo( + ZenModeConfig.toEventConditionId(eventInfo)); + assertThat(out.getRule().getTriggerDescription()).isEqualTo("My events"); + } + + @Test + public void updateState_setsPreferenceValues() { + ZenModeConfig.EventInfo eventInfo = new ZenModeConfig.EventInfo(); + eventInfo.calendarId = 1L; + eventInfo.calName = "Definitely A Calendar"; + eventInfo.reply = REPLY_YES; + + ZenMode mode = new ZenMode("id", + new AutomaticZenRule.Builder("name", + ZenModeConfig.toEventConditionId(eventInfo)).build(), + true); // is active + mPrefController.updateZenMode(mPrefCategory, mode); + + // We should see mCalendar, mReply have their values set + assertThat(mCalendar.getValue()).isEqualTo( + ZenModeSetCalendarPreferenceController.key(eventInfo.userId, eventInfo.calendarId, + eventInfo.calName)); + assertThat(mReply.getValue()).isEqualTo(Integer.toString(eventInfo.reply)); + } + + @Test + public void testNoDuplicateCalendars() { + List calendarsList = new ArrayList<>(); + addCalendar(1234, "calName", 1, calendarsList); + addCalendar(1234, "calName", 2, calendarsList); + addCalendar(1234, "calName", 3, calendarsList); + assertThat(calendarsList).hasSize(1); + } + + @Test + public void testCalendarInfoSortByName() { + List calendarsList = new ArrayList<>(); + addCalendar(123, "zyx", 1, calendarsList); + addCalendar(456, "wvu", 2, calendarsList); + addCalendar(789, "abc", 3, calendarsList); + Collections.sort(calendarsList, CALENDAR_NAME); + + List sortedList = new ArrayList<>(); + addCalendar(789, "abc", 3, sortedList); + addCalendar(456, "wvu", 2, sortedList); + addCalendar(123, "zyx", 1, sortedList); + + assertThat(calendarsList).containsExactlyElementsIn(sortedList).inOrder(); + } +} diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeSetTriggerLinkPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeSetTriggerLinkPreferenceControllerTest.java new file mode 100644 index 00000000000..7dcec1cfeed --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeSetTriggerLinkPreferenceControllerTest.java @@ -0,0 +1,170 @@ +/* + * 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.modes; + +import static android.app.AutomaticZenRule.TYPE_SCHEDULE_CALENDAR; +import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY; +import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT; + +import static com.android.settings.notification.modes.ZenModeSetTriggerLinkPreferenceController.AUTOMATIC_TRIGGER_PREF_KEY; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.AutomaticZenRule; +import android.app.Flags; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; +import android.service.notification.ZenModeConfig; +import android.service.notification.ZenPolicy; + +import androidx.preference.PreferenceCategory; +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.R; +import com.android.settings.SettingsActivity; +import com.android.settingslib.PrimarySwitchPreference; + +import org.junit.Before; +import org.junit.Rule; +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; + +@RunWith(RobolectricTestRunner.class) +public class ZenModeSetTriggerLinkPreferenceControllerTest { + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT); + + @Mock + private ZenModesBackend mBackend; + private Context mContext; + + @Mock + private PreferenceCategory mPrefCategory; + @Mock + private PrimarySwitchPreference mPreference; + private ZenModeSetTriggerLinkPreferenceController mPrefController; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = ApplicationProvider.getApplicationContext(); + + mPrefController = new ZenModeSetTriggerLinkPreferenceController(mContext, + "zen_automatic_trigger_category", mBackend); + when(mPrefCategory.findPreference(AUTOMATIC_TRIGGER_PREF_KEY)).thenReturn(mPreference); + } + + @Test + @EnableFlags(Flags.FLAG_MODES_UI) + public void testIsAvailable() { + // should not be available for manual DND + ZenMode manualMode = ZenMode.manualDndMode(new AutomaticZenRule.Builder("Do Not Disturb", + Uri.parse("manual")) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .build(), true); + + mPrefController.updateZenMode(mPrefCategory, manualMode); + assertThat(mPrefController.isAvailable()).isFalse(); + + // should be available for other modes + ZenMode zenMode = new ZenMode("id", + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().allowAlarms(true).build()) + .setEnabled(false) + .build(), false); + mPrefController.updateZenMode(mPrefCategory, zenMode); + assertThat(mPrefController.isAvailable()).isTrue(); + } + + @Test + public void testUpdateState() { + ZenMode zenMode = new ZenMode("id", + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().allowAlarms(true).build()) + .setEnabled(false) + .build(), false); + + // Update preference controller with a zen mode that is not enabled + mPrefController.updateZenMode(mPrefCategory, zenMode); + verify(mPreference).setChecked(false); + + // Now with the rule enabled + zenMode.getRule().setEnabled(true); + mPrefController.updateZenMode(mPrefCategory, zenMode); + verify(mPreference).setChecked(true); + } + + @Test + public void testOnPreferenceChange() { + ZenMode zenMode = new ZenMode("id", + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().allowAlarms(true).build()) + .setEnabled(false) + .build(), false); + + // start with disabled rule + mPrefController.updateZenMode(mPrefCategory, zenMode); + + // then update the preference to be checked + mPrefController.mSwitchChangeListener.onPreferenceChange(mPreference, true); + + // verify the backend got asked to update the mode to be enabled + ArgumentCaptor captor = ArgumentCaptor.forClass(ZenMode.class); + verify(mBackend).updateMode(captor.capture()); + assertThat(captor.getValue().getRule().isEnabled()).isTrue(); + } + + @Test + public void testRuleLink_calendar() { + ZenModeConfig.EventInfo eventInfo = new ZenModeConfig.EventInfo(); + eventInfo.calendarId = 1L; + eventInfo.calName = "My events"; + ZenMode mode = new ZenMode("id", new AutomaticZenRule.Builder("name", + ZenModeConfig.toEventConditionId(eventInfo)) + .setType(TYPE_SCHEDULE_CALENDAR) + .setTriggerDescription("My events") + .build(), + true); // is active + mPrefController.updateZenMode(mPrefCategory, mode); + + verify(mPreference).setTitle(R.string.zen_mode_set_calendar_link); + verify(mPreference).setSummary(mode.getRule().getTriggerDescription()); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Intent.class); + verify(mPreference).setIntent(captor.capture()); + // Destination as written into the intent by SubSettingLauncher + assertThat( + captor.getValue().getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT)).isEqualTo( + ZenModeSetCalendarFragment.class.getName()); + } +} From 2af266f42389c1d7cb01b2c01f1a9e8f525bb147 Mon Sep 17 00:00:00 2001 From: Julia Reynolds Date: Tue, 28 May 2024 16:29:16 -0400 Subject: [PATCH 04/12] Update check boxes to switches To match the rest of the settings Test: ZenModeNotifVisPreferenceControllerTest Bug: 337087926 Flag: android.app.modes_ui Change-Id: I193db8404a3b6c8bdacbbb9e19e3a7c1826d745f --- res/xml/modes_notif_vis_settings.xml | 14 ++++++------ .../ZenModeNotifVisPreferenceController.java | 10 ++++----- ...nModeNotifVisPreferenceControllerTest.java | 22 +++++++++---------- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/res/xml/modes_notif_vis_settings.xml b/res/xml/modes_notif_vis_settings.xml index 551c704a24a..10baf5f2f1a 100644 --- a/res/xml/modes_notif_vis_settings.xml +++ b/res/xml/modes_notif_vis_settings.xml @@ -24,15 +24,15 @@ android:title="@string/zen_mode_block_effects_screen_off" android:key="zen_mode_block_screen_off"> - - - @@ -40,19 +40,19 @@ - - - - diff --git a/src/com/android/settings/notification/modes/ZenModeNotifVisPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeNotifVisPreferenceController.java index 39f0d3cb9dc..f918b256caf 100644 --- a/src/com/android/settings/notification/modes/ZenModeNotifVisPreferenceController.java +++ b/src/com/android/settings/notification/modes/ZenModeNotifVisPreferenceController.java @@ -23,6 +23,7 @@ import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import androidx.preference.CheckBoxPreference; import androidx.preference.Preference; +import androidx.preference.TwoStatePreference; import com.android.settings.widget.DisabledCheckBoxPreference; @@ -57,7 +58,6 @@ public class ZenModeNotifVisPreferenceController extends AbstractZenModePreferen @Override public void updateState(Preference preference, @NonNull ZenMode zenMode) { - boolean suppressed = !zenMode.getPolicy().isVisualEffectAllowed(mEffect, false); boolean parentSuppressed = false; if (mParentSuppressedEffects != null) { @@ -68,12 +68,12 @@ public class ZenModeNotifVisPreferenceController extends AbstractZenModePreferen } } if (parentSuppressed) { - ((CheckBoxPreference) preference).setChecked(true); + ((TwoStatePreference) preference).setChecked(true); onPreferenceChange(preference, true); - ((DisabledCheckBoxPreference) preference).enableCheckbox(false); + preference.setEnabled(false); } else { - ((DisabledCheckBoxPreference) preference).enableCheckbox(true); - ((CheckBoxPreference) preference).setChecked(suppressed); + preference.setEnabled(true); + ((TwoStatePreference) preference).setChecked(suppressed); } } diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeNotifVisPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeNotifVisPreferenceControllerTest.java index 05b48480d41..54edaf440e3 100644 --- a/tests/robotests/src/com/android/settings/notification/modes/ZenModeNotifVisPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeNotifVisPreferenceControllerTest.java @@ -41,7 +41,7 @@ import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import android.service.notification.ZenPolicy; -import com.android.settings.widget.DisabledCheckBoxPreference; +import androidx.preference.TwoStatePreference; import org.junit.Before; import org.junit.Rule; @@ -95,7 +95,7 @@ public final class ZenModeNotifVisPreferenceControllerTest { @Test public void updateState_notChecked() { - DisabledCheckBoxPreference preference = mock(DisabledCheckBoxPreference.class); + TwoStatePreference preference = mock(TwoStatePreference.class); ZenMode zenMode = new ZenMode("id", new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) .setType(AutomaticZenRule.TYPE_DRIVING) @@ -109,12 +109,12 @@ public final class ZenModeNotifVisPreferenceControllerTest { mController.updateZenMode(preference, zenMode); verify(preference).setChecked(false); - verify(preference).enableCheckbox(true); + verify(preference).setEnabled(true); } @Test public void updateState_checked() { - DisabledCheckBoxPreference preference = mock(DisabledCheckBoxPreference.class); + TwoStatePreference preference = mock(TwoStatePreference.class); ZenMode zenMode = new ZenMode("id", new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) .setType(AutomaticZenRule.TYPE_DRIVING) @@ -128,12 +128,12 @@ public final class ZenModeNotifVisPreferenceControllerTest { mController.updateZenMode(preference, zenMode); verify(preference).setChecked(true); - verify(preference).enableCheckbox(true); + verify(preference).setEnabled(true); } @Test public void updateState_checkedFalse_parentChecked() { - DisabledCheckBoxPreference preference = mock(DisabledCheckBoxPreference.class); + TwoStatePreference preference = mock(TwoStatePreference.class); mController = new ZenModeNotifVisPreferenceController(mContext, "zen_effect_status", VISUAL_EFFECT_STATUS_BAR, new int[]{VISUAL_EFFECT_NOTIFICATION_LIST}, mBackend); @@ -152,7 +152,7 @@ public final class ZenModeNotifVisPreferenceControllerTest { mController.updateZenMode(preference, zenMode); verify(preference).setChecked(true); - verify(preference).enableCheckbox(false); + verify(preference).setEnabled(false); ArgumentCaptor captor = ArgumentCaptor.forClass(ZenMode.class); verify(mBackend).updateMode(captor.capture()); assertThat(captor.getValue().getPolicy().getVisualEffectStatusBar()) @@ -163,7 +163,7 @@ public final class ZenModeNotifVisPreferenceControllerTest { @Test public void updateState_checkedFalse_parentNotChecked() { - DisabledCheckBoxPreference preference = mock(DisabledCheckBoxPreference.class); + TwoStatePreference preference = mock(TwoStatePreference.class); mController = new ZenModeNotifVisPreferenceController(mContext, "zen_effect_status", VISUAL_EFFECT_STATUS_BAR, new int[]{VISUAL_EFFECT_NOTIFICATION_LIST}, mBackend); @@ -181,13 +181,13 @@ public final class ZenModeNotifVisPreferenceControllerTest { mController.updateZenMode(preference, zenMode); verify(preference).setChecked(false); - verify(preference).enableCheckbox(true); + verify(preference).setEnabled(true); verify(mBackend, never()).updateMode(any()); } @Test public void onPreferenceChanged_checkedFalse() { - DisabledCheckBoxPreference preference = mock(DisabledCheckBoxPreference.class); + TwoStatePreference preference = mock(TwoStatePreference.class); ZenMode zenMode = new ZenMode("id", new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) .setType(AutomaticZenRule.TYPE_DRIVING) @@ -212,7 +212,7 @@ public final class ZenModeNotifVisPreferenceControllerTest { @Test public void onPreferenceChanged_checkedTrue() { - DisabledCheckBoxPreference preference = mock(DisabledCheckBoxPreference.class); + TwoStatePreference preference = mock(TwoStatePreference.class); ZenMode zenMode = new ZenMode("id", new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) .setType(AutomaticZenRule.TYPE_DRIVING) From 5fd6d6435b4f692008c1b851d265bd1df9987462 Mon Sep 17 00:00:00 2001 From: josephpv Date: Wed, 29 May 2024 09:55:43 +0000 Subject: [PATCH 05/12] Add footer in private space auto lock settings page Add a footer note to private space auto lock settings page that apps in private space may need to be authenticated unlock when a separate lock is set for private space. Screenshot: go/ss/7fE8epun3A2hgq4.png Bug: 343166689 Test: Manual Change-Id: I18c650eba128da512116a3166babb49f7ef33bb3 --- res/values/strings.xml | 2 ++ .../privatespace/autolock/AutoLockSettingsFragment.java | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/res/values/strings.xml b/res/values/strings.xml index 3e097b87726..25212380e74 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1280,6 +1280,8 @@ 5 minutes after screen timeout Only after device restarts + + If you use a different lock for your private space, you may need to verify it\u2019s you to open apps in your private space. Hide private space diff --git a/src/com/android/settings/privatespace/autolock/AutoLockSettingsFragment.java b/src/com/android/settings/privatespace/autolock/AutoLockSettingsFragment.java index cb332d1e515..decca843e29 100644 --- a/src/com/android/settings/privatespace/autolock/AutoLockSettingsFragment.java +++ b/src/com/android/settings/privatespace/autolock/AutoLockSettingsFragment.java @@ -31,6 +31,7 @@ import com.android.settings.R; import com.android.settings.privatespace.PrivateSpaceMaintainer; import com.android.settings.widget.RadioButtonPickerFragment; import com.android.settingslib.widget.CandidateInfo; +import com.android.settingslib.widget.FooterPreference; import com.android.settingslib.widget.TopIntroPreference; import java.util.ArrayList; @@ -76,7 +77,10 @@ public class AutoLockSettingsFragment extends RadioButtonPickerFragment { protected void addStaticPreferences(PreferenceScreen screen) { final TopIntroPreference introPreference = new TopIntroPreference(screen.getContext()); introPreference.setTitle(R.string.private_space_auto_lock_page_summary); + final FooterPreference footerPreference = new FooterPreference(screen.getContext()); + footerPreference.setSummary(R.string.private_space_auto_lock_footer_message); screen.addPreference(introPreference); + screen.addPreference(footerPreference); } @Override From c233ecca5c31f9ed18c3ff92bbae4b54c7622cb0 Mon Sep 17 00:00:00 2001 From: Lifu Tang Date: Wed, 29 May 2024 20:52:18 +0000 Subject: [PATCH 06/12] Change the relative time format to `LONG` The `SHORT` format, which abbreviates relative time as 'x min. ago,' presents an accessibility issue for screen readers like TalkBack. TalkBack announces this as 'x minimum. Ago', and the dot after "min" is treated as period and causes a full-stop. Attempts to directly modify TalkBack's pronunciation of this text have proven difficult. Given the ample space available in the user interface, using abbreviations isn't necessary. Switching to the `LONG` format ('x minutes ago') would resolve this accessibility problem while also making the interface clearer for all users. Bug: 318323722 Change-Id: I42ba960bd8d5d8fc8a9cb895ecf24c56dab6d2c4 --- .../android/settings/applications/AppsPreferenceController.java | 2 +- .../location/RecentLocationAccessPreferenceController.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/com/android/settings/applications/AppsPreferenceController.java b/src/com/android/settings/applications/AppsPreferenceController.java index 963376662b7..02ddc1d2436 100644 --- a/src/com/android/settings/applications/AppsPreferenceController.java +++ b/src/com/android/settings/applications/AppsPreferenceController.java @@ -206,7 +206,7 @@ public class AppsPreferenceController extends BasePreferenceController implement pref.setIcon(Utils.getBadgedIcon(mContext, appEntry.info)); pref.setSummary(StringUtil.formatRelativeTime(mContext, System.currentTimeMillis() - stats.getLastTimeUsed(), false, - RelativeDateTimeFormatter.Style.SHORT)); + RelativeDateTimeFormatter.Style.LONG)); pref.setOrder(showAppsCount++); pref.setOnPreferenceClickListener(preference -> { startAppInfoSettings(pkgName, appEntry.info.uid, diff --git a/src/com/android/settings/location/RecentLocationAccessPreferenceController.java b/src/com/android/settings/location/RecentLocationAccessPreferenceController.java index a5c0e55a74d..3cb30251c51 100644 --- a/src/com/android/settings/location/RecentLocationAccessPreferenceController.java +++ b/src/com/android/settings/location/RecentLocationAccessPreferenceController.java @@ -172,7 +172,7 @@ public class RecentLocationAccessPreferenceController extends LocationBasePrefer pref.setTitle(access.label); pref.setSummary(StringUtil.formatRelativeTime(prefContext, System.currentTimeMillis() - access.accessFinishTime, false, - RelativeDateTimeFormatter.Style.SHORT)); + RelativeDateTimeFormatter.Style.LONG)); pref.setOnPreferenceClickListener(new PackageEntryClickedListener( fragment.getContext(), access.packageName, access.userHandle)); return pref; From 985b27c6a1b10210dc112710ae9bcfe1d27bc6f3 Mon Sep 17 00:00:00 2001 From: Ram Peri Date: Wed, 29 May 2024 15:16:19 -0700 Subject: [PATCH 07/12] Disable ChooseLockTypeDialogFragmentTest for robolectric Flag: NA Bug: 342667939 Test: atest SettingsRoboTests Change-Id: I13807fd0feb399ab000350152e1de7d67b4e96da --- .../settings/password/ChooseLockTypeDialogFragmentTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/robotests/src/com/android/settings/password/ChooseLockTypeDialogFragmentTest.java b/tests/robotests/src/com/android/settings/password/ChooseLockTypeDialogFragmentTest.java index 24418bfeb41..68f8ed736d1 100644 --- a/tests/robotests/src/com/android/settings/password/ChooseLockTypeDialogFragmentTest.java +++ b/tests/robotests/src/com/android/settings/password/ChooseLockTypeDialogFragmentTest.java @@ -37,6 +37,7 @@ import com.android.settings.testutils.shadow.ShadowAlertDialogCompat; import com.android.settings.testutils.shadow.ShadowLockPatternUtils; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @@ -46,6 +47,7 @@ import org.robolectric.shadows.androidx.fragment.FragmentController; @RunWith(RobolectricTestRunner.class) @Config(shadows = {ShadowAlertDialogCompat.class, ShadowLockPatternUtils.class}) +@Ignore("b/342667939") public class ChooseLockTypeDialogFragmentTest { private Context mContext; From 5920567beeaecd6c6c2b8b54f48acfecabcfc52b Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Thu, 30 May 2024 12:26:30 +0800 Subject: [PATCH 08/12] Remove unused "androidx.test.rules" from production Bug: 316941721 Test: m Settings Change-Id: I995efda7e4b9dbcb7a3dafcda6da0346153fa60e --- Android.bp | 1 - 1 file changed, 1 deletion(-) diff --git a/Android.bp b/Android.bp index 1d713a8561b..06ce8ab22f5 100644 --- a/Android.bp +++ b/Android.bp @@ -109,7 +109,6 @@ android_library { "settings-logtags", "settings-telephony-protos-lite", "statslog-settings", - "androidx.test.rules", "telephony_flags_core_java_lib", "setupdesign-lottie-loading-layout", "device_policy_aconfig_flags_lib", From 942622575ac1c47382eb3110f738930fa39bcd20 Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Tue, 28 May 2024 13:28:10 +0800 Subject: [PATCH 09/12] Disable "Reset app preference" during call Reset app preference will dismiss all the notification, disable "Reset app preference" during call so in call notification not get dismissed. Fix: 342627537 Test: manual - try "Reset app preference" during call Change-Id: Ida9961ca30dc80c05d4de5bc9faac3203967f3d3 --- .../settings/network/SimOnboardingActivity.kt | 13 ++++++------- .../settings/spa/app/ResetAppPreferences.kt | 16 ++++++++++++---- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/com/android/settings/network/SimOnboardingActivity.kt b/src/com/android/settings/network/SimOnboardingActivity.kt index 3a210493785..606e46f785a 100644 --- a/src/com/android/settings/network/SimOnboardingActivity.kt +++ b/src/com/android/settings/network/SimOnboardingActivity.kt @@ -499,14 +499,13 @@ class SimOnboardingActivity : SpaBaseDialogActivity() { SettingsAlertDialogWithIcon( onDismissRequest = cancelAction, confirmButton = AlertDialogButton( - getString(R.string.sim_onboarding_setup), - nextAction + text = getString(R.string.sim_onboarding_setup), + onClick = nextAction, + ), + dismissButton = AlertDialogButton( + text = getString(R.string.sim_onboarding_close), + onClick = cancelAction, ), - dismissButton = - AlertDialogButton( - getString(R.string.sim_onboarding_close), - cancelAction - ), title = stringResource(R.string.sim_onboarding_dialog_starting_title), icon = { Icon( diff --git a/src/com/android/settings/spa/app/ResetAppPreferences.kt b/src/com/android/settings/spa/app/ResetAppPreferences.kt index 34c4145e03f..2ce154c86f7 100644 --- a/src/com/android/settings/spa/app/ResetAppPreferences.kt +++ b/src/com/android/settings/spa/app/ResetAppPreferences.kt @@ -19,11 +19,14 @@ package com.android.settings.spa.app import android.os.UserManager import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.settings.R import com.android.settings.applications.manageapplications.ResetAppsHelper +import com.android.settings.network.telephony.CallStateRepository import com.android.settingslib.spa.widget.dialog.AlertDialogButton import com.android.settingslib.spa.widget.dialog.AlertDialogPresenter import com.android.settingslib.spa.widget.dialog.rememberAlertDialogPresenter @@ -35,9 +38,7 @@ import com.android.settingslib.spaprivileged.template.scaffold.RestrictedMenuIte fun MoreOptionsScope.ResetAppPreferences(onClick: () -> Unit) { RestrictedMenuItem( text = stringResource(R.string.reset_app_preferences), - restrictions = remember { - Restrictions(keys = listOf(UserManager.DISALLOW_APPS_CONTROL)) - }, + restrictions = Restrictions(keys = listOf(UserManager.DISALLOW_APPS_CONTROL)), onClick = onClick, ) } @@ -45,8 +46,15 @@ fun MoreOptionsScope.ResetAppPreferences(onClick: () -> Unit) { @Composable fun rememberResetAppDialogPresenter(): AlertDialogPresenter { val context = LocalContext.current + // Reset app preference will dismiss all the notification, disable "Reset app preference" during + // call so in call notification not get dismissed. + val isInCall by remember { CallStateRepository(context).isInCallFlow() } + .collectAsStateWithLifecycle(initialValue = false) return rememberAlertDialogPresenter( - confirmButton = AlertDialogButton(stringResource(R.string.reset_app_preferences_button)) { + confirmButton = AlertDialogButton( + text = stringResource(R.string.reset_app_preferences_button), + enabled = !isInCall, + ) { ResetAppsHelper(context).resetApps() }, dismissButton = AlertDialogButton(stringResource(R.string.cancel)), From 1bfea5d472bf0830a28150546e3fd7918fd961e0 Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Thu, 30 May 2024 16:37:51 +0800 Subject: [PATCH 10/12] Check if ECBMode when deactivate SIM card If in ECBMode, start ACTION_SHOW_NOTICE_ECM_BLOCK_OTHERS to show a dialog instead. This align with the current airplane mode switch. Fix: 191943857 Test: adb shell cmd phone emergency-callback-mode Test: unit test Change-Id: Icf646cd76990d621121b4367ec0fd02a3880b85c --- .../MobileNetworkSwitchController.kt | 10 +++- .../SubscriptionActivationRepository.kt | 41 ++++++++++++++ .../settings/spa/network/SimsSection.kt | 16 +++--- .../SubscriptionActivationRepositoryTest.kt | 55 ++++++++++++++++++- 4 files changed, 111 insertions(+), 11 deletions(-) diff --git a/src/com/android/settings/network/telephony/MobileNetworkSwitchController.kt b/src/com/android/settings/network/telephony/MobileNetworkSwitchController.kt index 6c5127f90eb..63364f9d990 100644 --- a/src/com/android/settings/network/telephony/MobileNetworkSwitchController.kt +++ b/src/com/android/settings/network/telephony/MobileNetworkSwitchController.kt @@ -21,14 +21,15 @@ import android.telephony.SubscriptionManager import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.settings.R -import com.android.settings.network.SubscriptionUtil import com.android.settings.spa.preference.ComposePreferenceController import com.android.settingslib.spa.widget.preference.MainSwitchPreference import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel +import kotlinx.coroutines.launch class MobileNetworkSwitchController @JvmOverloads constructor( context: Context, @@ -56,12 +57,15 @@ class MobileNetworkSwitchController @JvmOverloads constructor( val changeable by remember { subscriptionActivationRepository.isActivationChangeableFlow() }.collectAsStateWithLifecycle(initialValue = true) + val coroutineScope = rememberCoroutineScope() MainSwitchPreference(model = object : SwitchPreferenceModel { override val title = stringResource(R.string.mobile_network_use_sim_on) override val changeable = { changeable } override val checked = { checked } - override val onCheckedChange = { newChecked: Boolean -> - SubscriptionUtil.startToggleSubscriptionDialogActivity(mContext, subId, newChecked) + override val onCheckedChange: (Boolean) -> Unit = { newChecked -> + coroutineScope.launch { + subscriptionActivationRepository.setActive(subId, newChecked) + } } }) } diff --git a/src/com/android/settings/network/telephony/SubscriptionActivationRepository.kt b/src/com/android/settings/network/telephony/SubscriptionActivationRepository.kt index 416dda19a2c..185af0c3d09 100644 --- a/src/com/android/settings/network/telephony/SubscriptionActivationRepository.kt +++ b/src/com/android/settings/network/telephony/SubscriptionActivationRepository.kt @@ -17,9 +17,18 @@ package com.android.settings.network.telephony import android.content.Context +import android.content.Intent +import android.telephony.SubscriptionManager +import android.telephony.TelephonyManager.ACTION_SHOW_NOTICE_ECM_BLOCK_OTHERS +import android.util.Log +import com.android.settings.Utils +import com.android.settings.flags.Flags import com.android.settings.network.SatelliteRepository +import com.android.settings.network.SimOnboardingActivity.Companion.startSimOnboardingActivity +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.withContext class SubscriptionActivationRepository( private val context: Context, @@ -32,4 +41,36 @@ class SubscriptionActivationRepository( ) { isInCall, isSatelliteModemEnabled -> !isInCall && !isSatelliteModemEnabled } + + /** + * Starts a dialog activity to handle SIM enabling / disabling. + * @param subId The id of subscription need to be enabled or disabled. + * @param active Whether the subscription with [subId] should be enabled or disabled. + */ + suspend fun setActive(subId: Int, active: Boolean) { + if (!SubscriptionManager.isUsableSubscriptionId(subId)) { + Log.i(TAG, "Unable to toggle subscription due to unusable subscription ID.") + return + } + if (!active && isEmergencyCallbackMode(subId)) { + val intent = Intent(ACTION_SHOW_NOTICE_ECM_BLOCK_OTHERS).apply { + setPackage(Utils.PHONE_PACKAGE_NAME) + } + context.startActivity(intent) + return + } + if (active && Flags.isDualSimOnboardingEnabled()) { + startSimOnboardingActivity(context, subId) + return + } + context.startActivity(ToggleSubscriptionDialogActivity.getIntent(context, subId, active)) + } + + private suspend fun isEmergencyCallbackMode(subId: Int) = withContext(Dispatchers.Default) { + context.telephonyManager(subId).emergencyCallbackMode + } + + private companion object { + private const val TAG = "SubscriptionActivationR" + } } diff --git a/src/com/android/settings/spa/network/SimsSection.kt b/src/com/android/settings/spa/network/SimsSection.kt index 07da034b33b..842656e28bd 100644 --- a/src/com/android/settings/spa/network/SimsSection.kt +++ b/src/com/android/settings/spa/network/SimsSection.kt @@ -30,6 +30,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -47,6 +48,7 @@ import com.android.settingslib.spaprivileged.model.enterprise.Restrictions import com.android.settingslib.spaprivileged.template.preference.RestrictedPreference import com.android.settingslib.spaprivileged.template.preference.RestrictedTwoTargetSwitchPreference import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.launch @Composable fun SimsSection(subscriptionInfoList: List) { @@ -71,9 +73,11 @@ private fun SimPreference(subInfo: SubscriptionInfo) { emit(SubscriptionUtil.isConvertedPsimSubscription(subInfo)) } }.collectAsStateWithLifecycle(initialValue = false) + val subscriptionActivationRepository = remember { SubscriptionActivationRepository(context) } val isActivationChangeable by remember { - SubscriptionActivationRepository(context).isActivationChangeableFlow() + subscriptionActivationRepository.isActivationChangeableFlow() }.collectAsStateWithLifecycle(initialValue = false) + val coroutineScope = rememberCoroutineScope() RestrictedTwoTargetSwitchPreference( model = object : SwitchPreferenceModel { override val title = subInfo.displayName.toString() @@ -87,12 +91,10 @@ private fun SimPreference(subInfo: SubscriptionInfo) { override val icon = @Composable { SimIcon(subInfo.isEmbedded) } override val changeable = { isActivationChangeable && !isConvertedPsim } override val checked = { checked.value } - override val onCheckedChange = { newChecked: Boolean -> - SubscriptionUtil.startToggleSubscriptionDialogActivity( - context, - subInfo.subscriptionId, - newChecked, - ) + override val onCheckedChange: (Boolean) -> Unit = { newChecked -> + coroutineScope.launch { + subscriptionActivationRepository.setActive(subInfo.subscriptionId, newChecked) + } } }, restrictions = Restrictions(keys = listOf(UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS)), diff --git a/tests/spa_unit/src/com/android/settings/network/telephony/SubscriptionActivationRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/network/telephony/SubscriptionActivationRepositoryTest.kt index dd9c505635b..427ab7b1685 100644 --- a/tests/spa_unit/src/com/android/settings/network/telephony/SubscriptionActivationRepositoryTest.kt +++ b/tests/spa_unit/src/com/android/settings/network/telephony/SubscriptionActivationRepositoryTest.kt @@ -17,6 +17,9 @@ package com.android.settings.network.telephony import android.content.Context +import android.telephony.SubscriptionManager +import android.telephony.TelephonyManager +import android.telephony.TelephonyManager.ACTION_SHOW_NOTICE_ECM_BLOCK_OTHERS import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.settings.network.SatelliteRepository @@ -26,14 +29,29 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.argThat +import org.mockito.kotlin.doNothing import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.spy import org.mockito.kotlin.stub +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever @RunWith(AndroidJUnit4::class) class SubscriptionActivationRepositoryTest { - private val context: Context = ApplicationProvider.getApplicationContext() + private val mockTelephonyManager = mock { + on { createForSubscriptionId(SUB_ID) } doReturn mock + } + + private val context: Context = spy(ApplicationProvider.getApplicationContext()) { + doNothing().whenever(mock).startActivity(any()) + on { getSystemService(TelephonyManager::class.java) } doReturn mockTelephonyManager + } + private val mockCallStateRepository = mock() private val mockSatelliteRepository = mock() @@ -81,4 +99,39 @@ class SubscriptionActivationRepositoryTest { assertThat(changeable).isFalse() } + + @Test + fun setActive_defaultSubId_doNothing() = runBlocking { + repository.setActive(subId = SubscriptionManager.DEFAULT_SUBSCRIPTION_ID, active = true) + + verify(context, never()).startActivity(any()) + } + + @Test + fun setActive_turnOffAndIsEmergencyCallbackMode() = runBlocking { + mockTelephonyManager.stub { + on { emergencyCallbackMode } doReturn true + } + + repository.setActive(subId = SUB_ID, active = false) + + verify(context).startActivity(argThat { action == ACTION_SHOW_NOTICE_ECM_BLOCK_OTHERS }) + } + + @Test + fun setActive_turnOffAndNotEmergencyCallbackMode() = runBlocking { + mockTelephonyManager.stub { + on { emergencyCallbackMode } doReturn false + } + + repository.setActive(subId = SUB_ID, active = false) + + verify(context).startActivity(argThat { + component?.className == ToggleSubscriptionDialogActivity::class.qualifiedName + }) + } + + private companion object { + const val SUB_ID = 1 + } } From b48ec2f772e2009f88a853f6372f4bfb6aa19223 Mon Sep 17 00:00:00 2001 From: songferngwang Date: Thu, 30 May 2024 09:24:25 +0000 Subject: [PATCH 11/12] To hide the mobile data when there is no active sim Bug: 343633243 Test: verified the UI Change-Id: I77055a9c1cdb7e94c0339f4255b4799b7b6e02ac --- .../network/NetworkCellularGroupProvider.kt | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/com/android/settings/spa/network/NetworkCellularGroupProvider.kt b/src/com/android/settings/spa/network/NetworkCellularGroupProvider.kt index 98d83402339..68869d8e903 100644 --- a/src/com/android/settings/spa/network/NetworkCellularGroupProvider.kt +++ b/src/com/android/settings/spa/network/NetworkCellularGroupProvider.kt @@ -31,6 +31,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableIntState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable @@ -108,7 +109,9 @@ open class NetworkCellularGroupProvider : SettingsPageProvider { var nonDdsRemember = rememberSaveable { mutableIntStateOf(SubscriptionManager.INVALID_SUBSCRIPTION_ID) } - + var showMobileDataSection = rememberSaveable { + mutableStateOf(false) + } val subscriptionViewModel = viewModel() CollectAirplaneModeAndFinishIfOn() @@ -125,13 +128,18 @@ open class NetworkCellularGroupProvider : SettingsPageProvider { val selectableSubscriptionInfoList by subscriptionViewModel .selectableSubscriptionInfoListFlow .collectAsStateWithLifecycle(initialValue = emptyList()) - + showMobileDataSection.value = selectableSubscriptionInfoList + .filter { subInfo -> subInfo.simSlotIndex > -1 } + .size > 0 val stringSims = stringResource(R.string.provider_network_settings_title) RegularScaffold(title = stringSims) { SimsSection(selectableSubscriptionInfoList) - MobileDataSectionImpl(mobileDataSelectedId, - nonDdsRemember, - ) + if(showMobileDataSection.value) { + MobileDataSectionImpl( + mobileDataSelectedId, + nonDdsRemember, + ) + } PrimarySimSectionImpl( subscriptionViewModel.selectableSubscriptionInfoListFlow, From 27d2115eaf85bab36d589383c3f2b932c2e1ca19 Mon Sep 17 00:00:00 2001 From: tomhsu Date: Thu, 30 May 2024 10:39:48 +0000 Subject: [PATCH 12/12] Avoid flaky test fail due to no Subscription service. Fix: 340875634 Test: atest passed Change-Id: Id922ace830f25dd730cad2d8aa19177988b6670d --- src/com/android/settings/network/SubscriptionUtil.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/com/android/settings/network/SubscriptionUtil.java b/src/com/android/settings/network/SubscriptionUtil.java index ff88b1a9c33..7e3f78dde0e 100644 --- a/src/com/android/settings/network/SubscriptionUtil.java +++ b/src/com/android/settings/network/SubscriptionUtil.java @@ -643,8 +643,13 @@ public class SubscriptionUtil { final SubscriptionManager subscriptionManager = context.getSystemService( SubscriptionManager.class); - String rawPhoneNumber = subscriptionManager.getPhoneNumber( - subscriptionInfo.getSubscriptionId()); + String rawPhoneNumber = ""; + try { + rawPhoneNumber = subscriptionManager.getPhoneNumber( + subscriptionInfo.getSubscriptionId()); + } catch (IllegalStateException e) { + Log.e(TAG, "Subscription service unavailable : " + e); + } if (TextUtils.isEmpty(rawPhoneNumber)) { return null; }