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", diff --git a/res/values/strings.xml b/res/values/strings.xml index bc573228030..f15137a66e8 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1282,6 +1282,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 @@ -7947,6 +7949,18 @@ Schedule + + Turn on automatically + + + Add a calendar + + + Use your calendar + + + Schedule + Schedule 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/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/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/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/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/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; 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/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; } 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/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/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/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/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 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)), 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, 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/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})) 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( 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) 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()); + } +} 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; 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 + } }