From 383b7960f13f0c7ddf9bb5d53d4b3ca54dfb43ef Mon Sep 17 00:00:00 2001 From: Oli Thompson Date: Wed, 24 Apr 2024 14:01:22 +0000 Subject: [PATCH 01/18] Backup connected apps settings Save the preference for connected apps for backup and restore Flag: android.app.admin.backup_connected_apps_settings Bug: 175067666 Test: manual Change-Id: Ic46fbd1ab82f551ecf89caf800e41d6afe48fc68 --- .../InteractAcrossProfilesDetails.java | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/com/android/settings/applications/specialaccess/interactacrossprofiles/InteractAcrossProfilesDetails.java b/src/com/android/settings/applications/specialaccess/interactacrossprofiles/InteractAcrossProfilesDetails.java index 81e84393d41..d40a075b058 100644 --- a/src/com/android/settings/applications/specialaccess/interactacrossprofiles/InteractAcrossProfilesDetails.java +++ b/src/com/android/settings/applications/specialaccess/interactacrossprofiles/InteractAcrossProfilesDetails.java @@ -28,6 +28,8 @@ import static android.app.admin.DevicePolicyResources.Strings.Settings.ONLY_CONN import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE; import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE; import static android.provider.Settings.ACTION_MANAGE_CROSS_PROFILE_ACCESS; +import static android.provider.Settings.Global.CONNECTED_APPS_ALLOWED_PACKAGES; +import static android.provider.Settings.Global.CONNECTED_APPS_DISALLOWED_PACKAGES; import android.Manifest; import android.annotation.UserIdInt; @@ -35,6 +37,7 @@ import android.app.ActionBar; import android.app.AppOpsManager; import android.app.admin.DevicePolicyEventLogger; import android.app.admin.DevicePolicyManager; +import android.app.admin.flags.Flags; import android.app.settings.SettingsEnums; import android.content.Context; import android.content.DialogInterface; @@ -49,6 +52,7 @@ import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.UserHandle; import android.os.UserManager; +import android.provider.Settings; import android.stats.devicepolicy.DevicePolicyEnums; import android.util.IconDrawableFactory; import android.view.LayoutInflater; @@ -68,6 +72,10 @@ import com.android.settingslib.RestrictedLockUtils; import com.android.settingslib.RestrictedSwitchPreference; import com.android.settingslib.widget.LayoutPreference; +import java.util.Collections; +import java.util.Optional; +import java.util.Set; + public class InteractAcrossProfilesDetails extends AppInfoBase implements Preference.OnPreferenceClickListener { @@ -381,6 +389,7 @@ public class InteractAcrossProfilesDetails extends AppInfoBase private void enableInteractAcrossProfiles(boolean newState) { mCrossProfileApps.setInteractAcrossProfilesAppOp( mPackageName, newState ? AppOpsManager.MODE_ALLOWED : AppOpsManager.MODE_IGNORED); + setUserPreferenceForPackage(newState, mPackageName); } private void handleInstallBannerClick() { @@ -552,4 +561,40 @@ public class InteractAcrossProfilesDetails extends AppInfoBase } return ACTION_MANAGE_CROSS_PROFILE_ACCESS.equals(intent.getAction()); } + + private void setUserPreferenceForPackage(boolean enabled, String crossProfilePackage) { + if (!Flags.backupConnectedAppsSettings()) { + return; + } + String allowedPackagesString = Settings.Global.getString(getContentResolver(), + CONNECTED_APPS_ALLOWED_PACKAGES); + String disallowedPackagesString = Settings.Global.getString(getContentResolver(), + CONNECTED_APPS_DISALLOWED_PACKAGES); + + Set allowedPackagesSet = getSetFromString(allowedPackagesString); + Set disallowedPackagesSet = getSetFromString(disallowedPackagesString); + + if (enabled) { + allowedPackagesSet.add(crossProfilePackage); + disallowedPackagesSet.remove(crossProfilePackage); + + } else { + allowedPackagesSet.remove(crossProfilePackage); + disallowedPackagesSet.add(crossProfilePackage); + } + + Settings.Global.putString(getContentResolver(), + CONNECTED_APPS_ALLOWED_PACKAGES, + String.join(",", allowedPackagesSet)); + + Settings.Global.putString(getContentResolver(), + CONNECTED_APPS_DISALLOWED_PACKAGES, + String.join(",", disallowedPackagesSet)); + } + + private Set getSetFromString(String packages) { + return Optional.ofNullable(packages) + .map(pkg -> Set.of(pkg.split(","))) + .orElse(Collections.emptySet()); + } } From 46119c9cbb7aa28235fa97ffed388caa8a19aa71 Mon Sep 17 00:00:00 2001 From: Edgar Wang Date: Mon, 20 May 2024 09:33:26 +0000 Subject: [PATCH 02/18] Make Choose Profile dialog scrollable Fixes: 326846585 Test: manual Change-Id: I15d25c7c123bfdd4f550fe923e3058ef576be653 --- res/layout/user_select.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/layout/user_select.xml b/res/layout/user_select.xml index 8c8c37a64fe..cbe097747fa 100644 --- a/res/layout/user_select.xml +++ b/res/layout/user_select.xml @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. --> - - + From 6eeddc8c298462892542a1a85e96ae88e0fc32e0 Mon Sep 17 00:00:00 2001 From: Lais Andrade Date: Mon, 20 May 2024 16:52:12 +0000 Subject: [PATCH 03/18] Update haptic settings owners Bug: 338334977 Change-Id: I789268bda12a5cc6ff49b0adfaf41a42e52d64d6 Test: N/A --- src/com/android/settings/accessibility/OWNERS | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/com/android/settings/accessibility/OWNERS b/src/com/android/settings/accessibility/OWNERS index 633e9c74aec..24ff9fd44ba 100644 --- a/src/com/android/settings/accessibility/OWNERS +++ b/src/com/android/settings/accessibility/OWNERS @@ -15,5 +15,5 @@ menghanli@google.com #{LAST_RESORT_SUGGESTION} cipson@google.com #{LAST_RESORT_SUGGESTION} # Partner-team files -per-file HapticFeedbackIntensityPreferenceController.java = michaelwr@google.com -per-file *Vibration* = michaelwr@google.com +per-file *Haptic* = file:platform/frameworks/base:/services/core/java/com/android/server/vibrator/OWNERS +per-file *Vibrat* = file:platform/frameworks/base:/services/core/java/com/android/server/vibrator/OWNERS From 2bd1799fb9924f4b53a5b8767ab3521d2383c5a4 Mon Sep 17 00:00:00 2001 From: Julia Reynolds Date: Fri, 17 May 2024 09:34:31 -0400 Subject: [PATCH 04/18] Migrate people settings to new modes UI Flag: android.app.modes_ui Bug: 337079247 Test: make -j RunSettingsRoboTests ROBOTEST_FILTER=com.android.settings.notification.modes Change-Id: If5e7b82a006e856b4aceca7acdfc8cc0d11092a7 --- res/xml/modes_calls_settings.xml | 37 ++ res/xml/modes_messages_settings.xml | 29 + res/xml/modes_people_settings.xml | 35 ++ res/xml/modes_rule_settings.xml | 4 + .../modes/ZenModeCallsFragment.java | 61 +++ .../ZenModeCallsLinkPreferenceController.java | 49 ++ .../notification/modes/ZenModeFragment.java | 5 +- .../modes/ZenModeMessagesFragment.java | 57 ++ ...nModeMessagesLinkPreferenceController.java | 51 ++ .../modes/ZenModePeopleFragment.java | 53 ++ ...ZenModePeopleLinkPreferenceController.java | 54 ++ ...dePrioritySendersPreferenceController.java | 447 +++++++++++++++ ...ModeRepeatCallersPreferenceController.java | 82 +++ .../modes/ZenModeSummaryHelper.java | 295 ++++++++++ .../notification/modes/ZenModesBackend.java | 58 ++ .../modes/ZenModesFragmentBase.java | 8 +- ...ModeCallsLinkPreferenceControllerTest.java | 80 +++ ...eMessagesLinkPreferenceControllerTest.java | 77 +++ ...odePeopleLinkPreferenceControllerTest.java | 77 +++ ...ioritySendersPreferenceControllerTest.java | 509 ++++++++++++++++++ .../modes/ZenModesSummaryHelperTest.java | 88 +++ 21 files changed, 2152 insertions(+), 4 deletions(-) create mode 100644 res/xml/modes_calls_settings.xml create mode 100644 res/xml/modes_messages_settings.xml create mode 100644 res/xml/modes_people_settings.xml create mode 100644 src/com/android/settings/notification/modes/ZenModeCallsFragment.java create mode 100644 src/com/android/settings/notification/modes/ZenModeCallsLinkPreferenceController.java create mode 100644 src/com/android/settings/notification/modes/ZenModeMessagesFragment.java create mode 100644 src/com/android/settings/notification/modes/ZenModeMessagesLinkPreferenceController.java create mode 100644 src/com/android/settings/notification/modes/ZenModePeopleFragment.java create mode 100644 src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java create mode 100644 src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceController.java create mode 100644 src/com/android/settings/notification/modes/ZenModeRepeatCallersPreferenceController.java create mode 100644 src/com/android/settings/notification/modes/ZenModeSummaryHelper.java create mode 100644 tests/robotests/src/com/android/settings/notification/modes/ZenModeCallsLinkPreferenceControllerTest.java create mode 100644 tests/robotests/src/com/android/settings/notification/modes/ZenModeMessagesLinkPreferenceControllerTest.java create mode 100644 tests/robotests/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceControllerTest.java create mode 100644 tests/robotests/src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceControllerTest.java create mode 100644 tests/robotests/src/com/android/settings/notification/modes/ZenModesSummaryHelperTest.java diff --git a/res/xml/modes_calls_settings.xml b/res/xml/modes_calls_settings.xml new file mode 100644 index 00000000000..f2ba7f13867 --- /dev/null +++ b/res/xml/modes_calls_settings.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + diff --git a/res/xml/modes_messages_settings.xml b/res/xml/modes_messages_settings.xml new file mode 100644 index 00000000000..d4aee3d1947 --- /dev/null +++ b/res/xml/modes_messages_settings.xml @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/res/xml/modes_people_settings.xml b/res/xml/modes_people_settings.xml new file mode 100644 index 00000000000..136a357dddc --- /dev/null +++ b/res/xml/modes_people_settings.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + diff --git a/res/xml/modes_rule_settings.xml b/res/xml/modes_rule_settings.xml index 1b79153cf7b..12a683e6bec 100644 --- a/res/xml/modes_rule_settings.xml +++ b/res/xml/modes_rule_settings.xml @@ -22,4 +22,8 @@ android:key="header" android:layout="@layout/settings_entity_header" /> + + \ No newline at end of file diff --git a/src/com/android/settings/notification/modes/ZenModeCallsFragment.java b/src/com/android/settings/notification/modes/ZenModeCallsFragment.java new file mode 100644 index 00000000000..4c85bf56727 --- /dev/null +++ b/src/com/android/settings/notification/modes/ZenModeCallsFragment.java @@ -0,0 +1,61 @@ +/* + * 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; + +/** + * DND Calls Settings page to determine which priority senders can bypass DND when this mode is + * activated. + */ +public class ZenModeCallsFragment extends ZenModeFragmentBase { + + @Override + protected List createPreferenceControllers(Context context) { + List controllers = new ArrayList<>(); + controllers.add(new ZenModePrioritySendersPreferenceController(context, + "zen_mode_settings_category_calls", false, mBackend)); + controllers.add(new ZenModeRepeatCallersPreferenceController(context, + "zen_mode_repeat_callers", mBackend, + context.getResources().getInteger(com.android.internal.R.integer + .config_zen_repeat_callers_threshold))); + return controllers; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.modes_calls_settings; + } + + @Override + public int getMetricsCategory() { + // TODO: b/332937635 - make this the correct metrics category + return SettingsEnums.DND_CALLS; + } + + @Override + public void onResume() { + super.onResume(); + use(ZenModePrioritySendersPreferenceController.class).onResume(); + } +} diff --git a/src/com/android/settings/notification/modes/ZenModeCallsLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeCallsLinkPreferenceController.java new file mode 100644 index 00000000000..1d1d7505944 --- /dev/null +++ b/src/com/android/settings/notification/modes/ZenModeCallsLinkPreferenceController.java @@ -0,0 +1,49 @@ +/* + * 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 com.android.settings.notification.modes.ZenModeFragmentBase.MODE_ID; + +import android.content.Context; +import android.os.Bundle; +import androidx.preference.Preference; +import com.android.settings.core.SubSettingLauncher; + +public class ZenModeCallsLinkPreferenceController extends AbstractZenModePreferenceController { + + private ZenModeSummaryHelper mSummaryHelper; + + public ZenModeCallsLinkPreferenceController(Context context, String key, + ZenModesBackend backend) { + super(context, key, backend); + mSummaryHelper = new ZenModeSummaryHelper(context, backend); + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + Bundle bundle = new Bundle(); + bundle.putString(MODE_ID, getMode().getId()); + // TODO(b/332937635): Update metrics category + preference.setIntent(new SubSettingLauncher(mContext) + .setDestination(ZenModeCallsFragment.class.getName()) + .setSourceMetricsCategory(0) + .setArguments(bundle) + .toIntent()); + preference.setSummary(mSummaryHelper.getCallsSettingSummary(getMode())); + } +} \ 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 d2126810358..1626d91bd85 100644 --- a/src/com/android/settings/notification/modes/ZenModeFragment.java +++ b/src/com/android/settings/notification/modes/ZenModeFragment.java @@ -20,9 +20,6 @@ import android.app.AutomaticZenRule; import android.app.settings.SettingsEnums; import android.content.Context; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; - import com.android.settings.R; import com.android.settingslib.core.AbstractPreferenceController; @@ -42,6 +39,8 @@ public class ZenModeFragment extends ZenModeFragmentBase { // {@link AbstractZenModePreferenceController}. List prefControllers = new ArrayList<>(); prefControllers.add(new ZenModeHeaderController(context, "header", this, mBackend)); + prefControllers.add(new ZenModePeopleLinkPreferenceController( + context, "zen_mode_people", mBackend)); return prefControllers; } diff --git a/src/com/android/settings/notification/modes/ZenModeMessagesFragment.java b/src/com/android/settings/notification/modes/ZenModeMessagesFragment.java new file mode 100644 index 00000000000..c86f8dd4750 --- /dev/null +++ b/src/com/android/settings/notification/modes/ZenModeMessagesFragment.java @@ -0,0 +1,57 @@ +/* + * 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; + +/** + * DND Messages Settings page to determine which priority senders can bypass DND. + * "Messages" include SMS, MMS, and messaging apps. + */ +public class ZenModeMessagesFragment extends ZenModeFragmentBase { + + @Override + protected List createPreferenceControllers(Context context) { + List controllers = new ArrayList<>(); + controllers.add(new ZenModePrioritySendersPreferenceController(context, + "zen_mode_settings_category_messages", true, mBackend)); + return controllers; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.modes_messages_settings; + } + + @Override + public int getMetricsCategory() { + // TODO: b/332937635 - make this the correct metrics category + return SettingsEnums.DND_MESSAGES; + } + + @Override + public void onResume() { + super.onResume(); + use(ZenModePrioritySendersPreferenceController.class).onResume(); + } +} diff --git a/src/com/android/settings/notification/modes/ZenModeMessagesLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeMessagesLinkPreferenceController.java new file mode 100644 index 00000000000..8261008aaf4 --- /dev/null +++ b/src/com/android/settings/notification/modes/ZenModeMessagesLinkPreferenceController.java @@ -0,0 +1,51 @@ +/* + * 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 com.android.settings.notification.modes.ZenModeFragmentBase.MODE_ID; + +import android.content.Context; +import android.os.Bundle; +import androidx.preference.Preference; +import com.android.settings.core.SubSettingLauncher; + +public class ZenModeMessagesLinkPreferenceController extends AbstractZenModePreferenceController { + private final ZenModeSummaryHelper mSummaryHelper; + + public ZenModeMessagesLinkPreferenceController(Context context, String key, + ZenModesBackend backend) { + super(context, key, backend); + mSummaryHelper = new ZenModeSummaryHelper(context, backend); + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + + Bundle bundle = new Bundle(); + bundle.putString(MODE_ID, getMode().getId()); + // TODO(b/332937635): Update metrics category + preference.setIntent(new SubSettingLauncher(mContext) + .setDestination(ZenModeMessagesFragment.class.getName()) + .setSourceMetricsCategory(0) + .setArguments(bundle) + .toIntent()); + + preference.setEnabled(true); + preference.setSummary(mSummaryHelper.getMessagesSettingSummary(getMode().getPolicy())); + } +} diff --git a/src/com/android/settings/notification/modes/ZenModePeopleFragment.java b/src/com/android/settings/notification/modes/ZenModePeopleFragment.java new file mode 100644 index 00000000000..e1f753cef05 --- /dev/null +++ b/src/com/android/settings/notification/modes/ZenModePeopleFragment.java @@ -0,0 +1,53 @@ +/* + * 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; + +/** + * Settings page that shows what calls and messages will break through the mode and links to the + * configuration pages for both. + */ +public class ZenModePeopleFragment extends ZenModeFragmentBase { + + @Override + protected List createPreferenceControllers(Context context) { + List prefControllers = new ArrayList<>(); + prefControllers.add(new ZenModeCallsLinkPreferenceController( + context, "zen_mode_people_calls", mBackend)); + prefControllers.add(new ZenModeMessagesLinkPreferenceController( + context, "zen_mode_people_messages", mBackend)); + return prefControllers; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.modes_people_settings; + } + + @Override + public int getMetricsCategory() { + // TODO: b/332937635 - make this the correct metrics category + return SettingsEnums.DND_PEOPLE; + } +} diff --git a/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java new file mode 100644 index 00000000000..f12200627ef --- /dev/null +++ b/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java @@ -0,0 +1,54 @@ +/* + * 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 com.android.settings.notification.modes.ZenModeFragmentBase.MODE_ID; + +import android.content.Context; +import android.os.Bundle; +import androidx.preference.Preference; +import com.android.settings.R; +import com.android.settings.core.SubSettingLauncher; + +/** + * Preference with a link and summary about what calls and messages can break through the mode + */ +public class ZenModePeopleLinkPreferenceController extends AbstractZenModePreferenceController { + + ZenModeSummaryHelper mSummaryHelper; + + public ZenModePeopleLinkPreferenceController(Context context, String key, + ZenModesBackend backend) { + super(context, key, backend); + mSummaryHelper = new ZenModeSummaryHelper(mContext, mBackend); + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + Bundle bundle = new Bundle(); + bundle.putString(MODE_ID, getMode().getId()); + // TODO(b/332937635): Update metrics category + preference.setIntent(new SubSettingLauncher(mContext) + .setDestination(ZenModePeopleFragment.class.getName()) + .setSourceMetricsCategory(0) + .setArguments(bundle) + .toIntent()); + preference.setSummary(mSummaryHelper.getPeopleSummary(getMode())); + } +} diff --git a/src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceController.java b/src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceController.java new file mode 100644 index 00000000000..a71bbe844b4 --- /dev/null +++ b/src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceController.java @@ -0,0 +1,447 @@ +/* + * 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.service.notification.ZenPolicy.CONVERSATION_SENDERS_ANYONE; +import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_IMPORTANT; +import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_NONE; +import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_UNSET; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_ANYONE; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_CONTACTS; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_NONE; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_STARRED; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_UNSET; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ParceledListSlice; +import android.icu.text.MessageFormat; +import android.provider.Contacts; +import android.service.notification.ConversationChannelWrapper; +import android.service.notification.ZenPolicy; +import android.view.View; +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceScreen; +import com.android.settings.R; +import com.android.settings.core.SubSettingLauncher; +import com.android.settings.notification.app.ConversationListSettings; +import com.android.settingslib.widget.SelectorWithWidgetPreference; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** + * Common preference controller functionality for zen mode priority senders preferences for both + * messages and calls. + * + * These controllers handle the settings regarding which priority senders that are allowed to + * bypass DND for calls or messages, which may be one of the following values: starred contacts, all + * contacts, priority conversations (for messages only), anyone, or no one. + */ +public class ZenModePrioritySendersPreferenceController + extends AbstractZenModePreferenceController { + private final boolean mIsMessages; // if this is false, then this preference is for calls + + static final String KEY_ANY = "senders_anyone"; + static final String KEY_CONTACTS = "senders_contacts"; + static final String KEY_STARRED = "senders_starred_contacts"; + static final String KEY_IMPORTANT = "conversations_important"; + static final String KEY_NONE = "senders_none"; + + private int mNumImportantConversations = CONVERSATION_SENDERS_UNSET; + + private static final Intent ALL_CONTACTS_INTENT = + new Intent(Contacts.Intents.UI.LIST_DEFAULT) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + private static final Intent STARRED_CONTACTS_INTENT = + new Intent(Contacts.Intents.UI.LIST_STARRED_ACTION) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + private static final Intent FALLBACK_INTENT = new Intent(Intent.ACTION_MAIN) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + + private final PackageManager mPackageManager; + private PreferenceCategory mPreferenceCategory; + private List mSelectorPreferences = new ArrayList<>(); + + private final ZenModeSummaryHelper mZenModeSummaryHelper; + + public ZenModePrioritySendersPreferenceController(Context context, String key, + boolean isMessages, ZenModesBackend backend) { + super(context, key, backend); + mIsMessages = isMessages; + + String contactsPackage = context.getString(R.string.config_contacts_package_name); + ALL_CONTACTS_INTENT.setPackage(contactsPackage); + STARRED_CONTACTS_INTENT.setPackage(contactsPackage); + FALLBACK_INTENT.setPackage(contactsPackage); + + mPackageManager = mContext.getPackageManager(); + if (!FALLBACK_INTENT.hasCategory(Intent.CATEGORY_APP_CONTACTS)) { + FALLBACK_INTENT.addCategory(Intent.CATEGORY_APP_CONTACTS); + } + mZenModeSummaryHelper = new ZenModeSummaryHelper(mContext, mBackend); + } + + @Override + public void displayPreference(PreferenceScreen screen) { + mPreferenceCategory = screen.findPreference(getPreferenceKey()); + if (mPreferenceCategory.getPreferenceCount() == 0) { + makeSelectorPreference(KEY_STARRED, + com.android.settings.R.string.zen_mode_from_starred, mIsMessages); + makeSelectorPreference(KEY_CONTACTS, + com.android.settings.R.string.zen_mode_from_contacts, mIsMessages); + if (mIsMessages) { + makeSelectorPreference(KEY_IMPORTANT, + com.android.settings.R.string.zen_mode_from_important_conversations, true); + } + makeSelectorPreference(KEY_ANY, + com.android.settings.R.string.zen_mode_from_anyone, mIsMessages); + makeSelectorPreference(KEY_NONE, + com.android.settings.R.string.zen_mode_none_messages, mIsMessages); + } + super.displayPreference(screen); + } + + @Override + public void updateState(Preference preference) { + if (mIsMessages) { + updateChannelCounts(); + } + final int currContactsSetting = getPrioritySenders(); + final int currConversationsSetting = getPriorityConversationSenders(); + for (SelectorWithWidgetPreference pref : mSelectorPreferences) { + // for each preference, check whether the current state matches what this state + // would look like if the button were checked. + final int[] checkedState = keyToSettingEndState(pref.getKey(), true); + final int checkedContactsSetting = checkedState[0]; + final int checkedConversationsSetting = checkedState[1]; + + boolean match = checkedContactsSetting == currContactsSetting; + if (mIsMessages && checkedConversationsSetting != CONVERSATION_SENDERS_UNSET) { + // "CONVERSATION_SENDERS_UNSET" in checkedContactsSetting means this preference + // doesn't govern the priority senders setting, so the full match happens when + // either the priority senders setting matches or if it's CONVERSATION_SENDERS_UNSET + // so only the conversation setting needs to match. + match = (match || checkedContactsSetting == PEOPLE_TYPE_UNSET) + && (checkedConversationsSetting == currConversationsSetting); + } + + pref.setChecked(match); + } + updateSummaries(); + } + + public void onResume() { + if (mIsMessages) { + updateChannelCounts(); + } + updateSummaries(); + } + + private void updateChannelCounts() { + ParceledListSlice impConversations = + mBackend.getConversations(true); + int numImportantConversations = 0; + if (impConversations != null) { + for (ConversationChannelWrapper conversation : impConversations.getList()) { + if (!conversation.getNotificationChannel().isDemoted()) { + numImportantConversations++; + } + } + } + mNumImportantConversations = numImportantConversations; + } + + private int getPrioritySenders() { + if (mIsMessages) { + return getMode().getPolicy().getPriorityMessageSenders(); + } else { + return getMode().getPolicy().getPriorityCallSenders(); + } + } + + private int getPriorityConversationSenders() { + if (mIsMessages) { + return getMode().getPolicy().getPriorityConversationSenders(); + } + return CONVERSATION_SENDERS_UNSET; + } + + private SelectorWithWidgetPreference makeSelectorPreference(String key, int titleId, + boolean isCheckbox) { + final SelectorWithWidgetPreference pref = + new SelectorWithWidgetPreference(mPreferenceCategory.getContext(), isCheckbox); + pref.setKey(key); + pref.setTitle(titleId); + pref.setOnClickListener(mSelectorClickListener); + + View.OnClickListener widgetClickListener = getWidgetClickListener(key); + if (widgetClickListener != null) { + pref.setExtraWidgetOnClickListener(widgetClickListener); + } + + mPreferenceCategory.addPreference(pref); + mSelectorPreferences.add(pref); + return pref; + } + + private View.OnClickListener getWidgetClickListener(String key) { + if (!KEY_CONTACTS.equals(key) && !KEY_STARRED.equals(key) && !KEY_IMPORTANT.equals(key)) { + return null; + } + + if (KEY_STARRED.equals(key) && !isStarredIntentValid()) { + return null; + } + + if (KEY_CONTACTS.equals(key) && !isContactsIntentValid()) { + return null; + } + + return v -> { + if (KEY_STARRED.equals(key) + && STARRED_CONTACTS_INTENT.resolveActivity(mPackageManager) != null) { + mContext.startActivity(STARRED_CONTACTS_INTENT); + } else if (KEY_CONTACTS.equals(key) + && ALL_CONTACTS_INTENT.resolveActivity(mPackageManager) != null) { + mContext.startActivity(ALL_CONTACTS_INTENT); + } else if (KEY_IMPORTANT.equals(key)) { + new SubSettingLauncher(mContext) + .setDestination(ConversationListSettings.class.getName()) + .setSourceMetricsCategory(SettingsEnums.DND_CONVERSATIONS) + .launch(); + } else { + mContext.startActivity(FALLBACK_INTENT); + } + }; + } + + private boolean isStarredIntentValid() { + return STARRED_CONTACTS_INTENT.resolveActivity(mPackageManager) != null + || FALLBACK_INTENT.resolveActivity(mPackageManager) != null; + } + + private boolean isContactsIntentValid() { + return ALL_CONTACTS_INTENT.resolveActivity(mPackageManager) != null + || FALLBACK_INTENT.resolveActivity(mPackageManager) != null; + } + + void updateSummaries() { + for (SelectorWithWidgetPreference pref : mSelectorPreferences) { + pref.setSummary(getSummary(pref.getKey())); + } + } + + // Gets the desired end state of the priority senders and conversations for the given key + // and whether it is being checked or unchecked. [type]_UNSET indicates no change in state. + // + // Returns an integer array with 2 entries. The first entry is the setting for priority senders + // and the second entry is for priority conversation senders; if isMessages is false, then + // no changes will ever be prescribed for conversation senders. + int[] keyToSettingEndState(String key, boolean checked) { + int[] endState = new int[]{ PEOPLE_TYPE_UNSET, CONVERSATION_SENDERS_UNSET }; + if (!checked) { + // Unchecking any priority-senders-based state should reset the state to NONE. + // "Unchecking" the NONE state doesn't do anything, in practice. + switch (key) { + case KEY_STARRED: + case KEY_CONTACTS: + case KEY_ANY: + case KEY_NONE: + endState[0] = PEOPLE_TYPE_NONE; + } + + // For messages, unchecking "priority conversations" and "any" should reset conversation + // state to "NONE" as well. + if (mIsMessages) { + switch (key) { + case KEY_IMPORTANT: + case KEY_ANY: + case KEY_NONE: + endState[1] = CONVERSATION_SENDERS_NONE; + } + } + } else { + // All below is for the enabling (checked) state. + switch (key) { + case KEY_STARRED: + endState[0] = PEOPLE_TYPE_STARRED; + break; + case KEY_CONTACTS: + endState[0] = PEOPLE_TYPE_CONTACTS; + break; + case KEY_ANY: + endState[0] = PEOPLE_TYPE_ANYONE; + break; + case KEY_NONE: + endState[0] = PEOPLE_TYPE_NONE; + } + + // In the messages case *only*, also handle changing of conversation settings. + if (mIsMessages) { + switch (key) { + case KEY_IMPORTANT: + endState[1] = CONVERSATION_SENDERS_IMPORTANT; + break; + case KEY_ANY: + endState[1] = CONVERSATION_SENDERS_ANYONE; + break; + case KEY_NONE: + endState[1] = CONVERSATION_SENDERS_NONE; + } + } + } + + // Error case check: if somehow, after all of that, endState is still + // {PEOPLE_TYPE_UNSET, CONVERSATION_SENDERS_UNSET}, something has gone wrong. + if (endState[0] == PEOPLE_TYPE_UNSET && endState[1] == CONVERSATION_SENDERS_UNSET) { + throw new IllegalArgumentException("invalid key " + key); + } + + return endState; + } + + // Returns the preferences, if any, that should be newly saved for the specified setting and + // checked state in an array where index 0 is the new senders setting and 1 the new + // conversations setting. A return value of [type]_UNSET indicates that nothing should + // change. + // + // The returned conversations setting will always be CONVERSATION_SENDERS_UNSET (not to change) + // in the calls case. + // + // Checking and unchecking is mostly an operation of setting or unsetting the relevant + // preference, except for some special handling where the conversation setting overlaps: + // - setting or unsetting "priority contacts" or "contacts" has no effect on the + // priority conversation setting, and vice versa + // - if "priority conversations" is selected, and the user checks "anyone", the conversation + // setting is also set to any conversations + // - if "anyone" is previously selected, and the user clicks "priority conversations", then + // the contacts setting is additionally reset to "none". + // - if "anyone" is previously selected, and the user clicks one of the contacts values, + // then the conversations setting is additionally reset to "none". + int[] settingsToSaveOnClick(SelectorWithWidgetPreference preference, + int currSendersSetting, int currConvosSetting) { + int[] savedSettings = new int[]{ PEOPLE_TYPE_UNSET, CONVERSATION_SENDERS_UNSET }; + + // If the preference isn't a checkbox, always consider this to be "checking" the setting. + // Otherwise, toggle. + final int[] endState = keyToSettingEndState(preference.getKey(), + preference.isCheckBox() ? preference.isChecked() : true); + final int prioritySendersSetting = endState[0]; + final int priorityConvosSetting = endState[1]; + + if (prioritySendersSetting != PEOPLE_TYPE_UNSET + && prioritySendersSetting != currSendersSetting) { + savedSettings[0] = prioritySendersSetting; + } + + // Only handle conversation settings for the messages case. If not messages, there should + // never be any change to the conversation senders setting. + if (mIsMessages) { + if (priorityConvosSetting != CONVERSATION_SENDERS_UNSET + && priorityConvosSetting != currConvosSetting) { + savedSettings[1] = priorityConvosSetting; + } + + // Special-case handling for the "priority conversations" checkbox: + // If a specific selection exists for priority senders (starred, contacts), we leave + // it untouched. Otherwise (when the senders is set to "any"), set it to NONE. + if (preference.getKey() == KEY_IMPORTANT + && currSendersSetting == PEOPLE_TYPE_ANYONE) { + savedSettings[0] = PEOPLE_TYPE_NONE; + } + + // Flip-side special case for clicking either "contacts" option: if a specific selection + // exists for priority conversations, leave it untouched; otherwise, set to none. + if ((preference.getKey() == KEY_STARRED || preference.getKey() == KEY_CONTACTS) + && currConvosSetting == CONVERSATION_SENDERS_ANYONE) { + savedSettings[1] = CONVERSATION_SENDERS_NONE; + } + } + + return savedSettings; + } + + private String getSummary(String key) { + switch (key) { + case KEY_STARRED: + return mZenModeSummaryHelper.getStarredContactsSummary(); + case KEY_CONTACTS: + return mZenModeSummaryHelper.getContactsNumberSummary(); + case KEY_IMPORTANT: + return getConversationSummary(); + case KEY_ANY: + return mContext.getResources().getString(mIsMessages + ? R.string.zen_mode_all_messages_summary + : R.string.zen_mode_all_calls_summary); + case KEY_NONE: + default: + return null; + } + } + + private String getConversationSummary() { + final int numConversations = mNumImportantConversations; + + if (numConversations == CONVERSATION_SENDERS_UNSET) { + return null; + } else { + MessageFormat msgFormat = new MessageFormat( + mContext.getString(R.string.zen_mode_conversations_count), + Locale.getDefault()); + Map args = new HashMap<>(); + args.put("count", numConversations); + return msgFormat.format(args); + } + } + + @VisibleForTesting + SelectorWithWidgetPreference.OnClickListener mSelectorClickListener = + new SelectorWithWidgetPreference.OnClickListener() { + @Override + public void onRadioButtonClicked(SelectorWithWidgetPreference preference) { + // The settingsToSaveOnClick function takes whether the preference is a + // checkbox into account to determine whether this selection is checked or unchecked. + final int[] settingsToSave = settingsToSaveOnClick(preference, + getPrioritySenders(), getPriorityConversationSenders()); + final int prioritySendersSetting = settingsToSave[0]; + final int priorityConvosSetting = settingsToSave[1]; + + ZenPolicy.Builder diffPolicy = new ZenPolicy.Builder(); + if (prioritySendersSetting != PEOPLE_TYPE_UNSET) { + if (mIsMessages) { + diffPolicy.allowMessages(prioritySendersSetting); + + } else { + diffPolicy.allowCalls(prioritySendersSetting); + } + } + if (mIsMessages && priorityConvosSetting != CONVERSATION_SENDERS_UNSET) { + diffPolicy.allowConversations(priorityConvosSetting); + } + getMode().setPolicy(diffPolicy.build()); + mBackend.updateMode(getMode()); + } + }; +} diff --git a/src/com/android/settings/notification/modes/ZenModeRepeatCallersPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeRepeatCallersPreferenceController.java new file mode 100644 index 00000000000..d6de9c22d3c --- /dev/null +++ b/src/com/android/settings/notification/modes/ZenModeRepeatCallersPreferenceController.java @@ -0,0 +1,82 @@ +/* + * 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.service.notification.ZenPolicy.PEOPLE_TYPE_ANYONE; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_NONE; +import static android.service.notification.ZenPolicy.STATE_ALLOW; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.provider.Settings; +import android.service.notification.ZenPolicy; +import android.util.Log; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; +import androidx.preference.TwoStatePreference; +import com.android.settings.R; + +public class ZenModeRepeatCallersPreferenceController extends AbstractZenModePreferenceController + implements Preference.OnPreferenceChangeListener { + + private final int mRepeatCallersThreshold; + + public ZenModeRepeatCallersPreferenceController(Context context, + String key, ZenModesBackend backend, int repeatCallersThreshold) { + super(context, key, backend); + + mRepeatCallersThreshold = repeatCallersThreshold; + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + + TwoStatePreference pref = (TwoStatePreference) preference; + + boolean anyCallersCanBypassDnd = + getMode().getPolicy().getPriorityCategoryCalls() == STATE_ALLOW + && getMode().getPolicy().getPriorityCallSenders() == PEOPLE_TYPE_ANYONE; + // if any caller can bypass dnd then repeat callers preference is disabled + if (anyCallersCanBypassDnd) { + pref.setEnabled(false); + pref.setChecked(true); + } else { + pref.setEnabled(true); + pref.setChecked( + getMode().getPolicy().getPriorityCategoryRepeatCallers() == STATE_ALLOW); + } + + setRepeatCallerSummary(preference); + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + final boolean allowRepeatCallers = (Boolean) newValue; + ZenPolicy diffPolicy = new ZenPolicy.Builder() + .allowRepeatCallers(allowRepeatCallers) + .build(); + getMode().setPolicy(diffPolicy); + mBackend.updateMode(getMode()); + return true; + } + + private void setRepeatCallerSummary(Preference preference) { + preference.setSummary(mContext.getString(R.string.zen_mode_repeat_callers_summary, + mRepeatCallersThreshold)); + } +} diff --git a/src/com/android/settings/notification/modes/ZenModeSummaryHelper.java b/src/com/android/settings/notification/modes/ZenModeSummaryHelper.java new file mode 100644 index 00000000000..cf0c3db7499 --- /dev/null +++ b/src/com/android/settings/notification/modes/ZenModeSummaryHelper.java @@ -0,0 +1,295 @@ +/* + * 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.service.notification.ZenPolicy.CONVERSATION_SENDERS_ANYONE; +import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_IMPORTANT; +import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_NONE; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_ANYONE; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_CONTACTS; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_NONE; +import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_ALARMS; +import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_CALLS; +import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_CONVERSATIONS; +import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_EVENTS; +import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_MEDIA; +import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_MESSAGES; +import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_REMINDERS; +import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_REPEAT_CALLERS; +import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_SYSTEM; + +import android.content.Context; +import android.icu.text.MessageFormat; +import android.service.notification.ZenPolicy; + +import com.android.settings.R; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.function.Predicate; + +public class ZenModeSummaryHelper { + + private Context mContext; + private ZenModesBackend mBackend; + + public ZenModeSummaryHelper(Context context, ZenModesBackend backend) { + mContext = context; + mBackend = backend; + } + + private static final int[] ALL_PRIORITY_CATEGORIES = { + PRIORITY_CATEGORY_ALARMS, + PRIORITY_CATEGORY_MEDIA, + PRIORITY_CATEGORY_SYSTEM, + PRIORITY_CATEGORY_MESSAGES, + PRIORITY_CATEGORY_CONVERSATIONS, + PRIORITY_CATEGORY_EVENTS, + PRIORITY_CATEGORY_REMINDERS, + PRIORITY_CATEGORY_CALLS, + PRIORITY_CATEGORY_REPEAT_CALLERS, + }; + + String getOtherSoundCategoriesSummary(ZenMode zenMode) { + List enabledCategories = getEnabledCategories( + zenMode.getPolicy(), + category -> PRIORITY_CATEGORY_ALARMS == category + || PRIORITY_CATEGORY_MEDIA == category + || PRIORITY_CATEGORY_SYSTEM == category + || PRIORITY_CATEGORY_REMINDERS == category + || PRIORITY_CATEGORY_EVENTS == category, + true); + int numCategories = enabledCategories.size(); + MessageFormat msgFormat = new MessageFormat( + mContext.getString(R.string.zen_mode_other_sounds_summary), + Locale.getDefault()); + Map args = new HashMap<>(); + args.put("count", numCategories); + if (numCategories >= 1) { + args.put("sound_category_1", enabledCategories.get(0)); + if (numCategories >= 2) { + args.put("sound_category_2", enabledCategories.get(1)); + if (numCategories == 3) { + args.put("sound_category_3", enabledCategories.get(2)); + } + } + } + return msgFormat.format(args); + } + + String getCallsSettingSummary(ZenMode zenMode) { + List enabledCategories = getEnabledCategories(zenMode.getPolicy(), + category -> PRIORITY_CATEGORY_CALLS == category + || PRIORITY_CATEGORY_REPEAT_CALLERS == category, true); + int numCategories = enabledCategories.size(); + if (numCategories == 0) { + return mContext.getString(R.string.zen_mode_none_calls); + } else if (numCategories == 1) { + return mContext.getString(R.string.zen_mode_calls_summary_one, + enabledCategories.get(0)); + } else { + return mContext.getString(R.string.zen_mode_calls_summary_two, + enabledCategories.get(0), + enabledCategories.get(1)); + } + } + + String getMessagesSettingSummary(ZenPolicy policy) { + List enabledCategories = getEnabledCategories(policy, + category -> PRIORITY_CATEGORY_MESSAGES == category + || PRIORITY_CATEGORY_CONVERSATIONS == category, true); + int numCategories = enabledCategories.size(); + if (numCategories == 0) { + return mContext.getString(R.string.zen_mode_none_messages); + } else if (numCategories == 1) { + return enabledCategories.get(0); + } else { + // While this string name seems like a slight misnomer: it's borrowing the analogous + // calls-summary functionality to combine two permissions. + return mContext.getString(R.string.zen_mode_calls_summary_two, + enabledCategories.get(0), + enabledCategories.get(1)); + } + } + + String getBlockedEffectsSummary(ZenMode zenMode) { + if (zenMode.getPolicy().shouldShowAllVisualEffects()) { + return mContext.getResources().getString( + R.string.zen_mode_restrict_notifications_summary_muted); + } else if (zenMode.getPolicy().shouldHideAllVisualEffects()) { + return mContext.getResources().getString( + R.string.zen_mode_restrict_notifications_summary_hidden); + } else { + return mContext.getResources().getString( + R.string.zen_mode_restrict_notifications_summary_custom); + } + } + + private List getEnabledCategories(ZenPolicy policy, + Predicate filteredCategories, boolean capitalizeFirstInList) { + List enabledCategories = new ArrayList<>(); + for (int category : ALL_PRIORITY_CATEGORIES) { + boolean isFirst = capitalizeFirstInList && enabledCategories.isEmpty(); + if (filteredCategories.test(category) && policy.isCategoryAllowed(category, false)) { + if (category == PRIORITY_CATEGORY_REPEAT_CALLERS + && policy.isCategoryAllowed(PRIORITY_CATEGORY_CALLS, false) + && policy.getPriorityCallSenders() == PEOPLE_TYPE_ANYONE) { + continue; + } + + // For conversations, only the "priority conversations" setting is relevant; any + // other setting is subsumed by the messages-specific messaging. + if (category == PRIORITY_CATEGORY_CONVERSATIONS + && policy.isCategoryAllowed(PRIORITY_CATEGORY_CONVERSATIONS, false) + && policy.getPriorityConversationSenders() + != CONVERSATION_SENDERS_IMPORTANT) { + continue; + } + + enabledCategories.add(getCategory(category, policy, isFirst)); + } + } + return enabledCategories; + } + + private String getCategory(int category, ZenPolicy policy, boolean isFirst) { + if (category == PRIORITY_CATEGORY_ALARMS) { + if (isFirst) { + return mContext.getString(R.string.zen_mode_alarms_list_first); + } else { + return mContext.getString(R.string.zen_mode_alarms_list); + } + } else if (category == PRIORITY_CATEGORY_MEDIA) { + if (isFirst) { + return mContext.getString(R.string.zen_mode_media_list_first); + } else { + return mContext.getString(R.string.zen_mode_media_list); + } + } else if (category == PRIORITY_CATEGORY_SYSTEM) { + if (isFirst) { + return mContext.getString(R.string.zen_mode_system_list_first); + } else { + return mContext.getString(R.string.zen_mode_system_list); + } + } else if (category == PRIORITY_CATEGORY_MESSAGES) { + if (policy.getPriorityMessageSenders() == PEOPLE_TYPE_ANYONE) { + return mContext.getString(R.string.zen_mode_from_anyone); + } else if (policy.getPriorityMessageSenders() == PEOPLE_TYPE_CONTACTS) { + return mContext.getString(R.string.zen_mode_from_contacts); + } else { + return mContext.getString(R.string.zen_mode_from_starred); + } + } else if (category == PRIORITY_CATEGORY_CONVERSATIONS + && policy.getPriorityConversationSenders() == CONVERSATION_SENDERS_IMPORTANT) { + if (isFirst) { + return mContext.getString(R.string.zen_mode_from_important_conversations); + } else { + return mContext.getString( + R.string.zen_mode_from_important_conversations_second); + } + } else if (category == PRIORITY_CATEGORY_EVENTS) { + if (isFirst) { + return mContext.getString(R.string.zen_mode_events_list_first); + } else { + return mContext.getString(R.string.zen_mode_events_list); + } + } else if (category == PRIORITY_CATEGORY_REMINDERS) { + if (isFirst) { + return mContext.getString(R.string.zen_mode_reminders_list_first); + } else { + return mContext.getString(R.string.zen_mode_reminders_list); + } + } else if (category == PRIORITY_CATEGORY_CALLS) { + if (policy.getPriorityCallSenders() == PEOPLE_TYPE_ANYONE) { + if (isFirst) { + return mContext.getString(R.string.zen_mode_from_anyone); + } + return mContext.getString(R.string.zen_mode_all_callers); + } else if (policy.getPriorityCallSenders() == PEOPLE_TYPE_CONTACTS) { + if (isFirst) { + return mContext.getString(R.string.zen_mode_from_contacts); + } + return mContext.getString(R.string.zen_mode_contacts_callers); + } else { + if (isFirst) { + return mContext.getString(R.string.zen_mode_from_starred); + } + return mContext.getString(R.string.zen_mode_starred_callers); + } + } else if (category == PRIORITY_CATEGORY_REPEAT_CALLERS) { + if (isFirst) { + return mContext.getString(R.string.zen_mode_repeat_callers); + } else { + return mContext.getString(R.string.zen_mode_repeat_callers_list); + } + } + + return ""; + } + + public String getStarredContactsSummary() { + List starredContacts = mBackend.getStarredContacts(); + int numStarredContacts = starredContacts.size(); + MessageFormat msgFormat = new MessageFormat( + mContext.getString(R.string.zen_mode_starred_contacts_summary_contacts), + Locale.getDefault()); + Map args = new HashMap<>(); + args.put("count", numStarredContacts); + if (numStarredContacts >= 1) { + args.put("contact_1", starredContacts.get(0)); + if (numStarredContacts >= 2) { + args.put("contact_2", starredContacts.get(1)); + if (numStarredContacts == 3) { + args.put("contact_3", starredContacts.get(2)); + } + } + } + return msgFormat.format(args); + } + + public String getContactsNumberSummary() { + MessageFormat msgFormat = new MessageFormat( + mContext.getString(R.string.zen_mode_contacts_count), + Locale.getDefault()); + Map args = new HashMap<>(); + args.put("count", mBackend.queryAllContactsData().getCount()); + return msgFormat.format(args); + } + + public String getPeopleSummary(ZenMode zenMode) { + final int callersAllowed = zenMode.getPolicy().getPriorityCallSenders(); + final int messagesAllowed = zenMode.getPolicy().getPriorityMessageSenders(); + final int conversationsAllowed = zenMode.getPolicy().getPriorityConversationSenders(); + final boolean areRepeatCallersAllowed = + zenMode.getPolicy().isCategoryAllowed(PRIORITY_CATEGORY_REPEAT_CALLERS, false); + + if (callersAllowed == PEOPLE_TYPE_ANYONE + && messagesAllowed == PEOPLE_TYPE_ANYONE + && conversationsAllowed == CONVERSATION_SENDERS_ANYONE) { + return mContext.getResources().getString(R.string.zen_mode_people_all); + } else if (callersAllowed == PEOPLE_TYPE_NONE + && messagesAllowed == PEOPLE_TYPE_NONE + && conversationsAllowed == CONVERSATION_SENDERS_NONE + && !areRepeatCallersAllowed) { + return mContext.getResources().getString(R.string.zen_mode_people_none); + } else { + return mContext.getResources().getString(R.string.zen_mode_people_some); + } + } +} diff --git a/src/com/android/settings/notification/modes/ZenModesBackend.java b/src/com/android/settings/notification/modes/ZenModesBackend.java index 355adb46198..ac170c6d3d9 100644 --- a/src/com/android/settings/notification/modes/ZenModesBackend.java +++ b/src/com/android/settings/notification/modes/ZenModesBackend.java @@ -21,14 +21,22 @@ import static java.util.Objects.requireNonNull; import android.annotation.Nullable; import android.app.ActivityManager; import android.app.AutomaticZenRule; +import android.app.INotificationManager; import android.app.NotificationManager; import android.content.Context; +import android.content.pm.ParceledListSlice; +import android.database.Cursor; import android.net.Uri; +import android.os.ServiceManager; +import android.provider.ContactsContract; import android.provider.Settings; import android.service.notification.Condition; +import android.service.notification.ConversationChannelWrapper; import android.service.notification.ZenAdapters; import android.service.notification.ZenModeConfig; +import android.util.Log; +import androidx.annotation.VisibleForTesting; import com.android.settings.R; import java.time.Duration; @@ -51,6 +59,8 @@ class ZenModesBackend { private static ZenModesBackend sInstance; private final NotificationManager mNotificationManager; + static INotificationManager sINM = INotificationManager.Stub.asInterface( + ServiceManager.getService(Context.NOTIFICATION_SERVICE)); private final Context mContext; @@ -105,6 +115,54 @@ class ZenModesBackend { } } + public ParceledListSlice getConversations(boolean onlyImportant) { + try { + return sINM.getConversations(onlyImportant); + } catch (Exception e) { + Log.w(TAG, "Error calling NoMan", e); + return ParceledListSlice.emptyList(); + } + } + + public List getStarredContacts() { + Cursor cursor = null; + try { + cursor = queryStarredContactsData(); + return getStarredContacts(cursor); + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + + @VisibleForTesting + List getStarredContacts(Cursor cursor) { + List starredContacts = new ArrayList<>(); + if (cursor != null && cursor.moveToFirst()) { + do { + String contact = cursor.getString(0); + starredContacts.add(contact != null ? contact : + mContext.getString(R.string.zen_mode_starred_contacts_empty_name)); + + } while (cursor.moveToNext()); + } + return starredContacts; + } + + private Cursor queryStarredContactsData() { + return mContext.getContentResolver().query(ContactsContract.Contacts.CONTENT_URI, + new String[]{ContactsContract.Contacts.DISPLAY_NAME_PRIMARY}, + ContactsContract.Data.STARRED + "=1", null, + ContactsContract.Data.TIMES_CONTACTED); + } + + Cursor queryAllContactsData() { + return mContext.getContentResolver().query(ContactsContract.Contacts.CONTENT_URI, + new String[]{ContactsContract.Contacts.DISPLAY_NAME_PRIMARY}, + null, null, null); + } + private ZenMode getManualDndMode(ZenModeConfig config) { // TODO: b/333530553 - Read ZenDeviceEffects of manual DND. // TODO: b/333682392 - Replace with final strings for name & trigger description diff --git a/src/com/android/settings/notification/modes/ZenModesFragmentBase.java b/src/com/android/settings/notification/modes/ZenModesFragmentBase.java index 3b6d17f4225..595f1d08f29 100644 --- a/src/com/android/settings/notification/modes/ZenModesFragmentBase.java +++ b/src/com/android/settings/notification/modes/ZenModesFragmentBase.java @@ -57,9 +57,9 @@ abstract class ZenModesFragmentBase extends RestrictedDashboardFragment { @Override public void onAttach(@NonNull Context context) { - super.onAttach(context); mContext = context; mBackend = ZenModesBackend.getInstance(context); + super.onAttach(context); } @Override @@ -77,6 +77,12 @@ abstract class ZenModesFragmentBase extends RestrictedDashboardFragment { } } + @Override + public void onResume() { + super.onResume(); + updateZenModeState(); + } + @Override public void onStop() { super.onStop(); diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeCallsLinkPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeCallsLinkPreferenceControllerTest.java new file mode 100644 index 00000000000..04d625a2848 --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeCallsLinkPreferenceControllerTest.java @@ -0,0 +1,80 @@ +/* + * 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.NotificationManager.INTERRUPTION_FILTER_PRIORITY; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +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.ZenPolicy; +import androidx.preference.Preference; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.shadows.ShadowApplication; +import org.robolectric.util.ReflectionHelpers; + +@RunWith(RobolectricTestRunner.class) +public final class ZenModeCallsLinkPreferenceControllerTest { + + private ZenModeCallsLinkPreferenceController mController; + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + + private Context mContext; + @Mock + private ZenModesBackend mBackend; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + mContext = RuntimeEnvironment.application; + + mController = new ZenModeCallsLinkPreferenceController( + mContext, "something", mBackend); + } + + @Test + @EnableFlags(Flags.FLAG_MODES_UI) + public void testHasSummary() { + Preference pref = mock(Preference.class); + ZenMode zenMode = new ZenMode("id", + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build()) + .build(), true); + mController.updateZenMode(pref, zenMode); + verify(pref).setSummary(any()); + } +} \ No newline at end of file diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeMessagesLinkPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeMessagesLinkPreferenceControllerTest.java new file mode 100644 index 00000000000..cfeefb40a43 --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeMessagesLinkPreferenceControllerTest.java @@ -0,0 +1,77 @@ +/* + * 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.NotificationManager.INTERRUPTION_FILTER_PRIORITY; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +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.ZenPolicy; +import androidx.preference.Preference; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +@RunWith(RobolectricTestRunner.class) +public final class ZenModeMessagesLinkPreferenceControllerTest { + + private ZenModeMessagesLinkPreferenceController mController; + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + private Context mContext; + @Mock + private ZenModesBackend mBackend; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + mContext = RuntimeEnvironment.application; + + mController = new ZenModeMessagesLinkPreferenceController( + mContext, "something", mBackend); + } + + @Test + @EnableFlags(Flags.FLAG_MODES_UI) + public void testHasSummary() { + Preference pref = mock(Preference.class); + ZenMode zenMode = new ZenMode("id", + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build()) + .build(), true); + mController.updateZenMode(pref, zenMode); + verify(pref).setSummary(any()); + } +} \ No newline at end of file diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceControllerTest.java new file mode 100644 index 00000000000..81e64648a33 --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceControllerTest.java @@ -0,0 +1,77 @@ +/* + * 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.NotificationManager.INTERRUPTION_FILTER_PRIORITY; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +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.ZenPolicy; +import androidx.preference.Preference; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +@RunWith(RobolectricTestRunner.class) +public final class ZenModePeopleLinkPreferenceControllerTest { + + private ZenModePeopleLinkPreferenceController mController; + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + private Context mContext; + @Mock + private ZenModesBackend mBackend; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + mContext = RuntimeEnvironment.application; + + mController = new ZenModePeopleLinkPreferenceController( + mContext, "something", mBackend); + } + + @Test + @EnableFlags(Flags.FLAG_MODES_UI) + public void testHasSummary() { + Preference pref = mock(Preference.class); + ZenMode zenMode = new ZenMode("id", + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build()) + .build(), true); + mController.updateZenMode(pref, zenMode); + verify(pref).setSummary(any()); + } +} \ No newline at end of file diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceControllerTest.java new file mode 100644 index 00000000000..91eb59a77dc --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceControllerTest.java @@ -0,0 +1,509 @@ +/* + * 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.NotificationManager.INTERRUPTION_FILTER_PRIORITY; +import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_ANYONE; +import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_IMPORTANT; +import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_NONE; +import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_UNSET; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_ANYONE; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_CONTACTS; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_NONE; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_STARRED; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_UNSET; +import static android.service.notification.ZenPolicy.STATE_ALLOW; +import static android.service.notification.ZenPolicy.STATE_DISALLOW; +import static android.service.notification.ZenPolicy.STATE_UNSET; +import static com.android.settings.notification.modes.ZenModePrioritySendersPreferenceController.KEY_ANY; +import static com.android.settings.notification.modes.ZenModePrioritySendersPreferenceController.KEY_CONTACTS; +import static com.android.settings.notification.modes.ZenModePrioritySendersPreferenceController.KEY_IMPORTANT; +import static com.android.settings.notification.modes.ZenModePrioritySendersPreferenceController.KEY_NONE; +import static com.android.settings.notification.modes.ZenModePrioritySendersPreferenceController.KEY_STARRED; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +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.net.Uri; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; +import android.service.notification.ZenPolicy; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceScreen; +import com.android.settingslib.widget.SelectorWithWidgetPreference; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatcher; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +@RunWith(RobolectricTestRunner.class) +@EnableFlags(Flags.FLAG_MODES_UI) +public final class ZenModePrioritySendersPreferenceControllerTest { + + private ZenModePrioritySendersPreferenceController mCallsController; + private ZenModePrioritySendersPreferenceController mMessagesController; + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + private Context mContext; + @Mock + private ZenModesBackend mBackend; + + @Mock + private PreferenceCategory mMockMessagesPrefCategory, mMockCallsPrefCategory; + @Mock + private PreferenceScreen mPreferenceScreen; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + mContext = RuntimeEnvironment.application; + + mMessagesController = new ZenModePrioritySendersPreferenceController( + mContext, "messages", true, mBackend); + mCallsController = new ZenModePrioritySendersPreferenceController( + mContext, "calls", false, mBackend); + when(mMockMessagesPrefCategory.getContext()).thenReturn(mContext); + when(mMockCallsPrefCategory.getContext()).thenReturn(mContext); + when(mPreferenceScreen.findPreference(mMessagesController.getPreferenceKey())) + .thenReturn(mMockMessagesPrefCategory); + when(mPreferenceScreen.findPreference(mCallsController.getPreferenceKey())) + .thenReturn(mMockCallsPrefCategory); + } + + // Makes a preference with the provided key and whether it's a checkbox with + // mSelectorClickListener as the onClickListener set. + private SelectorWithWidgetPreference makePreference( + String key, boolean isCheckbox, boolean isMessages) { + final SelectorWithWidgetPreference pref = + new SelectorWithWidgetPreference(mContext, isCheckbox); + pref.setKey(key); + pref.setOnClickListener( + isMessages ? mMessagesController.mSelectorClickListener + : mCallsController.mSelectorClickListener); + return pref; + } + + // Extension of ArgumentMatcher to check that a preference argument has the correct preference + // key, but doesn't check any other properties. + private class PrefKeyMatcher implements ArgumentMatcher { + private String mKey; + PrefKeyMatcher(String key) { + mKey = key; + } + + public boolean matches(SelectorWithWidgetPreference pref) { + return pref.getKey() != null && pref.getKey().equals(mKey); + } + + public String toString() { + return "SelectorWithWidgetPreference matcher for key " + mKey; + } + } + + @Test + public void testDisplayPreferences_makeMessagesPrefs() { + ArgumentCaptor prefCaptor = + ArgumentCaptor.forClass(SelectorWithWidgetPreference.class); + when(mMockMessagesPrefCategory.getPreferenceCount()).thenReturn(0); // not yet created + mMessagesController.displayPreference(mPreferenceScreen); + + // Starred contacts, Contacts, Priority Conversations, Any, None + verify(mMockMessagesPrefCategory, times(5)).addPreference(prefCaptor.capture()); + } + + @Test + public void testDisplayPreferences_makeCallsPrefs() { + ArgumentCaptor prefCaptor = + ArgumentCaptor.forClass(SelectorWithWidgetPreference.class); + when(mMockCallsPrefCategory.getPreferenceCount()).thenReturn(0); // not yet created + mCallsController.displayPreference(mPreferenceScreen); + + // Starred contacts, Contacts, Any, None + verify(mMockCallsPrefCategory, times(4)).addPreference(prefCaptor.capture()); + + // Make sure we never have the conversation one + verify(mMockCallsPrefCategory, never()) + .addPreference(argThat(new PrefKeyMatcher(KEY_IMPORTANT))); + } + + @Test + public void testDisplayPreferences_createdOnlyOnce() { + // Return a nonzero number of child preference when asked. + // Then when displayPreference is called, it should never make any new preferences. + when(mMockCallsPrefCategory.getPreferenceCount()).thenReturn(4); // already created + mCallsController.displayPreference(mPreferenceScreen); + mCallsController.displayPreference(mPreferenceScreen); + mCallsController.displayPreference(mPreferenceScreen); + + // Even though we called display 3 times we shouldn't add more preferences here. + verify(mMockCallsPrefCategory, never()) + .addPreference(any(SelectorWithWidgetPreference.class)); + } + + @Test + public void testKeyToSettingEndState_messagesCheck() { + int[] endState; + + // For KEY_NONE everything should be none. + endState = mMessagesController.keyToSettingEndState(KEY_NONE, true); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_NONE); + + // For KEY_ANY everything should be allowed. + endState = mMessagesController.keyToSettingEndState(KEY_ANY, true); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_ANYONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_ANYONE); + + // For [starred] contacts, we should set the priority senders, but not the conversations + endState = mMessagesController.keyToSettingEndState(KEY_STARRED, true); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_STARRED); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + endState = mMessagesController.keyToSettingEndState(KEY_CONTACTS, true); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_CONTACTS); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + // For priority conversations, we should set the conversations but not priority senders + endState = mMessagesController.keyToSettingEndState(KEY_IMPORTANT, true); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_UNSET); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_IMPORTANT); + } + + @Test + public void testKeyToSettingEndState_messagesUncheck() { + int[] endState; + + // For KEY_NONE, "unchecking" still means "none". + endState = mMessagesController.keyToSettingEndState(KEY_NONE, false); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_NONE); + + // For KEY_ANY unchecking resets the state to "none". + endState = mMessagesController.keyToSettingEndState(KEY_ANY, false); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_NONE); + + // For [starred] contacts, we should unset the priority senders, but not the conversations + endState = mMessagesController.keyToSettingEndState(KEY_STARRED, false); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + endState = mMessagesController.keyToSettingEndState(KEY_CONTACTS, false); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + // For priority conversations, we should set the conversations but not priority senders + endState = mMessagesController.keyToSettingEndState(KEY_IMPORTANT, false); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_UNSET); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_NONE); + } + + @Test + public void testKeyToSettingEndState_callsCheck() { + int[] endState; + + // For calls: we should never set conversations, as this is unrelated to calls. + // For KEY_NONE senders should be none. + endState = mCallsController.keyToSettingEndState(KEY_NONE, true); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + // For KEY_ANY senders should be ANY. + endState = mCallsController.keyToSettingEndState(KEY_ANY, true); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_ANYONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + // For [starred] contacts, we should set the priority senders accordingly + endState = mCallsController.keyToSettingEndState(KEY_STARRED, true); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_STARRED); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + endState = mCallsController.keyToSettingEndState(KEY_CONTACTS, true); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_CONTACTS); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + } + + @Test + public void testKeyToSettingEndState_callsUncheck() { + int[] endState; + + // A calls setup should never set conversations settings. + // For KEY_NONE, "unchecking" still means "none". + endState = mCallsController.keyToSettingEndState(KEY_NONE, false); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + // For KEY_ANY unchecking resets the state to "none". + endState = mCallsController.keyToSettingEndState(KEY_ANY, false); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + // For [starred] contacts, we should unset the priority senders, but not the conversations + endState = mCallsController.keyToSettingEndState(KEY_STARRED, false); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + endState = mCallsController.keyToSettingEndState(KEY_CONTACTS, false); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + } + + @Test + public void testSettingsToSaveOnClick_messagesCheck() { + SelectorWithWidgetPreference anyPref = makePreference(KEY_ANY, true, true); + SelectorWithWidgetPreference nonePref = makePreference(KEY_NONE, true, true); + SelectorWithWidgetPreference contactsPref = makePreference(KEY_CONTACTS, true, true); + SelectorWithWidgetPreference starredPref = makePreference(KEY_STARRED, true, true); + SelectorWithWidgetPreference impPref = makePreference(KEY_IMPORTANT, true, true); + int[] endState; + + // For KEY_NONE everything should be none. + nonePref.setChecked(true); + endState = mMessagesController.settingsToSaveOnClick( + nonePref, PEOPLE_TYPE_ANYONE, CONVERSATION_SENDERS_ANYONE); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_NONE); + + // For KEY_ANY everything should be allowed. + anyPref.setChecked(true); + endState = mMessagesController.settingsToSaveOnClick( + anyPref, PEOPLE_TYPE_NONE, CONVERSATION_SENDERS_NONE); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_ANYONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_ANYONE); + + // For [starred] contacts, we should set the priority senders, but not the conversations + starredPref.setChecked(true); + endState = mMessagesController.settingsToSaveOnClick( + starredPref, PEOPLE_TYPE_NONE, CONVERSATION_SENDERS_NONE); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_STARRED); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + contactsPref.setChecked(true); + endState = mMessagesController.settingsToSaveOnClick( + contactsPref, PEOPLE_TYPE_NONE, CONVERSATION_SENDERS_NONE); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_CONTACTS); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + // For priority conversations, we should set the conversations but not priority senders + impPref.setChecked(true); + endState = mMessagesController.settingsToSaveOnClick( + impPref, PEOPLE_TYPE_NONE, CONVERSATION_SENDERS_NONE); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_UNSET); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_IMPORTANT); + } + + @Test + public void testSettingsToSaveOnClick_messagesUncheck() { + int[] endState; + + SelectorWithWidgetPreference anyPref = makePreference(KEY_ANY, true, true); + SelectorWithWidgetPreference nonePref = makePreference(KEY_NONE, true, true); + SelectorWithWidgetPreference contactsPref = makePreference(KEY_CONTACTS, true, true); + SelectorWithWidgetPreference starredPref = makePreference(KEY_STARRED, true, true); + SelectorWithWidgetPreference impPref = makePreference(KEY_IMPORTANT, true, true); + + // For KEY_NONE, "unchecking" still means "none". + nonePref.setChecked(false); + endState = mMessagesController.settingsToSaveOnClick( + nonePref, PEOPLE_TYPE_NONE, CONVERSATION_SENDERS_NONE); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_UNSET); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + // For KEY_ANY unchecking resets the state to "none". + anyPref.setChecked(false); + endState = mMessagesController.settingsToSaveOnClick( + anyPref, PEOPLE_TYPE_ANYONE, CONVERSATION_SENDERS_ANYONE); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_NONE); + + // For [starred] contacts, we should unset the priority senders, but not the conversations + starredPref.setChecked(false); + endState = mMessagesController.settingsToSaveOnClick( + starredPref, PEOPLE_TYPE_STARRED, CONVERSATION_SENDERS_IMPORTANT); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + contactsPref.setChecked(false); + endState = mMessagesController.settingsToSaveOnClick( + contactsPref, PEOPLE_TYPE_CONTACTS, CONVERSATION_SENDERS_IMPORTANT); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + // For priority conversations, we should set the conversations but not priority senders + impPref.setChecked(false); + endState = mMessagesController.settingsToSaveOnClick( + impPref, PEOPLE_TYPE_CONTACTS, CONVERSATION_SENDERS_IMPORTANT); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_UNSET); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_NONE); + } + + @Test + public void testSettingsToSaveOnClick_callsCheck() { + int[] endState; + SelectorWithWidgetPreference anyPref = makePreference(KEY_ANY, true, true); + SelectorWithWidgetPreference nonePref = makePreference(KEY_NONE, true, true); + SelectorWithWidgetPreference contactsPref = makePreference(KEY_CONTACTS, true, true); + SelectorWithWidgetPreference starredPref = makePreference(KEY_STARRED, true, true); + + // For calls: we should never set conversations, as this is unrelated to calls. + // For KEY_NONE senders should be none. + nonePref.setChecked(true); + endState = mCallsController.settingsToSaveOnClick( + nonePref, PEOPLE_TYPE_CONTACTS, CONVERSATION_SENDERS_IMPORTANT); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + // For KEY_ANY senders should be ANY. + anyPref.setChecked(true); + endState = mCallsController.settingsToSaveOnClick( + anyPref, PEOPLE_TYPE_CONTACTS, CONVERSATION_SENDERS_IMPORTANT); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_ANYONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + // For [starred] contacts, we should set the priority senders accordingly + starredPref.setChecked(true); + endState = mCallsController.settingsToSaveOnClick( + starredPref, PEOPLE_TYPE_CONTACTS, CONVERSATION_SENDERS_IMPORTANT); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_STARRED); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + contactsPref.setChecked(true); + endState = mCallsController.settingsToSaveOnClick( + contactsPref, PEOPLE_TYPE_STARRED, CONVERSATION_SENDERS_IMPORTANT); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_CONTACTS); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + } + + @Test + public void testSettingsToSaveOnClick_callsUncheck() { + int[] endState; + SelectorWithWidgetPreference anyPref = makePreference(KEY_ANY, true, true); + SelectorWithWidgetPreference nonePref = makePreference(KEY_NONE, true, true); + SelectorWithWidgetPreference contactsPref = makePreference(KEY_CONTACTS, true, true); + SelectorWithWidgetPreference starredPref = makePreference(KEY_STARRED, true, true); + + // A calls setup should never set conversations settings. + // For KEY_NONE, "unchecking" still means "none". + nonePref.setChecked(false); + endState = mCallsController.settingsToSaveOnClick( + nonePref, PEOPLE_TYPE_NONE, CONVERSATION_SENDERS_NONE); + assertThat(endState[0]).isEqualTo(STATE_UNSET); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + // For KEY_ANY unchecking resets the state to "none". + anyPref.setChecked(false); + endState = mCallsController.settingsToSaveOnClick( + anyPref, PEOPLE_TYPE_ANYONE, CONVERSATION_SENDERS_ANYONE); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + // For [starred] contacts, we should unset the priority senders, but not the conversations + starredPref.setChecked(false); + endState = mCallsController.settingsToSaveOnClick( + starredPref, PEOPLE_TYPE_STARRED, CONVERSATION_SENDERS_IMPORTANT); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + contactsPref.setChecked(false); + endState = mCallsController.settingsToSaveOnClick( + contactsPref, PEOPLE_TYPE_CONTACTS, CONVERSATION_SENDERS_IMPORTANT); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + } + + @Test + public void testSettingsToSave_messages_noChange() { + int[] savedSettings; + + SelectorWithWidgetPreference nonePref = makePreference(KEY_NONE, true, true); + nonePref.setChecked(true); + savedSettings = mMessagesController.settingsToSaveOnClick( + nonePref, PEOPLE_TYPE_NONE, CONVERSATION_SENDERS_NONE); + assertThat(savedSettings[0]).isEqualTo(STATE_UNSET); + assertThat(savedSettings[1]).isEqualTo(STATE_UNSET); + + SelectorWithWidgetPreference anyPref = makePreference(KEY_ANY, true, true); + anyPref.setChecked(true); + savedSettings = mMessagesController.settingsToSaveOnClick( + anyPref, PEOPLE_TYPE_ANYONE, CONVERSATION_SENDERS_ANYONE); + assertThat(savedSettings[0]).isEqualTo(STATE_UNSET); + assertThat(savedSettings[1]).isEqualTo(STATE_UNSET); + + SelectorWithWidgetPreference starredPref = makePreference(KEY_STARRED, true, true); + SelectorWithWidgetPreference contactsPref = makePreference(KEY_CONTACTS, true, true); + starredPref.setChecked(true); + savedSettings = mMessagesController.settingsToSaveOnClick( + starredPref, PEOPLE_TYPE_STARRED, CONVERSATION_SENDERS_ANYONE); + assertThat(savedSettings[0]).isEqualTo(STATE_UNSET); + + contactsPref.setChecked(true); + savedSettings = mMessagesController.settingsToSaveOnClick( + contactsPref, PEOPLE_TYPE_CONTACTS, CONVERSATION_SENDERS_ANYONE); + assertThat(savedSettings[0]).isEqualTo(STATE_UNSET); + + SelectorWithWidgetPreference impPref = makePreference(KEY_IMPORTANT, true, true); + impPref.setChecked(true); + savedSettings = mMessagesController.settingsToSaveOnClick( + impPref, PEOPLE_TYPE_CONTACTS, CONVERSATION_SENDERS_IMPORTANT); + assertThat(savedSettings[1]).isEqualTo(STATE_UNSET); + } + + @Test + public void testSettingsToSave_calls_noChange() { + int[] savedSettings; + SelectorWithWidgetPreference nonePref = makePreference(KEY_NONE, false, false); + + savedSettings = mMessagesController.settingsToSaveOnClick( + nonePref, PEOPLE_TYPE_NONE, CONVERSATION_SENDERS_NONE); + assertThat(savedSettings[0]).isEqualTo(STATE_UNSET); + assertThat(savedSettings[1]).isEqualTo(STATE_UNSET); + + SelectorWithWidgetPreference anyPref = makePreference(KEY_ANY, false, false); + + savedSettings = mMessagesController.settingsToSaveOnClick( + anyPref, PEOPLE_TYPE_ANYONE, CONVERSATION_SENDERS_ANYONE); + assertThat(savedSettings[0]).isEqualTo(STATE_UNSET); + assertThat(savedSettings[1]).isEqualTo(STATE_UNSET); + + SelectorWithWidgetPreference starredPref = makePreference(KEY_STARRED, false, false); + SelectorWithWidgetPreference contactsPref = makePreference(KEY_CONTACTS, false, false); + savedSettings = mMessagesController.settingsToSaveOnClick( + starredPref, PEOPLE_TYPE_STARRED, CONVERSATION_SENDERS_ANYONE); + assertThat(savedSettings[0]).isEqualTo(STATE_UNSET); + + savedSettings = mMessagesController.settingsToSaveOnClick( + contactsPref, PEOPLE_TYPE_CONTACTS, CONVERSATION_SENDERS_ANYONE); + assertThat(savedSettings[0]).isEqualTo(STATE_UNSET); + } +} \ No newline at end of file diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModesSummaryHelperTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModesSummaryHelperTest.java new file mode 100644 index 00000000000..621c5b06f9d --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModesSummaryHelperTest.java @@ -0,0 +1,88 @@ +/* + * 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.NotificationManager.INTERRUPTION_FILTER_PRIORITY; +import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_ANYONE; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_ANYONE; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_CONTACTS; +import static com.google.common.truth.Truth.assertThat; + +import android.app.AutomaticZenRule; +import android.content.Context; +import android.net.Uri; +import android.service.notification.ZenPolicy; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +@RunWith(RobolectricTestRunner.class) +public class ZenModesSummaryHelperTest { + private Context mContext; + private ZenModesBackend mBackend; + + private ZenModeSummaryHelper mSummaryHelper; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mContext = RuntimeEnvironment.application; + mBackend = new ZenModesBackend(mContext); + mSummaryHelper = new ZenModeSummaryHelper(mContext, mBackend); + } + + @Test + public void getPeopleSummary_noOne() { + AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed")) + .setType(AutomaticZenRule.TYPE_BEDTIME) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().disallowAllSounds().build()) + .build(); + ZenMode zenMode = new ZenMode("id", rule, true); + + assertThat(mSummaryHelper.getPeopleSummary(zenMode)).isEqualTo("No one can interrupt"); + } + + @Test + public void getPeopleSummary_some() { + AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed")) + .setType(AutomaticZenRule.TYPE_BEDTIME) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().allowCalls(PEOPLE_TYPE_CONTACTS).build()) + .build(); + ZenMode zenMode = new ZenMode("id", rule, true); + + assertThat(mSummaryHelper.getPeopleSummary(zenMode)).isEqualTo("Some people can interrupt"); + } + + @Test + public void getPeopleSummary_all() { + AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed")) + .setType(AutomaticZenRule.TYPE_BEDTIME) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().allowCalls(PEOPLE_TYPE_ANYONE). + allowConversations(CONVERSATION_SENDERS_ANYONE) + .allowMessages(PEOPLE_TYPE_ANYONE).build()) + .build(); + ZenMode zenMode = new ZenMode("id", rule, true); + + assertThat(mSummaryHelper.getPeopleSummary(zenMode)).isEqualTo("All people can interrupt"); + } +} From f6849078c58e9139fb389c48a865b08ee816de11 Mon Sep 17 00:00:00 2001 From: Joshua McCloskey Date: Wed, 27 Mar 2024 17:26:25 +0000 Subject: [PATCH 05/18] UDFPS Enrollment Refactor (5/N) Adding minor touch ups, such as starting the FingerprintEnrollConfirmation when enrollment has been completed. Bug: 297082837 Test: atest Change-Id: I7b3edebb141bdb3e4648f71527d45dc5a0185b60 --- .../model/EnrollStageModel.kt} | 16 ++-- .../interactor/EnrollStageInteractor.kt | 14 +-- .../interactor/OrientationInteractor.kt | 1 + .../interactor/UdfpsEnrollInteractor.kt | 96 +++++++++++++++++++ .../FingerprintEnrollmentV2Activity.kt | 40 +++++--- .../udfps/ui/fragment/UdfpsEnrollFragment.kt | 59 +++++++----- .../{viewmodel => model}/DescriptionText.kt | 6 +- .../ui/{viewmodel => model}/HeaderText.kt | 6 +- .../ui/viewmodel/EducationAnimationModel.kt | 4 +- .../udfps/ui/viewmodel/UdfpsViewModel.kt | 73 ++++++++++++-- .../udfps/ui/widget/UdfpsEnrollHelperV2.kt | 89 ----------------- .../udfps/ui/widget/UdfpsEnrollIconV2.kt | 78 +++++++-------- .../UdfpsEnrollProgressBarDrawableV2.kt | 16 ++++ .../udfps/ui/widget/UdfpsEnrollViewV2.kt | 32 +++++-- .../viewmodel/FingerprintNavigationStep.kt | 28 ++++-- .../viewmodel/TransitionViewModel.kt | 41 ++++++++ .../FingerprintEnrollIntroFragmentTest.kt | 2 +- .../FingerprintEnrollIntroScreenshotTest.kt | 2 +- 18 files changed, 391 insertions(+), 212 deletions(-) rename src/com/android/settings/biometrics/fingerprint2/{lib/model/StageViewModel.kt => data/model/EnrollStageModel.kt} (80%) create mode 100644 src/com/android/settings/biometrics/fingerprint2/domain/interactor/UdfpsEnrollInteractor.kt rename src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/{viewmodel => model}/DescriptionText.kt (84%) rename src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/{viewmodel => model}/HeaderText.kt (83%) delete mode 100644 src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollHelperV2.kt create mode 100644 src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/TransitionViewModel.kt diff --git a/src/com/android/settings/biometrics/fingerprint2/lib/model/StageViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/data/model/EnrollStageModel.kt similarity index 80% rename from src/com/android/settings/biometrics/fingerprint2/lib/model/StageViewModel.kt rename to src/com/android/settings/biometrics/fingerprint2/data/model/EnrollStageModel.kt index 81bba157291..ab6ef217b0d 100644 --- a/src/com/android/settings/biometrics/fingerprint2/lib/model/StageViewModel.kt +++ b/src/com/android/settings/biometrics/fingerprint2/data/model/EnrollStageModel.kt @@ -14,38 +14,38 @@ * limitations under the License. */ -package com.android.settings.biometrics.fingerprint2.lib.model +package com.android.settings.biometrics.fingerprint2.data.model /** * A view model that describes the various stages of UDFPS Enrollment. This stages typically update * the enrollment UI in a major way, such as changing the lottie animation or changing the location * of the where the user should press their fingerprint */ -sealed class StageViewModel { +sealed class EnrollStageModel { /** Unknown stage */ - data object Unknown : StageViewModel() + data object Unknown : EnrollStageModel() /** This is the stage that moves the fingerprint icon around during enrollment. */ - data object Guided : StageViewModel() + data object Guided : EnrollStageModel() /** The center stage is the initial stage of enrollment. */ - data object Center : StageViewModel() + data object Center : EnrollStageModel() /** * Fingerprint stage of enrollment. Typically there is some sort of indication that a user should * be using their finger tip to enroll. */ - data object Fingertip : StageViewModel() + data object Fingertip : EnrollStageModel() /** * Left edge stage of enrollment. Typically there is an indication that a user should be using the * left edge of their fingerprint. */ - data object LeftEdge : StageViewModel() + data object LeftEdge : EnrollStageModel() /** * Right edge stage of enrollment. Typically there is an indication that a user should be using * the right edge of their fingerprint. */ - data object RightEdge : StageViewModel() + data object RightEdge : EnrollStageModel() } diff --git a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/EnrollStageInteractor.kt b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/EnrollStageInteractor.kt index 2d4cb409868..e683cb84cf9 100644 --- a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/EnrollStageInteractor.kt +++ b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/EnrollStageInteractor.kt @@ -16,11 +16,11 @@ package com.android.settings.biometrics.fingerprint2.domain.interactor -import com.android.settings.biometrics.fingerprint2.lib.model.StageViewModel +import com.android.settings.biometrics.fingerprint2.data.model.EnrollStageModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf -typealias EnrollStageThresholds = Map +typealias EnrollStageThresholds = Map /** Interactor that provides enroll stages for enrollment. */ interface EnrollStageInteractor { @@ -33,11 +33,11 @@ class EnrollStageInteractorImpl() : EnrollStageInteractor { override val enrollStageThresholds: Flow = flowOf( mapOf( - 0.0f to StageViewModel.Center, - 0.25f to StageViewModel.Guided, - 0.5f to StageViewModel.Fingertip, - 0.75f to StageViewModel.LeftEdge, - 0.875f to StageViewModel.RightEdge, + 0.0f to EnrollStageModel.Center, + 0.25f to EnrollStageModel.Guided, + 0.5f to EnrollStageModel.Fingertip, + 0.75f to EnrollStageModel.LeftEdge, + 0.875f to EnrollStageModel.RightEdge, ) ) } diff --git a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/OrientationInteractor.kt b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/OrientationInteractor.kt index 5d1d8c86e37..f9276e63ddf 100644 --- a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/OrientationInteractor.kt +++ b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/OrientationInteractor.kt @@ -42,6 +42,7 @@ interface OrientationInteractor { * A flow that contains the rotation info matched against the def [config_reverseDefaultRotation] */ val rotationFromDefault: Flow + /** * A Helper function that computes rotation if device is in * [R.bool.config_reverseDefaultConfigRotation] diff --git a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/UdfpsEnrollInteractor.kt b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/UdfpsEnrollInteractor.kt new file mode 100644 index 00000000000..ec09ffd0011 --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/UdfpsEnrollInteractor.kt @@ -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.biometrics.fingerprint2.domain.interactor + +import android.graphics.PointF +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.update + +/** + * This interactor provides information about the current offset of the sensor for guided enrollment + * on UDFPS devices. + */ +interface UdfpsEnrollInteractor { + /** Indicates at which step a UDFPS enrollment is in. */ + fun onEnrollmentStep(stepsRemaining: Int, totalStep: Int) + + /** Indicates if guided enrollment should be enabled or not. */ + fun updateGuidedEnrollment(enabled: Boolean) + + /** + * A flow indicating how much the sensor image drawable should be offset for guided enrollment. A + * null point indicates that the icon should be in its default position. + */ + val guidedEnrollmentOffset: Flow +} + +/** Keeps track of which guided enrollment point we should be using */ +class UdfpsEnrollInteractorImpl( + pixelsPerMillimeter: Float, + accessibilityInteractor: AccessibilityInteractor, +) : UdfpsEnrollInteractor { + + private var isGuidedEnrollment = MutableStateFlow(false) + // Number of pixels per mm + val px = pixelsPerMillimeter + private val guidedEnrollmentPoints: MutableList = + mutableListOf( + PointF(2.00f * px, 0.00f * px), + PointF(0.87f * px, -2.70f * px), + PointF(-1.80f * px, -1.31f * px), + PointF(-1.80f * px, 1.31f * px), + PointF(0.88f * px, 2.70f * px), + PointF(3.94f * px, -1.06f * px), + PointF(2.90f * px, -4.14f * px), + PointF(-0.52f * px, -5.95f * px), + PointF(-3.33f * px, -3.33f * px), + PointF(-3.99f * px, -0.35f * px), + PointF(-3.62f * px, 2.54f * px), + PointF(-1.49f * px, 5.57f * px), + PointF(2.29f * px, 4.92f * px), + PointF(3.82f * px, 1.78f * px), + ) + + override fun onEnrollmentStep(stepsRemaining: Int, totalStep: Int) { + val index = (totalStep - stepsRemaining) % guidedEnrollmentPoints.size + _guidedEnrollment.update { guidedEnrollmentPoints[index] } + } + + override fun updateGuidedEnrollment(enabled: Boolean) { + isGuidedEnrollment.update { enabled } + } + + private val _guidedEnrollment = MutableStateFlow(PointF(0f, 0f)) + override val guidedEnrollmentOffset: Flow = + combine( + _guidedEnrollment, + accessibilityInteractor.isAccessibilityEnabled, + isGuidedEnrollment, + ) { point, accessibilityEnabled, guidedEnrollmentEnabled -> + if (accessibilityEnabled || !guidedEnrollmentEnabled) { + return@combine PointF(0f, 0f) + } else { + return@combine PointF(point.x * SCALE, point.y * SCALE) + } + } + + companion object { + private const val SCALE = 0.5f + } +} diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/activity/FingerprintEnrollmentV2Activity.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/activity/FingerprintEnrollmentV2Activity.kt index c8e9ca38d85..6d353a42a62 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/activity/FingerprintEnrollmentV2Activity.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/activity/FingerprintEnrollmentV2Activity.kt @@ -24,6 +24,7 @@ import android.hardware.fingerprint.FingerprintManager import android.os.Bundle import android.os.Vibrator import android.util.Log +import android.util.TypedValue import android.view.accessibility.AccessibilityManager import androidx.activity.result.contract.ActivityResultContracts import androidx.fragment.app.Fragment @@ -54,6 +55,8 @@ import com.android.settings.biometrics.fingerprint2.domain.interactor.FoldStateI import com.android.settings.biometrics.fingerprint2.domain.interactor.FoldStateInteractorImpl import com.android.settings.biometrics.fingerprint2.domain.interactor.OrientationInteractor import com.android.settings.biometrics.fingerprint2.domain.interactor.OrientationInteractorImpl +import com.android.settings.biometrics.fingerprint2.domain.interactor.UdfpsEnrollInteractor +import com.android.settings.biometrics.fingerprint2.domain.interactor.UdfpsEnrollInteractorImpl import com.android.settings.biometrics.fingerprint2.domain.interactor.VibrationInteractor import com.android.settings.biometrics.fingerprint2.domain.interactor.VibrationInteractorImpl import com.android.settings.biometrics.fingerprint2.lib.model.Default @@ -89,6 +92,7 @@ import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.Fing import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintNavigationViewModel import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintScrollViewModel import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.GatekeeperInfo +import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.Transition import com.android.settings.flags.Flags import com.android.settings.password.ChooseLockGeneric import com.android.settings.password.ChooseLockSettingsHelper @@ -116,6 +120,7 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() { private lateinit var foldStateInteractor: FoldStateInteractor private lateinit var orientationInteractor: OrientationInteractor private lateinit var displayDensityInteractor: DisplayDensityInteractor + private lateinit var udfpsEnrollInteractor: UdfpsEnrollInteractor private lateinit var fingerprintScrollViewModel: FingerprintScrollViewModel private lateinit var backgroundViewModel: BackgroundViewModel private lateinit var fingerprintFlowViewModel: FingerprintFlowViewModel @@ -256,6 +261,15 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() { fingerprintManager, Settings, ) + val accessibilityInteractor = + AccessibilityInteractorImpl( + getSystemService(AccessibilityManager::class.java)!!, + lifecycleScope, + ) + + val pixelsPerMillimeter = + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_MM, 1f, context.resources.displayMetrics) + udfpsEnrollInteractor = UdfpsEnrollInteractorImpl(pixelsPerMillimeter, accessibilityInteractor) val fingerprintManagerInteractor = FingerprintManagerInteractorImpl( @@ -273,12 +287,6 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() { val hasConfirmedDeviceCredential = gatekeeperInfo is GatekeeperInfo.GatekeeperPasswordInfo - val accessibilityInteractor = - AccessibilityInteractorImpl( - getSystemService(AccessibilityManager::class.java)!!, - lifecycleScope, - ) - navigationViewModel = ViewModelProvider( this, @@ -384,6 +392,7 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() { orientationInteractor, backgroundViewModel, fingerprintSensorRepo, + udfpsEnrollInteractor, ), )[UdfpsViewModel::class.java] @@ -435,17 +444,17 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() { else -> FingerprintEnrollEnrollingV2Fragment() } } - Introduction -> FingerprintEnrollIntroV2Fragment() + is Introduction -> FingerprintEnrollIntroV2Fragment() else -> null } if (theClass != null) { - supportFragmentManager.fragments.onEach { fragment -> - supportFragmentManager.beginTransaction().remove(fragment).commit() - } - supportFragmentManager .beginTransaction() + .setCustomAnimations( + step.enterTransition.toAnimation(), + step.exitTransition.toAnimation(), + ) .setReorderingAllowed(true) .add(R.id.fragment_container_view, theClass::class.java, null) .commit() @@ -512,3 +521,12 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() { } } } + +private fun Transition.toAnimation(): Int { + return when (this) { + Transition.EnterFromLeft -> com.google.android.setupdesign.R.anim.sud_slide_back_in + Transition.EnterFromRight -> com.google.android.setupdesign.R.anim.sud_slide_next_in + Transition.ExitToLeft -> com.google.android.setupdesign.R.anim.sud_slide_next_out + Transition.ExitToRight -> com.google.android.setupdesign.R.anim.sud_slide_back_out + } +} diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/fragment/UdfpsEnrollFragment.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/fragment/UdfpsEnrollFragment.kt index c96a1b45302..a2e52329dd8 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/fragment/UdfpsEnrollFragment.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/fragment/UdfpsEnrollFragment.kt @@ -32,12 +32,12 @@ import androidx.lifecycle.repeatOnLifecycle import com.airbnb.lottie.LottieAnimationView import com.airbnb.lottie.LottieCompositionFactory import com.android.settings.R +import com.android.settings.biometrics.fingerprint2.data.model.EnrollStageModel import com.android.settings.biometrics.fingerprint2.lib.model.FingerEnrollState -import com.android.settings.biometrics.fingerprint2.lib.model.StageViewModel import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.common.widget.FingerprintErrorDialog -import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel.DescriptionText +import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.model.DescriptionText +import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.model.HeaderText import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel.EducationAnimationModel -import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel.HeaderText import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel.UdfpsViewModel import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.widget.UdfpsEnrollViewV2 import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintNavigationStep @@ -83,6 +83,8 @@ class UdfpsEnrollFragment() : Fragment(R.layout.fingerprint_v2_udfps_enroll_enro window.statusBarColor = color view.setBackgroundColor(color) + udfpsEnrollView.setFinishAnimationCompleted { viewModel.finishedSuccessfully() } + viewLifecycleOwner.lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.RESUMED) { launch { @@ -159,7 +161,14 @@ class UdfpsEnrollFragment() : Fragment(R.layout.fingerprint_v2_udfps_enroll_enro } viewLifecycleOwner.lifecycleScope.launch { - viewModel.enrollStage.collect { udfpsEnrollView.updateStage(it) } + viewModel.guidedEnrollment.collect { + glifLayout.post { udfpsEnrollView.updateGuidedEnrollment(it) } + } + } + viewLifecycleOwner.lifecycleScope.launch { + viewModel.guidedEnrollmentSaved.collect { + glifLayout.post { udfpsEnrollView.onGuidedPointSaved(it) } + } } } } @@ -175,35 +184,35 @@ class UdfpsEnrollFragment() : Fragment(R.layout.fingerprint_v2_udfps_enroll_enro } private fun HeaderText.toResource(): Int { - return when (this.stageViewModel) { - StageViewModel.Center, - StageViewModel.Guided, - StageViewModel.Fingertip, - StageViewModel.Unknown -> R.string.security_settings_udfps_enroll_fingertip_title - StageViewModel.LeftEdge -> R.string.security_settings_udfps_enroll_left_edge_title - StageViewModel.RightEdge -> R.string.security_settings_udfps_enroll_right_edge_title + return when (this.enrollStageModel) { + EnrollStageModel.Center, + EnrollStageModel.Guided, + EnrollStageModel.Fingertip, + EnrollStageModel.Unknown -> R.string.security_settings_udfps_enroll_fingertip_title + EnrollStageModel.LeftEdge -> R.string.security_settings_udfps_enroll_left_edge_title + EnrollStageModel.RightEdge -> R.string.security_settings_udfps_enroll_right_edge_title } } private fun DescriptionText.toResource(): Int? { - return when (this.stageViewModel) { - StageViewModel.Center, - StageViewModel.Guided, - StageViewModel.Fingertip, - StageViewModel.LeftEdge, - StageViewModel.RightEdge -> null - StageViewModel.Unknown -> R.string.security_settings_udfps_enroll_start_message + return when (this.enrollStageModel) { + EnrollStageModel.Center, + EnrollStageModel.Guided, + EnrollStageModel.Fingertip, + EnrollStageModel.LeftEdge, + EnrollStageModel.RightEdge -> null + EnrollStageModel.Unknown -> R.string.security_settings_udfps_enroll_start_message } } private fun EducationAnimationModel.toResource(): Int? { - return when (this.stageViewModel) { - StageViewModel.Center, - StageViewModel.Guided -> R.raw.udfps_center_hint_lottie - StageViewModel.Fingertip -> R.raw.udfps_tip_hint_lottie - StageViewModel.LeftEdge -> R.raw.udfps_left_edge_hint_lottie - StageViewModel.RightEdge -> R.raw.udfps_right_edge_hint_lottie - StageViewModel.Unknown -> null + return when (this.enrollStageModel) { + EnrollStageModel.Center, + EnrollStageModel.Guided -> R.raw.udfps_center_hint_lottie + EnrollStageModel.Fingertip -> R.raw.udfps_tip_hint_lottie + EnrollStageModel.LeftEdge -> R.raw.udfps_left_edge_hint_lottie + EnrollStageModel.RightEdge -> R.raw.udfps_right_edge_hint_lottie + EnrollStageModel.Unknown -> null } } diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/DescriptionText.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/model/DescriptionText.kt similarity index 84% rename from src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/DescriptionText.kt rename to src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/model/DescriptionText.kt index 175fea0ac13..a949545ec19 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/DescriptionText.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/model/DescriptionText.kt @@ -14,13 +14,13 @@ * limitations under the License. */ -package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel +package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.model -import com.android.settings.biometrics.fingerprint2.lib.model.StageViewModel +import com.android.settings.biometrics.fingerprint2.data.model.EnrollStageModel /** Represents the description text for UDFPS enrollment */ data class DescriptionText( val isSuw: Boolean, val isAccessibility: Boolean, - val stageViewModel: StageViewModel, + val enrollStageModel: EnrollStageModel, ) diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/HeaderText.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/model/HeaderText.kt similarity index 83% rename from src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/HeaderText.kt rename to src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/model/HeaderText.kt index c565f35487c..9e45107537f 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/HeaderText.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/model/HeaderText.kt @@ -14,13 +14,13 @@ * limitations under the License. */ -package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel +package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.model -import com.android.settings.biometrics.fingerprint2.lib.model.StageViewModel +import com.android.settings.biometrics.fingerprint2.data.model.EnrollStageModel /** Represents the header text for UDFPS enrollment */ data class HeaderText( val isSuw: Boolean, val isAccessibility: Boolean, - val stageViewModel: StageViewModel, + val enrollStageModel: EnrollStageModel, ) diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/EducationAnimationModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/EducationAnimationModel.kt index a274179e303..64f345f13a5 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/EducationAnimationModel.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/EducationAnimationModel.kt @@ -16,11 +16,11 @@ package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel -import com.android.settings.biometrics.fingerprint2.lib.model.StageViewModel +import com.android.settings.biometrics.fingerprint2.data.model.EnrollStageModel /** Represents the lottie for UDFPS enrollment */ data class EducationAnimationModel( val isSuw: Boolean, val isAccessibility: Boolean, - val stageViewModel: StageViewModel, + val enrollStageModel: EnrollStageModel, ) diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/UdfpsViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/UdfpsViewModel.kt index 37822378f21..a22f680b641 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/UdfpsViewModel.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/UdfpsViewModel.kt @@ -17,20 +17,24 @@ package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel import android.graphics.Point +import android.graphics.PointF import android.view.Surface import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.android.settings.biometrics.fingerprint2.data.repository.FingerprintSensorRepository +import com.android.settings.biometrics.fingerprint2.data.model.EnrollStageModel import com.android.settings.biometrics.fingerprint2.data.repository.SimulatedTouchEventsRepository import com.android.settings.biometrics.fingerprint2.domain.interactor.DebuggingInteractor import com.android.settings.biometrics.fingerprint2.domain.interactor.DisplayDensityInteractor import com.android.settings.biometrics.fingerprint2.domain.interactor.EnrollStageInteractor import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintVibrationEffects import com.android.settings.biometrics.fingerprint2.domain.interactor.OrientationInteractor +import com.android.settings.biometrics.fingerprint2.domain.interactor.UdfpsEnrollInteractor import com.android.settings.biometrics.fingerprint2.domain.interactor.VibrationInteractor import com.android.settings.biometrics.fingerprint2.lib.model.FingerEnrollState -import com.android.settings.biometrics.fingerprint2.lib.model.StageViewModel +import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.model.DescriptionText +import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.model.HeaderText import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.BackgroundViewModel import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintAction import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollEnrollingViewModel @@ -61,9 +65,11 @@ class UdfpsViewModel( orientationInteractor: OrientationInteractor, backgroundViewModel: BackgroundViewModel, sensorRepository: FingerprintSensorRepository, + udfpsEnrollInteractor: UdfpsEnrollInteractor, ) : ViewModel() { private val isSetupWizard = flowOf(false) + private var shouldResetErollment = false private var _enrollState: Flow = fingerprintEnrollEnrollingViewModel.enrollFlow @@ -112,6 +118,17 @@ class UdfpsViewModel( } } + /** + * This indicates at which point the UI should offset the fingerprint sensor icon for guided + * enrollment. + */ + val guidedEnrollment: Flow = + udfpsEnrollInteractor.guidedEnrollmentOffset.distinctUntilChanged() + + /** The saved version of [guidedEnrollment] */ + val guidedEnrollmentSaved: Flow = + guidedEnrollment.shareIn(this.viewModelScope, SharingStarted.Eagerly, replay = 1) + /** * This is the saved progress, this is for when views are recreated and need saved state for the * first time. @@ -132,13 +149,13 @@ class UdfpsViewModel( } } - /** Determines the current [StageViewModel] enrollment is in */ - val enrollStage: Flow = + /** Determines the current [EnrollStageModel] enrollment is in */ + private val enrollStage: Flow = combine(enrollStageInteractor.enrollStageThresholds, enrollState) { thresholds, event -> if (event is FingerEnrollState.EnrollProgress) { val progress = (event.totalStepsRequired - event.remainingSteps).toFloat() / event.totalStepsRequired - var stageToReturn: StageViewModel = StageViewModel.Center + var stageToReturn: EnrollStageModel = EnrollStageModel.Center thresholds.forEach { (threshold, stage) -> if (progress < threshold) { return@forEach @@ -153,6 +170,40 @@ class UdfpsViewModel( .filterNotNull() .shareIn(this.viewModelScope, SharingStarted.Eagerly, replay = 1) + init { + viewModelScope.launch { + enrollState + .combine(accessibilityEnabled) { event, isEnabled -> Pair(event, isEnabled) } + .collect { + if ( + when (it.first) { + is FingerEnrollState.EnrollError -> true + is FingerEnrollState.EnrollHelp -> it.second + is FingerEnrollState.EnrollProgress -> true + else -> false + } + ) { + vibrate(it.first) + } + } + } + viewModelScope.launch { + enrollStage.collect { + udfpsEnrollInteractor.updateGuidedEnrollment(it is EnrollStageModel.Guided) + } + } + + viewModelScope.launch { + enrollState.filterIsInstance().collect { + udfpsEnrollInteractor.onEnrollmentStep(it.remainingSteps, it.totalStepsRequired) + } + } + + viewModelScope.launch { + backgroundViewModel.background.filter { true }.collect { didGoToBackground() } + } + } + /** Indicates if we should show the lottie. */ val shouldShowLottie: Flow = combine( @@ -183,7 +234,7 @@ class UdfpsViewModel( } .shareIn(this.viewModelScope, SharingStarted.Eagerly, replay = 1) - private val shouldClearDescriptionText = enrollStage.map { it is StageViewModel.Unknown } + private val shouldClearDescriptionText = enrollStage.map { it is EnrollStageModel.Unknown } /** The description text for UDFPS enrollment */ val descriptionText: Flow = @@ -202,6 +253,10 @@ class UdfpsViewModel( /** Indicates if the consumer is ready for enrollment */ fun readyForEnrollment() { + if (shouldResetErollment) { + shouldResetErollment = false + _enrollState = fingerprintEnrollEnrollingViewModel.enrollFlow + } fingerprintEnrollEnrollingViewModel.canEnroll() } @@ -237,8 +292,12 @@ class UdfpsViewModel( } private fun doReset() { - /** Indicates if the icon should be animating or not */ _enrollState = fingerprintEnrollEnrollingViewModel.enrollFlow + progressSaved = + enrollState + .filterIsInstance() + .filterNotNull() + .shareIn(this.viewModelScope, SharingStarted.Eagerly, replay = 1) } /** The lottie that should be shown for UDFPS Enrollment */ @@ -272,6 +331,7 @@ class UdfpsViewModel( private val orientationInteractor: OrientationInteractor, private val backgroundViewModel: BackgroundViewModel, private val sensorRepository: FingerprintSensorRepository, + private val udfpsEnrollInteractor: UdfpsEnrollInteractor, ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") @@ -287,6 +347,7 @@ class UdfpsViewModel( orientationInteractor, backgroundViewModel, sensorRepository, + udfpsEnrollInteractor, ) as T } diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollHelperV2.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollHelperV2.kt deleted file mode 100644 index 141924161d1..00000000000 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollHelperV2.kt +++ /dev/null @@ -1,89 +0,0 @@ -/* - * 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.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.widget - -import android.content.Context -import android.graphics.PointF -import android.util.TypedValue -import android.view.accessibility.AccessibilityManager -import com.android.settings.biometrics.fingerprint2.lib.model.StageViewModel - -/** Keeps track of which guided enrollment point we should be using */ -class UdfpsEnrollHelperV2(private val mContext: Context) { - - private var isGuidedEnrollment: Boolean = false - private val accessibilityEnabled: Boolean - private val guidedEnrollmentPoints: MutableList - /** The current index of [guidedEnrollmentPoints] for the guided enrollment. */ - private var index = 0 - - init { - val am = mContext.getSystemService(AccessibilityManager::class.java) - accessibilityEnabled = am!!.isEnabled - guidedEnrollmentPoints = ArrayList() - - // Number of pixels per mm - val px = - TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_MM, 1f, mContext.resources.displayMetrics) - guidedEnrollmentPoints.add(PointF(2.00f * px, 0.00f * px)) - guidedEnrollmentPoints.add(PointF(0.87f * px, -2.70f * px)) - guidedEnrollmentPoints.add(PointF(-1.80f * px, -1.31f * px)) - guidedEnrollmentPoints.add(PointF(-1.80f * px, 1.31f * px)) - guidedEnrollmentPoints.add(PointF(0.88f * px, 2.70f * px)) - guidedEnrollmentPoints.add(PointF(3.94f * px, -1.06f * px)) - guidedEnrollmentPoints.add(PointF(2.90f * px, -4.14f * px)) - guidedEnrollmentPoints.add(PointF(-0.52f * px, -5.95f * px)) - guidedEnrollmentPoints.add(PointF(-3.33f * px, -3.33f * px)) - guidedEnrollmentPoints.add(PointF(-3.99f * px, -0.35f * px)) - guidedEnrollmentPoints.add(PointF(-3.62f * px, 2.54f * px)) - guidedEnrollmentPoints.add(PointF(-1.49f * px, 5.57f * px)) - guidedEnrollmentPoints.add(PointF(2.29f * px, 4.92f * px)) - guidedEnrollmentPoints.add(PointF(3.82f * px, 1.78f * px)) - } - - /** - * This indicates whether we should be offsetting the enrollment icon based on - * [guidedEnrollmentPoints] - */ - fun onUpdateStage(stage: StageViewModel) { - this.isGuidedEnrollment = stage is StageViewModel.Guided - } - - /** Updates [index] to be used by [guidedEnrollmentPoints] */ - fun onEnrollmentProgress(remaining: Int, totalSteps: Int) { - index = totalSteps - remaining - } - - /** - * Returns the current guided enrollment point, or (0,0) if we are not in guided enrollment or are - * in accessibility. - */ - val guidedEnrollmentLocation: PointF? - get() { - if (accessibilityEnabled || !isGuidedEnrollment) { - return null - } - val scale = SCALE - val originalPoint = guidedEnrollmentPoints[index % guidedEnrollmentPoints.size] - return PointF(originalPoint.x * scale, originalPoint.y * scale) - } - - companion object { - private const val TAG = "UdfpsEnrollHelperV2" - private const val SCALE = 0.5f - } -} diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollIconV2.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollIconV2.kt index 0d489954b80..c209c55cb76 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollIconV2.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollIconV2.kt @@ -24,6 +24,7 @@ import android.graphics.Canvas import android.graphics.ColorFilter import android.graphics.Paint import android.graphics.PixelFormat +import android.graphics.PointF import android.graphics.Rect import android.graphics.RectF import android.graphics.drawable.Drawable @@ -37,7 +38,6 @@ import androidx.core.animation.addListener import androidx.core.graphics.toRect import androidx.core.graphics.toRectF import com.android.settings.R -import com.android.settings.biometrics.fingerprint2.lib.model.StageViewModel import kotlin.math.sin /** @@ -51,11 +51,11 @@ class UdfpsEnrollIconV2 internal constructor(context: Context, attrs: AttributeS private val fingerprintDrawable: ShapeDrawable private val sensorOutlinePaint: Paint private val blueFill: Paint - private val helper = UdfpsEnrollHelperV2(context) @ColorInt private var enrollIconColor = 0 @ColorInt private var movingTargetFill = 0 private var currentScale = 1.0f private var alpha = 0 + private var guidedEnrollmentOffset: PointF? = null /** * This is the physical location of the sensor. This rect will be updated by [drawSensorRectAt] @@ -143,45 +143,6 @@ class UdfpsEnrollIconV2 internal constructor(context: Context, attrs: AttributeS invalidateSelf() } - /** Update the progress of the icon */ - fun onEnrollmentProgress(remaining: Int, totalSteps: Int, isRecreating: Boolean = false) { - restoreAnimationTime() - // If we are restoring this view from a saved state, set animation duration to 0 to avoid - // animating progress that has already occurred. - if (isRecreating) { - setAnimationTimeToZero() - } else { - restoreAnimationTime() - } - - helper.onEnrollmentProgress(remaining, totalSteps) - val offset = helper.guidedEnrollmentLocation - val currentBounds = getCurrLocation().toRect() - if (offset != null) { - // This is the desired location of the sensor rect, the [EnrollHelper] - // offsets the initial sensor rect by a bit to get the user to move their finger a bit more. - val targetRect = Rect(sensorRectBounds).toRectF() - targetRect.offset(offset.x, offset.y) - val shouldAnimateMovement = - !currentBounds.equals(targetRect) && offset.x != 0f && offset.y != 0f - if (shouldAnimateMovement) { - targetAnimatorSet?.cancel() - animateMovement(currentBounds, targetRect, true) - } - } else { - // If we are not offsetting the sensor, move it back to its original place - animateMovement(currentBounds, sensorRectBounds.toRectF(), false) - } - - invalidateSelf() - } - - /** Update the stage of the icon */ - fun updateStage(it: StageViewModel) { - helper.onUpdateStage(it) - invalidateSelf() - } - /** Stop drawing the fingerprint icon. */ fun stopDrawing() { alpha = 0 @@ -211,6 +172,7 @@ class UdfpsEnrollIconV2 internal constructor(context: Context, attrs: AttributeS if (currentBounds.equals(offsetRect)) { return } + val xAnimator = ValueAnimator.ofFloat(currentBounds.left.toFloat(), offsetRect.left) xAnimator.addUpdateListener { currX = it.animatedValue as Float @@ -260,6 +222,40 @@ class UdfpsEnrollIconV2 internal constructor(context: Context, attrs: AttributeS targetAnimationDuration = TARGET_ANIM_DURATION_LONG } + /** + * Indicates a change to guided enrollment has occurred. Also indicates if we are recreating the + * view, in which case their is no need to animate the icon to whatever position it was in. + */ + fun updateGuidedEnrollment(point: PointF, isRecreating: Boolean) { + guidedEnrollmentOffset = point + if (isRecreating) { + setAnimationTimeToZero() + } else { + restoreAnimationTime() + } + + val currentBounds = getCurrLocation().toRect() + val offset = guidedEnrollmentOffset + if (offset?.x != 0f && offset?.y != 0f) { + val targetRect = Rect(sensorRectBounds).toRectF() + // This is the desired location of the sensor rect, the [EnrollHelper] + // offsets the initial sensor rect by a bit to get the user to move their finger a bit more. + targetRect.offset(offset!!.x, offset!!.y) + val shouldAnimateMovement = !currentBounds.equals(targetRect) + if (shouldAnimateMovement) { + targetAnimatorSet?.cancel() + animateMovement(currentBounds, targetRect, true) + } else { + // If we are not offsetting the sensor, move it back to its original place + animateMovement(currentBounds, sensorRectBounds.toRectF(), false) + } + } else { + // If we are not offsetting the sensor, move it back to its original place + animateMovement(currentBounds, sensorRectBounds.toRectF(), false) + } + invalidateSelf() + } + companion object { private const val TAG = "UdfpsEnrollDrawableV2" private const val DEFAULT_STROKE_WIDTH = 3f diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollProgressBarDrawableV2.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollProgressBarDrawableV2.kt index 8f0e84564a6..bf2f0261112 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollProgressBarDrawableV2.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollProgressBarDrawableV2.kt @@ -27,10 +27,12 @@ import android.graphics.Rect import android.graphics.drawable.Drawable import android.util.AttributeSet import android.util.DisplayMetrics +import android.util.Log import android.view.animation.DecelerateInterpolator import android.view.animation.Interpolator import android.view.animation.OvershootInterpolator import androidx.annotation.ColorInt +import androidx.core.animation.addListener import androidx.core.animation.doOnEnd import androidx.core.graphics.toRectF import com.android.internal.annotations.VisibleForTesting @@ -46,6 +48,7 @@ import kotlin.math.sin class UdfpsEnrollProgressBarDrawableV2(private val context: Context, attrs: AttributeSet?) : Drawable() { private val sensorRect: Rect = Rect() + private var onFinishedCompletionAnimation: (() -> Unit)? = null private var rotation: Int = 0 private val strokeWidthPx: Float @@ -287,6 +290,12 @@ class UdfpsEnrollProgressBarDrawableV2(private val context: Context, attrs: Attr checkMarkDrawable.bounds = newBounds checkMarkDrawable.setVisible(true, false) } + doOnEnd { + onFinishedCompletionAnimation?.let{ + it() + } + + } start() } } @@ -380,6 +389,13 @@ class UdfpsEnrollProgressBarDrawableV2(private val context: Context, attrs: Attr checkmarkAnimationDuration = CHECKMARK_ANIMATION_DURATION_MS } + /** + * Indicates that the finish animation has completed, and enrollment can proceed to the next stage + */ + fun setFinishAnimationCompleted(onFinishedAnimation: () -> Unit) { + this.onFinishedCompletionAnimation = onFinishedAnimation + } + companion object { private const val TAG = "UdfpsProgressBar" private const val FILL_COLOR_ANIMATION_DURATION_MS = 350L diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollViewV2.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollViewV2.kt index b355f7735d7..586408f0c28 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollViewV2.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollViewV2.kt @@ -18,6 +18,7 @@ package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrol import android.content.Context import android.graphics.Point +import android.graphics.PointF import android.graphics.Rect import android.util.AttributeSet import android.util.Log @@ -31,7 +32,6 @@ import android.widget.FrameLayout import android.widget.ImageView import com.android.settings.R import com.android.settings.biometrics.fingerprint2.lib.model.FingerEnrollState -import com.android.settings.biometrics.fingerprint2.lib.model.StageViewModel import com.android.systemui.biometrics.UdfpsUtils import com.android.systemui.biometrics.shared.model.FingerprintSensorType import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams @@ -53,6 +53,13 @@ class UdfpsEnrollViewV2(context: Context, attrs: AttributeSet?) : FrameLayout(co private val udfpsUtils: UdfpsUtils = UdfpsUtils() private lateinit var touchExplorationAnnouncer: TouchExplorationAnnouncer private var isRecreating = false + private var onFinishedCompletionAnimation: (() -> Unit)? = null + + init { + fingerprintProgressDrawable.setFinishAnimationCompleted { + onFinishedCompletionAnimation?.let { it() } + } + } /** * This function computes the center (x,y) location with respect to the parent [FrameLayout] for @@ -112,11 +119,6 @@ class UdfpsEnrollViewV2(context: Context, attrs: AttributeSet?) : FrameLayout(co touchExplorationAnnouncer = TouchExplorationAnnouncer(context, this, overlayParams, udfpsUtils) } - /** Updates the current enrollment stage. */ - fun updateStage(it: StageViewModel) { - fingerprintIcon.updateStage(it) - } - /** Receive enroll progress event */ fun onUdfpsEvent(event: FingerEnrollState) { when (event) { @@ -174,7 +176,6 @@ class UdfpsEnrollViewV2(context: Context, attrs: AttributeSet?) : FrameLayout(co /** Receive enroll progress event */ private fun onEnrollmentProgress(remaining: Int, totalSteps: Int) { - fingerprintIcon.onEnrollmentProgress(remaining, totalSteps) fingerprintProgressDrawable.onEnrollmentProgress(remaining, totalSteps) } @@ -241,10 +242,25 @@ class UdfpsEnrollViewV2(context: Context, attrs: AttributeSet?) : FrameLayout(co /** Indicates we should should restore the views saved state. */ fun onEnrollProgressSaved(it: FingerEnrollState.EnrollProgress) { - fingerprintIcon.onEnrollmentProgress(it.remainingSteps, it.totalStepsRequired, true) fingerprintProgressDrawable.onEnrollmentProgress(it.remainingSteps, it.totalStepsRequired, true) } + /** Indicates we are recreating the UI from a saved state. */ + fun onGuidedPointSaved(it: PointF) { + fingerprintIcon.updateGuidedEnrollment(it, true) + } + + /** + * Indicates that the finish animation has completed, and enrollment can proceed to the next stage + */ + fun setFinishAnimationCompleted(onFinishedAnimation: () -> Unit) { + this.onFinishedCompletionAnimation = onFinishedAnimation + } + + fun updateGuidedEnrollment(point: PointF) { + fingerprintIcon.updateGuidedEnrollment(point, false) + } + companion object { private const val TAG = "UdfpsEnrollView" } diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintNavigationStep.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintNavigationStep.kt index 76b4895f2bb..ecb330ea62b 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintNavigationStep.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintNavigationStep.kt @@ -88,7 +88,10 @@ sealed interface FingerprintNavigationStep { } /** UiSteps should have a 1 to 1 mapping between each screen of FingerprintEnrollment */ - sealed class UiStep : FingerprintNavigationStep + sealed class UiStep( + val enterTransition: Transition = Transition.EnterFromRight, + val exitTransition: Transition = Transition.ExitToLeft, + ) : FingerprintNavigationStep /** This is the landing page for enrollment, where no content is shown. */ data object Init : UiStep() { @@ -103,7 +106,7 @@ sealed interface FingerprintNavigationStep { } else if (state.flowType is FastEnroll) { TransitionStep(Enrollment(state.fingerprintSensor!!)) } else { - TransitionStep(Introduction) + TransitionStep(Introduction()) } } else -> null @@ -118,7 +121,7 @@ sealed interface FingerprintNavigationStep { action: FingerprintAction, ): FingerprintNavigationStep? { return when (action) { - FingerprintAction.CONFIRM_DEVICE_SUCCESS -> TransitionStep(Introduction) + FingerprintAction.CONFIRM_DEVICE_SUCCESS -> TransitionStep(Introduction()) FingerprintAction.CONFIRM_DEVICE_FAIL -> Finish(null) else -> null } @@ -126,7 +129,10 @@ sealed interface FingerprintNavigationStep { } /** Indicates the FingerprintIntroduction screen is being presented to the user */ - data object Introduction : UiStep() { + class Introduction( + enterTransition: Transition = Transition.EnterFromRight, + exitTransition: Transition = Transition.ExitToLeft, + ) : UiStep(enterTransition, exitTransition) { override fun update( state: NavigationState, action: FingerprintAction, @@ -141,7 +147,11 @@ sealed interface FingerprintNavigationStep { } /** Indicates the FingerprintEducation screen is being presented to the user */ - data class Education(val sensor: FingerprintSensor) : UiStep() { + class Education( + val sensor: FingerprintSensor, + enterTransition: Transition = Transition.EnterFromRight, + exitTransition: Transition = Transition.ExitToLeft, + ) : UiStep(enterTransition, exitTransition) { override fun update( state: NavigationState, action: FingerprintAction, @@ -149,7 +159,8 @@ sealed interface FingerprintNavigationStep { return when (action) { FingerprintAction.NEXT -> TransitionStep(Enrollment(state.fingerprintSensor!!)) FingerprintAction.NEGATIVE_BUTTON_PRESSED, - FingerprintAction.PREV -> TransitionStep(Introduction) + FingerprintAction.PREV -> + TransitionStep(Introduction(Transition.EnterFromLeft, Transition.ExitToRight)) else -> null } } @@ -179,7 +190,10 @@ sealed interface FingerprintNavigationStep { ): FingerprintNavigationStep? { return when (action) { FingerprintAction.NEXT -> Finish(null) - FingerprintAction.PREV -> TransitionStep(Education(state.fingerprintSensor!!)) + FingerprintAction.PREV -> + TransitionStep( + Education(state.fingerprintSensor!!, Transition.EnterFromLeft, Transition.ExitToRight) + ) FingerprintAction.ADD_ANOTHER -> TransitionStep(Enrollment(state.fingerprintSensor!!)) else -> null } diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/TransitionViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/TransitionViewModel.kt new file mode 100644 index 00000000000..8fb72916cd7 --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/TransitionViewModel.kt @@ -0,0 +1,41 @@ +/* + * 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.biometrics.fingerprint2.ui.enrollment.viewmodel + +/** Indicates the type of transitions that can occur between fragments */ +sealed class Transition { + /** + * Indicates the new fragment should slide in from the left side + */ + data object EnterFromLeft : Transition() + + /** + * Indicates the new fragment should slide in from the right side + */ + data object EnterFromRight : Transition() + + /** + * Indicates the old fragment should slide out to the left side + */ + data object ExitToLeft : Transition() + + /** + * Indicates the old fragment should slide out to the right side + */ + data object ExitToRight : Transition() +} + diff --git a/tests/robotests/src/com/android/settings/biometrics/fingerprint2/fragment/FingerprintEnrollIntroFragmentTest.kt b/tests/robotests/src/com/android/settings/biometrics/fingerprint2/fragment/FingerprintEnrollIntroFragmentTest.kt index e876289866e..e30819bcb2f 100644 --- a/tests/robotests/src/com/android/settings/biometrics/fingerprint2/fragment/FingerprintEnrollIntroFragmentTest.kt +++ b/tests/robotests/src/com/android/settings/biometrics/fingerprint2/fragment/FingerprintEnrollIntroFragmentTest.kt @@ -90,7 +90,7 @@ class FingerprintEnrollIntroFragmentTest { private val navigationViewModel = FingerprintNavigationViewModel( - Introduction, + Introduction(), false, flowViewModel, interactor diff --git a/tests/screenshot/src/com/android/settings/tests/screenshot/biometrics/fingerprint/fragment/FingerprintEnrollIntroScreenshotTest.kt b/tests/screenshot/src/com/android/settings/tests/screenshot/biometrics/fingerprint/fragment/FingerprintEnrollIntroScreenshotTest.kt index 40a23d7fd56..68d600b1298 100644 --- a/tests/screenshot/src/com/android/settings/tests/screenshot/biometrics/fingerprint/fragment/FingerprintEnrollIntroScreenshotTest.kt +++ b/tests/screenshot/src/com/android/settings/tests/screenshot/biometrics/fingerprint/fragment/FingerprintEnrollIntroScreenshotTest.kt @@ -28,7 +28,7 @@ import platform.test.screenshot.ViewScreenshotTestRule.Mode @RunWith(AndroidJUnit4::class) class FingerprintEnrollIntroScreenshotTest { - private val injector: Injector = Injector(FingerprintNavigationStep.Introduction) + private val injector: Injector = Injector(FingerprintNavigationStep.Introduction()) @Rule @JvmField From 4506b6287a81fe380181672ddef363fb240a4b27 Mon Sep 17 00:00:00 2001 From: Omer Ozer Date: Tue, 21 May 2024 00:27:08 +0000 Subject: [PATCH 06/18] Add get permissions flag to package info query in factory reset preference controller. Bug: 341802418 Test: manual Change-Id: I1603662b706ec94e109afe957b585577876c8e75 --- .../settings/system/FactoryResetPreferenceController.java | 3 ++- .../system/FactoryResetPreferenceControllerTest.java | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/com/android/settings/system/FactoryResetPreferenceController.java b/src/com/android/settings/system/FactoryResetPreferenceController.java index 6d811797188..df7cc3df3a2 100644 --- a/src/com/android/settings/system/FactoryResetPreferenceController.java +++ b/src/com/android/settings/system/FactoryResetPreferenceController.java @@ -90,7 +90,8 @@ public class FactoryResetPreferenceController extends BasePreferenceController { String packageName = resolution.activityInfo.packageName; PackageInfo factoryResetWizardPackageInfo; try { - factoryResetWizardPackageInfo = pm.getPackageInfo(packageName, 0); + factoryResetWizardPackageInfo = pm.getPackageInfo(packageName, + PackageManager.GET_PERMISSIONS); } catch (PackageManager.NameNotFoundException e) { Log.e(TAG, "Unable to resolve a Factory Reset Handler Application"); return null; diff --git a/tests/robotests/src/com/android/settings/system/FactoryResetPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/system/FactoryResetPreferenceControllerTest.java index 0dede096ce8..383ed94b7db 100644 --- a/tests/robotests/src/com/android/settings/system/FactoryResetPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/system/FactoryResetPreferenceControllerTest.java @@ -145,10 +145,13 @@ public class FactoryResetPreferenceControllerTest { @Test @RequiresFlagsEnabled(com.android.settings.factory_reset.Flags.FLAG_ENABLE_FACTORY_RESET_WIZARD) - public void handlePreference_factoryResetWizardEnabled() { + public void handlePreference_factoryResetWizardEnabled() + throws PackageManager.NameNotFoundException { ArgumentCaptor intentArgumentCaptor = ArgumentCaptor.forClass(Intent.class); assertThat(mController.handlePreferenceTreeClick(mPreference)).isTrue(); + verify(mPackageManager).getPackageInfo(eq(FACTORY_RESET_APP_PACKAGE), + eq(PackageManager.GET_PERMISSIONS)); verify(mFactoryResetLauncher).launch(intentArgumentCaptor.capture()); assertThat(intentArgumentCaptor.getValue()).isNotNull(); assertThat(intentArgumentCaptor.getValue().getAction()) From 7a8d8fd091ed30b292ed102ffbf68be171f29007 Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Mon, 20 May 2024 10:35:01 +0800 Subject: [PATCH 07/18] Reapply "Migrate AppOps" This reverts commit 19b01bd68b9c989e7323eccabb8fa3116a60300b. Bug: 341016088 Test: manual - on Special app access Change-Id: I000274ae9c725f848e1c8910120ba886fcc5be95 Merged-In: I000274ae9c725f848e1c8910120ba886fcc5be95 --- .../specialaccess/AlarmsAndRemindersAppList.kt | 9 +++++++-- .../spa/app/specialaccess/AllFilesAccess.kt | 7 +++++-- .../spa/app/specialaccess/BackupTasksApps.kt | 4 ++-- .../spa/app/specialaccess/DisplayOverOtherApps.kt | 3 ++- .../spa/app/specialaccess/InstallUnknownApps.kt | 11 ++++------- .../app/specialaccess/LongBackgroundTasksApps.kt | 7 +++++-- .../spa/app/specialaccess/MediaManagementApps.kt | 7 +++++-- .../spa/app/specialaccess/MediaRoutingControl.kt | 7 +++++-- .../spa/app/specialaccess/ModifySystemSettings.kt | 3 ++- .../spa/app/specialaccess/PictureInPicture.kt | 11 ++++------- .../spa/app/specialaccess/TurnScreenOnApps.kt | 8 +++++--- .../spa/app/specialaccess/UseFullScreenIntent.kt | 7 +++++-- .../spa/app/specialaccess/WifiControlApps.kt | 7 +++++-- .../spa/app/WifiControlAppListModelTest.kt | 2 +- .../spa/app/specialaccess/AllFilesAccessTest.kt | 9 +++++++-- .../specialaccess/LongBackgroundTasksAppsTest.kt | 9 +++++++-- .../app/specialaccess/MediaManagementAppsTest.kt | 9 +++++++-- .../app/specialaccess/MediaRoutingControlTest.kt | 15 ++++++++++----- .../spa/app/specialaccess/PictureInPictureTest.kt | 3 ++- .../spa/app/specialaccess/TurnScreenOnAppsTest.kt | 9 +++++++-- 20 files changed, 97 insertions(+), 50 deletions(-) diff --git a/src/com/android/settings/spa/app/specialaccess/AlarmsAndRemindersAppList.kt b/src/com/android/settings/spa/app/specialaccess/AlarmsAndRemindersAppList.kt index 2b8d12d12d0..b15675ed14f 100644 --- a/src/com/android/settings/spa/app/specialaccess/AlarmsAndRemindersAppList.kt +++ b/src/com/android/settings/spa/app/specialaccess/AlarmsAndRemindersAppList.kt @@ -28,6 +28,7 @@ import androidx.compose.runtime.Composable import com.android.settings.overlay.FeatureFactory.Companion.featureFactory import com.android.settingslib.R import com.android.settingslib.spa.lifecycle.collectAsCallbackWithLifecycle +import com.android.settingslib.spaprivileged.model.app.AppOps import com.android.settingslib.spaprivileged.model.app.AppOpsController import com.android.settingslib.spaprivileged.model.app.AppRecord import com.android.settingslib.spaprivileged.model.app.IPackageManagers @@ -116,8 +117,7 @@ class AlarmsAndRemindersAppListModel( controller = AppOpsController( context = context, app = app, - op = AppOpsManager.OP_SCHEDULE_EXACT_ALARM, - setModeByUid = true, + appOps = APP_OPS, ), ) } @@ -136,6 +136,11 @@ class AlarmsAndRemindersAppListModel( } companion object { + private val APP_OPS = AppOps( + op = AppOpsManager.OP_SCHEDULE_EXACT_ALARM, + setModeByUid = true, + ) + private const val PERMISSION: String = Manifest.permission.SCHEDULE_EXACT_ALARM /** Checks whether [Manifest.permission.SCHEDULE_EXACT_ALARM] is enabled. */ diff --git a/src/com/android/settings/spa/app/specialaccess/AllFilesAccess.kt b/src/com/android/settings/spa/app/specialaccess/AllFilesAccess.kt index d82045d17b4..f6c08c0ca7c 100644 --- a/src/com/android/settings/spa/app/specialaccess/AllFilesAccess.kt +++ b/src/com/android/settings/spa/app/specialaccess/AllFilesAccess.kt @@ -22,6 +22,7 @@ import android.app.settings.SettingsEnums import android.content.Context import com.android.settings.R import com.android.settings.overlay.FeatureFactory.Companion.featureFactory +import com.android.settingslib.spaprivileged.model.app.AppOps import com.android.settingslib.spaprivileged.template.app.AppOpPermissionListModel import com.android.settingslib.spaprivileged.template.app.AppOpPermissionRecord import com.android.settingslib.spaprivileged.template.app.TogglePermissionAppListProvider @@ -35,9 +36,11 @@ class AllFilesAccessListModel(context: Context) : AppOpPermissionListModel(conte override val pageTitleResId = R.string.manage_external_storage_title override val switchTitleResId = R.string.permit_manage_external_storage override val footerResId = R.string.allow_manage_external_storage_description - override val appOp = AppOpsManager.OP_MANAGE_EXTERNAL_STORAGE + override val appOps = AppOps( + op = AppOpsManager.OP_MANAGE_EXTERNAL_STORAGE, + setModeByUid = true, + ) override val permission = Manifest.permission.MANAGE_EXTERNAL_STORAGE - override val setModeByUid = true override fun setAllowed(record: AppOpPermissionRecord, newAllowed: Boolean) { super.setAllowed(record, newAllowed) diff --git a/src/com/android/settings/spa/app/specialaccess/BackupTasksApps.kt b/src/com/android/settings/spa/app/specialaccess/BackupTasksApps.kt index d6d8fd4a172..7bb1233d1ba 100644 --- a/src/com/android/settings/spa/app/specialaccess/BackupTasksApps.kt +++ b/src/com/android/settings/spa/app/specialaccess/BackupTasksApps.kt @@ -22,6 +22,7 @@ import android.app.settings.SettingsEnums import android.content.Context import com.android.settings.R import com.android.settings.overlay.FeatureFactory.Companion.featureFactory +import com.android.settingslib.spaprivileged.model.app.AppOps import com.android.settingslib.spaprivileged.template.app.AppOpPermissionListModel import com.android.settingslib.spaprivileged.template.app.AppOpPermissionRecord import com.android.settingslib.spaprivileged.template.app.TogglePermissionAppListProvider @@ -35,9 +36,8 @@ class BackupTasksAppsListModel(context: Context) : AppOpPermissionListModel(cont override val pageTitleResId = R.string.run_backup_tasks_title override val switchTitleResId = R.string.run_backup_tasks_switch_title override val footerResId = R.string.run_backup_tasks_footer_title - override val appOp = AppOpsManager.OP_RUN_BACKUP_JOBS + override val appOps = AppOps(op = AppOpsManager.OP_RUN_BACKUP_JOBS, setModeByUid = true) override val permission = Manifest.permission.RUN_BACKUP_JOBS - override val setModeByUid = true override fun setAllowed(record: AppOpPermissionRecord, newAllowed: Boolean) { super.setAllowed(record, newAllowed) diff --git a/src/com/android/settings/spa/app/specialaccess/DisplayOverOtherApps.kt b/src/com/android/settings/spa/app/specialaccess/DisplayOverOtherApps.kt index 26d74518411..904f0afd3d6 100644 --- a/src/com/android/settings/spa/app/specialaccess/DisplayOverOtherApps.kt +++ b/src/com/android/settings/spa/app/specialaccess/DisplayOverOtherApps.kt @@ -22,6 +22,7 @@ import android.app.settings.SettingsEnums import android.content.Context import com.android.settings.R import com.android.settings.overlay.FeatureFactory.Companion.featureFactory +import com.android.settingslib.spaprivileged.model.app.AppOps import com.android.settingslib.spaprivileged.template.app.AppOpPermissionListModel import com.android.settingslib.spaprivileged.template.app.AppOpPermissionRecord import com.android.settingslib.spaprivileged.template.app.TogglePermissionAppListProvider @@ -35,7 +36,7 @@ class DisplayOverOtherAppsListModel(context: Context) : AppOpPermissionListModel override val pageTitleResId = R.string.system_alert_window_settings override val switchTitleResId = R.string.permit_draw_overlay override val footerResId = R.string.allow_overlay_description - override val appOp = AppOpsManager.OP_SYSTEM_ALERT_WINDOW + override val appOps = AppOps(AppOpsManager.OP_SYSTEM_ALERT_WINDOW) override val permission = Manifest.permission.SYSTEM_ALERT_WINDOW override fun setAllowed(record: AppOpPermissionRecord, newAllowed: Boolean) { diff --git a/src/com/android/settings/spa/app/specialaccess/InstallUnknownApps.kt b/src/com/android/settings/spa/app/specialaccess/InstallUnknownApps.kt index 3e9058f25ff..cceb5977ad3 100644 --- a/src/com/android/settings/spa/app/specialaccess/InstallUnknownApps.kt +++ b/src/com/android/settings/spa/app/specialaccess/InstallUnknownApps.kt @@ -20,13 +20,13 @@ import android.Manifest import android.app.AppGlobals import android.app.AppOpsManager import android.app.AppOpsManager.MODE_DEFAULT -import android.app.AppOpsManager.OP_REQUEST_INSTALL_PACKAGES import android.content.Context import android.content.pm.ApplicationInfo import android.os.UserManager import androidx.compose.runtime.Composable import com.android.settings.R import com.android.settingslib.spa.lifecycle.collectAsCallbackWithLifecycle +import com.android.settingslib.spaprivileged.model.app.AppOps import com.android.settingslib.spaprivileged.model.app.AppOpsController import com.android.settingslib.spaprivileged.model.app.AppRecord import com.android.settingslib.spaprivileged.model.app.userId @@ -61,12 +61,7 @@ class InstallUnknownAppsListModel(private val context: Context) : override fun transformItem(app: ApplicationInfo) = InstallUnknownAppsRecord( app = app, - appOpsController = - AppOpsController( - context = context, - app = app, - op = OP_REQUEST_INSTALL_PACKAGES, - ), + appOpsController = AppOpsController(context = context, app = app, appOps = APP_OPS), ) override fun filter( @@ -91,6 +86,8 @@ class InstallUnknownAppsListModel(private val context: Context) : } companion object { + private val APP_OPS = AppOps(AppOpsManager.OP_REQUEST_INSTALL_PACKAGES) + private fun isChangeable( record: InstallUnknownAppsRecord, potentialPackageNames: Set, diff --git a/src/com/android/settings/spa/app/specialaccess/LongBackgroundTasksApps.kt b/src/com/android/settings/spa/app/specialaccess/LongBackgroundTasksApps.kt index 3ba9b085d23..d897b5231db 100644 --- a/src/com/android/settings/spa/app/specialaccess/LongBackgroundTasksApps.kt +++ b/src/com/android/settings/spa/app/specialaccess/LongBackgroundTasksApps.kt @@ -22,6 +22,7 @@ import android.app.settings.SettingsEnums import android.content.Context import com.android.settings.R import com.android.settings.overlay.FeatureFactory.Companion.featureFactory +import com.android.settingslib.spaprivileged.model.app.AppOps import com.android.settingslib.spaprivileged.template.app.AppOpPermissionListModel import com.android.settingslib.spaprivileged.template.app.AppOpPermissionRecord import com.android.settingslib.spaprivileged.template.app.TogglePermissionAppListProvider @@ -35,9 +36,11 @@ class LongBackgroundTasksAppsListModel(context: Context) : AppOpPermissionListMo override val pageTitleResId = R.string.long_background_tasks_title override val switchTitleResId = R.string.long_background_tasks_switch_title override val footerResId = R.string.long_background_tasks_footer_title - override val appOp = AppOpsManager.OP_RUN_USER_INITIATED_JOBS + override val appOps = AppOps( + op = AppOpsManager.OP_RUN_USER_INITIATED_JOBS, + setModeByUid = true, + ) override val permission = Manifest.permission.RUN_USER_INITIATED_JOBS - override val setModeByUid = true override fun setAllowed(record: AppOpPermissionRecord, newAllowed: Boolean) { super.setAllowed(record, newAllowed) diff --git a/src/com/android/settings/spa/app/specialaccess/MediaManagementApps.kt b/src/com/android/settings/spa/app/specialaccess/MediaManagementApps.kt index 3e3457c2018..ce6abbdf2b8 100644 --- a/src/com/android/settings/spa/app/specialaccess/MediaManagementApps.kt +++ b/src/com/android/settings/spa/app/specialaccess/MediaManagementApps.kt @@ -22,6 +22,7 @@ import android.app.settings.SettingsEnums import android.content.Context import com.android.settings.R import com.android.settings.overlay.FeatureFactory.Companion.featureFactory +import com.android.settingslib.spaprivileged.model.app.AppOps import com.android.settingslib.spaprivileged.template.app.AppOpPermissionListModel import com.android.settingslib.spaprivileged.template.app.AppOpPermissionRecord import com.android.settingslib.spaprivileged.template.app.TogglePermissionAppListProvider @@ -35,9 +36,11 @@ class MediaManagementAppsListModel(context: Context) : AppOpPermissionListModel( override val pageTitleResId = R.string.media_management_apps_title override val switchTitleResId = R.string.media_management_apps_toggle_label override val footerResId = R.string.media_management_apps_description - override val appOp = AppOpsManager.OP_MANAGE_MEDIA + override val appOps = AppOps( + op = AppOpsManager.OP_MANAGE_MEDIA, + setModeByUid = true, + ) override val permission = Manifest.permission.MANAGE_MEDIA - override val setModeByUid = true override fun setAllowed(record: AppOpPermissionRecord, newAllowed: Boolean) { super.setAllowed(record, newAllowed) diff --git a/src/com/android/settings/spa/app/specialaccess/MediaRoutingControl.kt b/src/com/android/settings/spa/app/specialaccess/MediaRoutingControl.kt index b9fb9b8cd9a..21b9400da0a 100644 --- a/src/com/android/settings/spa/app/specialaccess/MediaRoutingControl.kt +++ b/src/com/android/settings/spa/app/specialaccess/MediaRoutingControl.kt @@ -25,6 +25,7 @@ import android.content.Context import com.android.media.flags.Flags; import com.android.settings.R import com.android.settings.overlay.FeatureFactory.Companion.featureFactory +import com.android.settingslib.spaprivileged.model.app.AppOps import com.android.settingslib.spaprivileged.template.app.AppOpPermissionListModel import com.android.settingslib.spaprivileged.template.app.AppOpPermissionRecord import com.android.settingslib.spaprivileged.template.app.TogglePermissionAppListProvider @@ -38,9 +39,11 @@ class MediaRoutingControlAppsListModel(context: Context) : AppOpPermissionListMo override val pageTitleResId = R.string.media_routing_control_title override val switchTitleResId = R.string.allow_media_routing_control override val footerResId = R.string.allow_media_routing_description - override val appOp = AppOpsManager.OP_MEDIA_ROUTING_CONTROL + override val appOps = AppOps( + op = AppOpsManager.OP_MEDIA_ROUTING_CONTROL, + setModeByUid = true, + ) override val permission = Manifest.permission.MEDIA_ROUTING_CONTROL - override val setModeByUid = true private val roleManager = context.getSystemService(RoleManager::class.java) override fun setAllowed(record: AppOpPermissionRecord, newAllowed: Boolean) { diff --git a/src/com/android/settings/spa/app/specialaccess/ModifySystemSettings.kt b/src/com/android/settings/spa/app/specialaccess/ModifySystemSettings.kt index 4ab7f52cc29..e5f65f5a4b7 100644 --- a/src/com/android/settings/spa/app/specialaccess/ModifySystemSettings.kt +++ b/src/com/android/settings/spa/app/specialaccess/ModifySystemSettings.kt @@ -22,6 +22,7 @@ import android.app.settings.SettingsEnums import android.content.Context import com.android.settings.R import com.android.settings.overlay.FeatureFactory.Companion.featureFactory +import com.android.settingslib.spaprivileged.model.app.AppOps import com.android.settingslib.spaprivileged.template.app.AppOpPermissionListModel import com.android.settingslib.spaprivileged.template.app.AppOpPermissionRecord import com.android.settingslib.spaprivileged.template.app.TogglePermissionAppListProvider @@ -35,7 +36,7 @@ class ModifySystemSettingsListModel(context: Context) : AppOpPermissionListModel override val pageTitleResId = R.string.write_system_settings override val switchTitleResId = R.string.permit_write_settings override val footerResId = R.string.write_settings_description - override val appOp = AppOpsManager.OP_WRITE_SETTINGS + override val appOps = AppOps(AppOpsManager.OP_WRITE_SETTINGS) override val permission = Manifest.permission.WRITE_SETTINGS override fun setAllowed(record: AppOpPermissionRecord, newAllowed: Boolean) { diff --git a/src/com/android/settings/spa/app/specialaccess/PictureInPicture.kt b/src/com/android/settings/spa/app/specialaccess/PictureInPicture.kt index 7885b869b83..d767f737d21 100644 --- a/src/com/android/settings/spa/app/specialaccess/PictureInPicture.kt +++ b/src/com/android/settings/spa/app/specialaccess/PictureInPicture.kt @@ -17,7 +17,6 @@ package com.android.settings.spa.app.specialaccess import android.app.AppOpsManager -import android.app.AppOpsManager.OP_PICTURE_IN_PICTURE import android.content.Context import android.content.pm.ActivityInfo import android.content.pm.ApplicationInfo @@ -28,6 +27,7 @@ import android.util.Log import androidx.compose.runtime.Composable import com.android.settings.R import com.android.settingslib.spa.lifecycle.collectAsCallbackWithLifecycle +import com.android.settingslib.spaprivileged.model.app.AppOps import com.android.settingslib.spaprivileged.model.app.AppOpsController import com.android.settingslib.spaprivileged.model.app.AppRecord import com.android.settingslib.spaprivileged.model.app.installed @@ -79,12 +79,7 @@ class PictureInPictureListModel(private val context: Context) : PictureInPictureRecord( app = app, isSupport = isSupport, - appOpsController = - AppOpsController( - context = context, - app = app, - op = OP_PICTURE_IN_PICTURE, - ), + appOpsController = AppOpsController(context = context, app = app, appOps = APP_OPS), ) override fun filter(userIdFlow: Flow, recordListFlow: Flow>) = @@ -131,6 +126,8 @@ class PictureInPictureListModel(private val context: Context) : companion object { private const val TAG = "PictureInPictureListModel" + private val APP_OPS = AppOps(AppOpsManager.OP_PICTURE_IN_PICTURE) + private fun PackageInfo.supportsPictureInPicture() = activities?.any(ActivityInfo::supportsPictureInPicture) ?: false diff --git a/src/com/android/settings/spa/app/specialaccess/TurnScreenOnApps.kt b/src/com/android/settings/spa/app/specialaccess/TurnScreenOnApps.kt index 262acb78d15..db9fe448b5a 100644 --- a/src/com/android/settings/spa/app/specialaccess/TurnScreenOnApps.kt +++ b/src/com/android/settings/spa/app/specialaccess/TurnScreenOnApps.kt @@ -20,8 +20,8 @@ import android.Manifest import android.app.AppOpsManager import android.app.settings.SettingsEnums import android.content.Context -import com.android.settings.R import com.android.settings.overlay.FeatureFactory.Companion.featureFactory +import com.android.settingslib.spaprivileged.model.app.AppOps import com.android.settingslib.spaprivileged.template.app.AppOpPermissionListModel import com.android.settingslib.spaprivileged.template.app.AppOpPermissionRecord import com.android.settingslib.spaprivileged.template.app.TogglePermissionAppListProvider @@ -35,9 +35,11 @@ class TurnScreenOnAppsListModel(context: Context) : AppOpPermissionListModel(con override val pageTitleResId = com.android.settingslib.R.string.turn_screen_on_title override val switchTitleResId = com.android.settingslib.R.string.allow_turn_screen_on override val footerResId = com.android.settingslib.R.string.allow_turn_screen_on_description - override val appOp = AppOpsManager.OP_TURN_SCREEN_ON + override val appOps = AppOps( + op = AppOpsManager.OP_TURN_SCREEN_ON, + setModeByUid = true, + ) override val permission = Manifest.permission.TURN_SCREEN_ON - override val setModeByUid = true override fun setAllowed(record: AppOpPermissionRecord, newAllowed: Boolean) { super.setAllowed(record, newAllowed) diff --git a/src/com/android/settings/spa/app/specialaccess/UseFullScreenIntent.kt b/src/com/android/settings/spa/app/specialaccess/UseFullScreenIntent.kt index 514e480250c..cc41e63c779 100644 --- a/src/com/android/settings/spa/app/specialaccess/UseFullScreenIntent.kt +++ b/src/com/android/settings/spa/app/specialaccess/UseFullScreenIntent.kt @@ -20,6 +20,7 @@ import android.Manifest import android.app.AppOpsManager import android.content.Context import com.android.settings.R +import com.android.settingslib.spaprivileged.model.app.AppOps import com.android.settingslib.spaprivileged.template.app.AppOpPermissionListModel import com.android.settingslib.spaprivileged.template.app.TogglePermissionAppListProvider @@ -32,7 +33,9 @@ class UseFullScreenIntentListModel(context: Context) : AppOpPermissionListModel( override val pageTitleResId = R.string.full_screen_intent_title override val switchTitleResId = R.string.permit_full_screen_intent override val footerResId = R.string.footer_description_full_screen_intent - override val appOp = AppOpsManager.OP_USE_FULL_SCREEN_INTENT + override val appOps = AppOps( + op = AppOpsManager.OP_USE_FULL_SCREEN_INTENT, + setModeByUid = true, + ) override val permission = Manifest.permission.USE_FULL_SCREEN_INTENT - override val setModeByUid = true } diff --git a/src/com/android/settings/spa/app/specialaccess/WifiControlApps.kt b/src/com/android/settings/spa/app/specialaccess/WifiControlApps.kt index 50bb5c9b9f6..c160569b46a 100644 --- a/src/com/android/settings/spa/app/specialaccess/WifiControlApps.kt +++ b/src/com/android/settings/spa/app/specialaccess/WifiControlApps.kt @@ -21,6 +21,7 @@ import android.app.AppOpsManager import android.app.AppOpsManager.MODE_IGNORED import android.content.Context import com.android.settings.R +import com.android.settingslib.spaprivileged.model.app.AppOps import com.android.settingslib.spaprivileged.model.app.IPackageManagers import com.android.settingslib.spaprivileged.model.app.PackageManagers import com.android.settingslib.spaprivileged.template.app.AppOpPermissionListModel @@ -39,11 +40,13 @@ class WifiControlAppListModel( override val switchTitleResId = R.string.change_wifi_state_app_detail_switch override val footerResId = R.string.change_wifi_state_app_detail_summary - override val appOp = AppOpsManager.OP_CHANGE_WIFI_STATE + override val appOps = AppOps( + op = AppOpsManager.OP_CHANGE_WIFI_STATE, + modeForNotAllowed = MODE_IGNORED, + ) override val permission = Manifest.permission.CHANGE_WIFI_STATE /** NETWORK_SETTINGS permission trumps CHANGE_WIFI_CONFIG. */ override val broaderPermission = Manifest.permission.NETWORK_SETTINGS override val permissionHasAppOpFlag = false - override val modeForNotAllowed = MODE_IGNORED } diff --git a/tests/spa_unit/src/com/android/settings/spa/app/WifiControlAppListModelTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/WifiControlAppListModelTest.kt index 537764afe7c..863a6e2e97b 100644 --- a/tests/spa_unit/src/com/android/settings/spa/app/WifiControlAppListModelTest.kt +++ b/tests/spa_unit/src/com/android/settings/spa/app/WifiControlAppListModelTest.kt @@ -269,7 +269,7 @@ class WifiControlAppListModelTest { private class FakeAppOpsController(private val fakeMode: Int) : IAppOpsController { var setAllowedCalledWith: Boolean? = null - override val mode = flowOf(fakeMode) + override val modeFlow = flowOf(fakeMode) override fun setAllowed(allowed: Boolean) { setAllowedCalledWith = allowed diff --git a/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/AllFilesAccessTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/AllFilesAccessTest.kt index 4c65d90a627..bf48b841b88 100644 --- a/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/AllFilesAccessTest.kt +++ b/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/AllFilesAccessTest.kt @@ -22,6 +22,7 @@ import android.content.Context import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.settings.R +import com.android.settingslib.spaprivileged.model.app.AppOps import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith @@ -37,8 +38,12 @@ class AllFilesAccessTest { assertThat(listModel.pageTitleResId).isEqualTo(R.string.manage_external_storage_title) assertThat(listModel.switchTitleResId).isEqualTo(R.string.permit_manage_external_storage) assertThat(listModel.footerResId).isEqualTo(R.string.allow_manage_external_storage_description) - assertThat(listModel.appOp).isEqualTo(AppOpsManager.OP_MANAGE_EXTERNAL_STORAGE) + assertThat(listModel.appOps).isEqualTo( + AppOps( + op = AppOpsManager.OP_MANAGE_EXTERNAL_STORAGE, + setModeByUid = true, + ) + ) assertThat(listModel.permission).isEqualTo(Manifest.permission.MANAGE_EXTERNAL_STORAGE) - assertThat(listModel.setModeByUid).isTrue() } } \ No newline at end of file diff --git a/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/LongBackgroundTasksAppsTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/LongBackgroundTasksAppsTest.kt index 579c6c95508..dc56ddf856d 100644 --- a/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/LongBackgroundTasksAppsTest.kt +++ b/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/LongBackgroundTasksAppsTest.kt @@ -23,6 +23,7 @@ import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import com.android.settings.R +import com.android.settingslib.spaprivileged.model.app.AppOps import org.junit.Test import org.junit.runner.RunWith @@ -37,8 +38,12 @@ class LongBackgroundTasksAppsTest { assertThat(listModel.pageTitleResId).isEqualTo(R.string.long_background_tasks_title) assertThat(listModel.switchTitleResId).isEqualTo(R.string.long_background_tasks_switch_title) assertThat(listModel.footerResId).isEqualTo(R.string.long_background_tasks_footer_title) - assertThat(listModel.appOp).isEqualTo(AppOpsManager.OP_RUN_USER_INITIATED_JOBS) + assertThat(listModel.appOps).isEqualTo( + AppOps( + op = AppOpsManager.OP_RUN_USER_INITIATED_JOBS, + setModeByUid = true, + ) + ) assertThat(listModel.permission).isEqualTo(Manifest.permission.RUN_USER_INITIATED_JOBS) - assertThat(listModel.setModeByUid).isTrue() } } \ No newline at end of file diff --git a/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/MediaManagementAppsTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/MediaManagementAppsTest.kt index b901043f59f..70b4b48867b 100644 --- a/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/MediaManagementAppsTest.kt +++ b/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/MediaManagementAppsTest.kt @@ -22,6 +22,7 @@ import android.content.Context import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.settings.R +import com.android.settingslib.spaprivileged.model.app.AppOps import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith @@ -37,8 +38,12 @@ class MediaManagementAppsTest { assertThat(listModel.pageTitleResId).isEqualTo(R.string.media_management_apps_title) assertThat(listModel.switchTitleResId).isEqualTo(R.string.media_management_apps_toggle_label) assertThat(listModel.footerResId).isEqualTo(R.string.media_management_apps_description) - assertThat(listModel.appOp).isEqualTo(AppOpsManager.OP_MANAGE_MEDIA) + assertThat(listModel.appOps).isEqualTo( + AppOps( + op = AppOpsManager.OP_MANAGE_MEDIA, + setModeByUid = true, + ) + ) assertThat(listModel.permission).isEqualTo(Manifest.permission.MANAGE_MEDIA) - assertThat(listModel.setModeByUid).isTrue() } } \ No newline at end of file diff --git a/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/MediaRoutingControlTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/MediaRoutingControlTest.kt index 990ec5ce417..ec070fdde05 100644 --- a/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/MediaRoutingControlTest.kt +++ b/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/MediaRoutingControlTest.kt @@ -29,6 +29,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.media.flags.Flags import com.android.settings.R import com.android.settings.testutils.FakeFeatureFactory +import com.android.settingslib.spaprivileged.model.app.AppOps import com.android.settingslib.spaprivileged.model.app.IAppOpsController import com.android.settingslib.spaprivileged.template.app.AppOpPermissionRecord import com.google.common.truth.Truth.assertThat @@ -74,9 +75,13 @@ class MediaRoutingControlTest { assertThat(listModel.pageTitleResId).isEqualTo(R.string.media_routing_control_title) assertThat(listModel.switchTitleResId).isEqualTo(R.string.allow_media_routing_control) assertThat(listModel.footerResId).isEqualTo(R.string.allow_media_routing_description) - assertThat(listModel.appOp).isEqualTo(AppOpsManager.OP_MEDIA_ROUTING_CONTROL) + assertThat(listModel.appOps).isEqualTo( + AppOps( + op = AppOpsManager.OP_MEDIA_ROUTING_CONTROL, + setModeByUid = true, + ) + ) assertThat(listModel.permission).isEqualTo(Manifest.permission.MEDIA_ROUTING_CONTROL) - assertThat(listModel.setModeByUid).isTrue() } @Test @@ -223,13 +228,13 @@ class MediaRoutingControlTest { private class FakeAppOpsController(fakeMode: Int) : IAppOpsController { - override val mode = MutableStateFlow(fakeMode) + override val modeFlow = MutableStateFlow(fakeMode) override fun setAllowed(allowed: Boolean) { - mode.value = if (allowed) AppOpsManager.MODE_ALLOWED else AppOpsManager.MODE_ERRORED + modeFlow.value = if (allowed) AppOpsManager.MODE_ALLOWED else AppOpsManager.MODE_ERRORED } - override fun getMode(): Int = mode.value + override fun getMode(): Int = modeFlow.value } companion object { diff --git a/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/PictureInPictureTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/PictureInPictureTest.kt index 42292478f56..6e41e92b1b0 100644 --- a/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/PictureInPictureTest.kt +++ b/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/PictureInPictureTest.kt @@ -27,6 +27,7 @@ import android.os.DeadSystemRuntimeException import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.settings.R +import com.android.settingslib.spaprivileged.model.app.AppOps import com.android.settingslib.spaprivileged.model.app.AppOpsController import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.first @@ -179,7 +180,7 @@ class PictureInPictureTest { appOpsController = AppOpsController( context = context, app = PICTURE_IN_PICTURE_APP, - op = AppOpsManager.OP_PICTURE_IN_PICTURE, + appOps = AppOps(AppOpsManager.OP_PICTURE_IN_PICTURE), ), ) diff --git a/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/TurnScreenOnAppsTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/TurnScreenOnAppsTest.kt index 9c6079d5eca..1b1204f31e8 100644 --- a/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/TurnScreenOnAppsTest.kt +++ b/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/TurnScreenOnAppsTest.kt @@ -20,6 +20,7 @@ import android.app.AppOpsManager import android.content.Context import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settingslib.spaprivileged.model.app.AppOps import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith @@ -35,8 +36,12 @@ class TurnScreenOnAppsTest { assertThat(listModel.pageTitleResId).isEqualTo(com.android.settingslib.R.string.turn_screen_on_title) assertThat(listModel.switchTitleResId).isEqualTo(com.android.settingslib.R.string.allow_turn_screen_on) assertThat(listModel.footerResId).isEqualTo(com.android.settingslib.R.string.allow_turn_screen_on_description) - assertThat(listModel.appOp).isEqualTo(AppOpsManager.OP_TURN_SCREEN_ON) + assertThat(listModel.appOps).isEqualTo( + AppOps( + op = AppOpsManager.OP_TURN_SCREEN_ON, + setModeByUid = true, + ) + ) assertThat(listModel.permission).isEqualTo(Manifest.permission.TURN_SCREEN_ON) - assertThat(listModel.setModeByUid).isTrue() } } \ No newline at end of file From 9780020f6b443b86f3b148f595cd71059a488fbc Mon Sep 17 00:00:00 2001 From: Roy Chou Date: Mon, 20 May 2024 08:03:15 +0000 Subject: [PATCH 08/18] chore(magnification suw): create MagnificationFeaturePreferenceController Add a new abstract class MagnificationFeaturePreferenceController that extends TogglePreferenceController, to wrap inSetupWizard setter/getter. Then for magnification feature preference controllers like alwaysOn or followTyping, they can just check isInSetupWizard to decide whether hiding in setup wizard. Besides, in ToggleScreenMagnificationPreferenceFragment we cache a flag mInSetupWizard when fragment created, so we can pass the info to the preferece controllers when creating them. Bug: 340721852 Flag: NONE Test: build pass Change-Id: I05c59a766219862117d2a6ede775d68a4c3dedac --- ...nificationFeaturePreferenceController.java | 42 +++++++++++++++++++ ...ScreenMagnificationPreferenceFragment.java | 5 ++- 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 src/com/android/settings/accessibility/MagnificationFeaturePreferenceController.java diff --git a/src/com/android/settings/accessibility/MagnificationFeaturePreferenceController.java b/src/com/android/settings/accessibility/MagnificationFeaturePreferenceController.java new file mode 100644 index 00000000000..9664756ac45 --- /dev/null +++ b/src/com/android/settings/accessibility/MagnificationFeaturePreferenceController.java @@ -0,0 +1,42 @@ +/* + * 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.accessibility; + +import android.content.Context; + +import com.android.settings.core.TogglePreferenceController; + +/** + * A base preference controller of magnification feature with common methods. + */ +public abstract class MagnificationFeaturePreferenceController extends TogglePreferenceController { + + private boolean mInSetupWizard; + + protected MagnificationFeaturePreferenceController(Context context, + String preferenceKey) { + super(context, preferenceKey); + } + + protected final boolean isInSetupWizard() { + return mInSetupWizard; + } + + protected final void setInSetupWizard(boolean inSetupWizard) { + mInSetupWizard = inSetupWizard; + } +} diff --git a/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java b/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java index 25c8768375f..e3f53e47172 100644 --- a/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java +++ b/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java @@ -92,10 +92,13 @@ public class ToggleScreenMagnificationPreferenceFragment extends @Nullable private CheckBox mTwoFingerTripleTapTypeCheckBox; private DialogCreatable mDialogDelegate; + private boolean mInSetupWizard; + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getActivity().setTitle(R.string.accessibility_screen_magnification_title); + mInSetupWizard = WizardManagerHelper.isAnySetupWizard(getIntent()); } @Override @@ -169,7 +172,7 @@ public class ToggleScreenMagnificationPreferenceFragment extends .showAccessibilityGestureTutorialDialog(getPrefContext()); case DialogEnums.MAGNIFICATION_EDIT_SHORTCUT: final CharSequence dialogTitle = getShortcutTitle(); - final int dialogType = WizardManagerHelper.isAnySetupWizard(getIntent()) + final int dialogType = mInSetupWizard ? DialogType.EDIT_SHORTCUT_MAGNIFICATION_SUW : DialogType.EDIT_SHORTCUT_MAGNIFICATION; mDialog = AccessibilityDialogUtils.showEditShortcutDialog(getPrefContext(), From 3761f61c7e3ac1e2ad4522ff70cafadca4ad6c28 Mon Sep 17 00:00:00 2001 From: Roy Chou Date: Mon, 20 May 2024 08:16:18 +0000 Subject: [PATCH 09/18] chore(magnification suw): hide always on toggle in setup wizard Bug: 340721852 Flag: NONE Test: manually atest MagnificationAlwaysOnPreferenceControllerTest Change-Id: I39868ac1d4423584e43473683db20a2259311049 --- .../MagnificationAlwaysOnPreferenceController.java | 7 +++---- ...ToggleScreenMagnificationPreferenceFragment.java | 1 + ...gnificationAlwaysOnPreferenceControllerTest.java | 13 +++++++++++++ 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/com/android/settings/accessibility/MagnificationAlwaysOnPreferenceController.java b/src/com/android/settings/accessibility/MagnificationAlwaysOnPreferenceController.java index 4a37a416414..43d11b0148d 100644 --- a/src/com/android/settings/accessibility/MagnificationAlwaysOnPreferenceController.java +++ b/src/com/android/settings/accessibility/MagnificationAlwaysOnPreferenceController.java @@ -34,7 +34,6 @@ import androidx.preference.PreferenceScreen; import com.android.settings.R; import com.android.settings.accessibility.MagnificationCapabilities.MagnificationMode; -import com.android.settings.core.TogglePreferenceController; import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.OnPause; import com.android.settingslib.core.lifecycle.events.OnResume; @@ -44,8 +43,8 @@ import com.android.settingslib.core.lifecycle.events.OnResume; * feature, where the magnifier will not deactivate on Activity transitions; it will only zoom out * to 100%. */ -public class MagnificationAlwaysOnPreferenceController extends TogglePreferenceController - implements LifecycleObserver, OnResume, OnPause { +public class MagnificationAlwaysOnPreferenceController extends + MagnificationFeaturePreferenceController implements LifecycleObserver, OnResume, OnPause { private static final String TAG = MagnificationAlwaysOnPreferenceController.class.getSimpleName(); @@ -89,7 +88,7 @@ public class MagnificationAlwaysOnPreferenceController extends TogglePreferenceC @Override public int getAvailabilityStatus() { - return AVAILABLE; + return isInSetupWizard() ? CONDITIONALLY_UNAVAILABLE : AVAILABLE; } @Override diff --git a/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java b/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java index e3f53e47172..5c1ddbecd4c 100644 --- a/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java +++ b/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java @@ -285,6 +285,7 @@ public class ToggleScreenMagnificationPreferenceFragment extends var alwaysOnPreferenceController = new MagnificationAlwaysOnPreferenceController( getContext(), MagnificationAlwaysOnPreferenceController.PREF_KEY); + alwaysOnPreferenceController.setInSetupWizard(mInSetupWizard); getSettingsLifecycle().addObserver(alwaysOnPreferenceController); alwaysOnPreferenceController.displayPreference(getPreferenceScreen()); addPreferenceController(alwaysOnPreferenceController); diff --git a/tests/robotests/src/com/android/settings/accessibility/MagnificationAlwaysOnPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/MagnificationAlwaysOnPreferenceControllerTest.java index 3e97cec0f4a..f0decabccee 100644 --- a/tests/robotests/src/com/android/settings/accessibility/MagnificationAlwaysOnPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/accessibility/MagnificationAlwaysOnPreferenceControllerTest.java @@ -18,6 +18,8 @@ package com.android.settings.accessibility; import static com.android.settings.accessibility.AccessibilityUtil.State.OFF; import static com.android.settings.accessibility.MagnificationCapabilities.MagnificationMode; +import static com.android.settings.core.BasePreferenceController.AVAILABLE; +import static com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE; import static com.google.common.truth.Truth.assertThat; @@ -148,4 +150,15 @@ public class MagnificationAlwaysOnPreferenceControllerTest { mController.updateState(mSwitchPreference); assertThat(mSwitchPreference.isEnabled()).isTrue(); } + + @Test + public void getAvailableStatus_notInSetupWizard_returnAvailable() { + assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE); + } + + @Test + public void getAvailableStatus_inSetupWizard_returnConditionallyUnavailable() { + mController.setInSetupWizard(true); + assertThat(mController.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE); + } } From c9d3b39a6284eb4daf99d2ba47a50f6dc74370c9 Mon Sep 17 00:00:00 2001 From: Roy Chou Date: Mon, 20 May 2024 08:22:17 +0000 Subject: [PATCH 10/18] refactor(magnification suw): hide follow typing toggle in setup wizard Originally we cache mFollowingTypingSwitchPreference in fragment so in ToggleScreenMagnificationPreferenceFragmentForSetupWizard we can set the preference visible to false to hide it. After creating MagnificationFeaturePreferenceController, the MagnificationFollowTypingPreferenceController can extend it and check isInSetupWizard internally then dicide whether to hide. Therefore, we don't need to cache mFollowingTypingSwitchPreference in fragment and let the fragment control the preference visibility anymore. Bug: 340721852 Flag: NONE Test: manually atest MagnificationFollowTypingPreferenceControllerTest atest ToggleScreenMagnificationPreferenceFragmentForSetupWizardTest atest ToggleScreenMagnificationPreferenceFragmentTest Change-Id: I44f7f0589b2df3d83a27139323fc68a0561f1cfa --- ...gnificationFollowTypingPreferenceController.java | 6 +++--- ...ToggleScreenMagnificationPreferenceFragment.java | 13 ++++++------- ...gnificationPreferenceFragmentForSetupWizard.java | 4 ---- ...icationFollowTypingPreferenceControllerTest.java | 13 +++++++++++++ ...icationPreferenceFragmentForSetupWizardTest.java | 3 --- 5 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/com/android/settings/accessibility/MagnificationFollowTypingPreferenceController.java b/src/com/android/settings/accessibility/MagnificationFollowTypingPreferenceController.java index b269a9975e5..6092b4b809c 100644 --- a/src/com/android/settings/accessibility/MagnificationFollowTypingPreferenceController.java +++ b/src/com/android/settings/accessibility/MagnificationFollowTypingPreferenceController.java @@ -23,10 +23,10 @@ import android.content.Context; import android.provider.Settings; import com.android.settings.R; -import com.android.settings.core.TogglePreferenceController; /** Controller that accesses and switches the preference status of following typing feature */ -public class MagnificationFollowTypingPreferenceController extends TogglePreferenceController { +public class MagnificationFollowTypingPreferenceController extends + MagnificationFeaturePreferenceController { private static final String TAG = MagnificationFollowTypingPreferenceController.class.getSimpleName(); @@ -38,7 +38,7 @@ public class MagnificationFollowTypingPreferenceController extends TogglePrefere @Override public int getAvailabilityStatus() { - return AVAILABLE; + return isInSetupWizard() ? CONDITIONALLY_UNAVAILABLE : AVAILABLE; } @Override diff --git a/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java b/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java index 5c1ddbecd4c..c8d566d0aae 100644 --- a/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java +++ b/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java @@ -82,8 +82,6 @@ public class ToggleScreenMagnificationPreferenceFragment extends private static final TextUtils.SimpleStringSplitter sStringColonSplitter = new TextUtils.SimpleStringSplitter(COMPONENT_NAME_SEPARATOR); - protected TwoStatePreference mFollowingTypingSwitchPreference; - // TODO(b/147021230): Move duplicated functions with android/internal/accessibility into util. private TouchExplorationStateChangeListener mTouchExplorationStateChangeListener; private CheckBox mSoftwareTypeCheckBox; @@ -243,17 +241,18 @@ public class ToggleScreenMagnificationPreferenceFragment extends } private void addFollowTypingSetting(PreferenceCategory generalCategory) { - mFollowingTypingSwitchPreference = new SwitchPreferenceCompat(getPrefContext()); - mFollowingTypingSwitchPreference.setTitle( + var followingTypingSwitchPreference = new SwitchPreferenceCompat(getPrefContext()); + followingTypingSwitchPreference.setTitle( R.string.accessibility_screen_magnification_follow_typing_title); - mFollowingTypingSwitchPreference.setSummary( + followingTypingSwitchPreference.setSummary( R.string.accessibility_screen_magnification_follow_typing_summary); - mFollowingTypingSwitchPreference.setKey( + followingTypingSwitchPreference.setKey( MagnificationFollowTypingPreferenceController.PREF_KEY); - generalCategory.addPreference(mFollowingTypingSwitchPreference); + generalCategory.addPreference(followingTypingSwitchPreference); var followTypingPreferenceController = new MagnificationFollowTypingPreferenceController( getContext(), MagnificationFollowTypingPreferenceController.PREF_KEY); + followTypingPreferenceController.setInSetupWizard(mInSetupWizard); followTypingPreferenceController.displayPreference(getPreferenceScreen()); addPreferenceController(followTypingPreferenceController); } diff --git a/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentForSetupWizard.java b/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentForSetupWizard.java index 9182fbb565a..97405d24e9f 100644 --- a/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentForSetupWizard.java +++ b/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentForSetupWizard.java @@ -72,10 +72,6 @@ public class ToggleScreenMagnificationPreferenceFragmentForSetupWizard if (mSettingsPreference != null) { mSettingsPreference.setVisible(false); } - // Setting of following typing - if (mFollowingTypingSwitchPreference != null) { - mFollowingTypingSwitchPreference.setVisible(false); - } } @Override diff --git a/tests/robotests/src/com/android/settings/accessibility/MagnificationFollowTypingPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/MagnificationFollowTypingPreferenceControllerTest.java index 3aeeca8f59c..bc0563a9631 100644 --- a/tests/robotests/src/com/android/settings/accessibility/MagnificationFollowTypingPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/accessibility/MagnificationFollowTypingPreferenceControllerTest.java @@ -17,6 +17,8 @@ package com.android.settings.accessibility; import static com.android.settings.accessibility.AccessibilityUtil.State.OFF; +import static com.android.settings.core.BasePreferenceController.AVAILABLE; +import static com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE; import static com.google.common.truth.Truth.assertThat; @@ -61,6 +63,17 @@ public class MagnificationFollowTypingPreferenceControllerTest { reset(mSwitchPreference); } + @Test + public void getAvailableStatus_notInSetupWizard_returnAvailable() { + assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE); + } + + @Test + public void getAvailableStatus_inSetupWizard_returnConditionallyUnavailable() { + mController.setInSetupWizard(true); + assertThat(mController.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE); + } + @Test public void performClick_switchDefaultStateForFollowTyping_shouldReturnFalse() { mSwitchPreference.performClick(); diff --git a/tests/robotests/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentForSetupWizardTest.java b/tests/robotests/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentForSetupWizardTest.java index f26e838375d..59bae921b0d 100644 --- a/tests/robotests/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentForSetupWizardTest.java +++ b/tests/robotests/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentForSetupWizardTest.java @@ -32,7 +32,6 @@ import androidx.lifecycle.LifecycleOwner; import androidx.preference.Preference; import androidx.preference.PreferenceManager; import androidx.preference.PreferenceScreen; -import androidx.preference.SwitchPreferenceCompat; import androidx.test.core.app.ApplicationProvider; import com.android.settings.R; @@ -97,7 +96,6 @@ public class ToggleScreenMagnificationPreferenceFragmentForSetupWizardTest { verify(mFooterBarMixin).setPrimaryButton(any()); assertThat(mFragment.mTopIntroPreference.isVisible()).isFalse(); assertThat(mFragment.mSettingsPreference.isVisible()).isFalse(); - assertThat(mFragment.mFollowingTypingSwitchPreference.isVisible()).isFalse(); } @Test @@ -124,7 +122,6 @@ public class ToggleScreenMagnificationPreferenceFragmentForSetupWizardTest { mPreferenceManager.setPreferences(mPreferenceManager.createPreferenceScreen(context)); mTopIntroPreference = new TopIntroPreference(context); mSettingsPreference = new Preference(context); - mFollowingTypingSwitchPreference = new SwitchPreferenceCompat(context); } @Override From f094c19d0ac9518b74a2fa376626fef6274cf29b Mon Sep 17 00:00:00 2001 From: Roy Chou Date: Mon, 20 May 2024 08:32:04 +0000 Subject: [PATCH 11/18] chore(magnification suw): hide one finger pan toggle in setup wizard Bug: 340721852 Flag: NONE Test: manually atest MagnificationOneFingerPanningPreferenceControllerTest Change-Id: Ic9b44d73f75b47e6b97c5f4994b2e0b23bfb623b --- ...icationOneFingerPanningPreferenceController.java | 7 +++---- ...ToggleScreenMagnificationPreferenceFragment.java | 1 + ...ionOneFingerPanningPreferenceControllerTest.java | 13 +++++++++++++ 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/com/android/settings/accessibility/MagnificationOneFingerPanningPreferenceController.java b/src/com/android/settings/accessibility/MagnificationOneFingerPanningPreferenceController.java index 4eb5090dce6..3e3b8d907df 100644 --- a/src/com/android/settings/accessibility/MagnificationOneFingerPanningPreferenceController.java +++ b/src/com/android/settings/accessibility/MagnificationOneFingerPanningPreferenceController.java @@ -35,13 +35,12 @@ import androidx.preference.TwoStatePreference; import com.android.settings.R; import com.android.settings.accessibility.MagnificationCapabilities.MagnificationMode; -import com.android.settings.core.TogglePreferenceController; import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.OnPause; import com.android.settingslib.core.lifecycle.events.OnResume; -public class MagnificationOneFingerPanningPreferenceController - extends TogglePreferenceController implements LifecycleObserver, OnResume, OnPause { +public class MagnificationOneFingerPanningPreferenceController extends + MagnificationFeaturePreferenceController implements LifecycleObserver, OnResume, OnPause { static final String PREF_KEY = Settings.Secure.ACCESSIBILITY_SINGLE_FINGER_PANNING_ENABLED; private TwoStatePreference mSwitchPreference; @@ -82,7 +81,7 @@ public class MagnificationOneFingerPanningPreferenceController @Override public int getAvailabilityStatus() { - return AVAILABLE; + return isInSetupWizard() ? CONDITIONALLY_UNAVAILABLE : AVAILABLE; } @Override diff --git a/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java b/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java index c8d566d0aae..a76dcf1bbb8 100644 --- a/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java +++ b/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java @@ -304,6 +304,7 @@ public class ToggleScreenMagnificationPreferenceFragment extends var oneFingerPanningPreferenceController = new MagnificationOneFingerPanningPreferenceController(getContext()); + oneFingerPanningPreferenceController.setInSetupWizard(mInSetupWizard); getSettingsLifecycle().addObserver(oneFingerPanningPreferenceController); oneFingerPanningPreferenceController.displayPreference(getPreferenceScreen()); addPreferenceController(oneFingerPanningPreferenceController); diff --git a/tests/robotests/src/com/android/settings/accessibility/MagnificationOneFingerPanningPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/MagnificationOneFingerPanningPreferenceControllerTest.java index bfc8313d142..8b4bcd8fe47 100644 --- a/tests/robotests/src/com/android/settings/accessibility/MagnificationOneFingerPanningPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/accessibility/MagnificationOneFingerPanningPreferenceControllerTest.java @@ -19,6 +19,8 @@ package com.android.settings.accessibility; import static com.android.settings.accessibility.AccessibilityUtil.State.OFF; import static com.android.settings.accessibility.AccessibilityUtil.State.ON; import static com.android.settings.accessibility.MagnificationCapabilities.MagnificationMode; +import static com.android.settings.core.BasePreferenceController.AVAILABLE; +import static com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE; import static com.google.common.truth.Truth.assertThat; @@ -186,6 +188,17 @@ public class MagnificationOneFingerPanningPreferenceControllerTest { assertThat(mSwitchPreference.isChecked()).isFalse(); } + @Test + public void getAvailableStatus_notInSetupWizard_returnAvailable() { + assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE); + } + + @Test + public void getAvailableStatus_inSetupWizard_returnConditionallyUnavailable() { + mController.setInSetupWizard(true); + assertThat(mController.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE); + } + private String enabledSummary() { return mContext.getString( R.string.accessibility_magnification_one_finger_panning_summary_on); From a511ddb75ac0eb5b41f67fb991a99d2331818325 Mon Sep 17 00:00:00 2001 From: Roy Chou Date: Mon, 20 May 2024 08:43:48 +0000 Subject: [PATCH 12/18] chore(magnification suw): hide joystick toggle in setup wizard Although we haven't implemented joystick feature, but the joystick preference controller is already in the codebase, so we also add hiding logic for joystick toggle when in setup wizard. Bug: 340721852 Flag: NONE Test: manually atest MagnificationJoystickPreferenceControllerTest Change-Id: Ife93548583c3e82eac030e6e3aa55b9f643b055a --- .../MagnificationJoystickPreferenceController.java | 6 +++--- ...ToggleScreenMagnificationPreferenceFragment.java | 1 + ...gnificationJoystickPreferenceControllerTest.java | 13 +++++++++++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/com/android/settings/accessibility/MagnificationJoystickPreferenceController.java b/src/com/android/settings/accessibility/MagnificationJoystickPreferenceController.java index b480a0a3f2f..690a9453c29 100644 --- a/src/com/android/settings/accessibility/MagnificationJoystickPreferenceController.java +++ b/src/com/android/settings/accessibility/MagnificationJoystickPreferenceController.java @@ -23,12 +23,12 @@ import android.content.Context; import android.provider.Settings; import com.android.settings.R; -import com.android.settings.core.TogglePreferenceController; /** * Controller that accesses and switches the preference status of the magnification joystick feature */ -public class MagnificationJoystickPreferenceController extends TogglePreferenceController { +public class MagnificationJoystickPreferenceController extends + MagnificationFeaturePreferenceController { private static final String TAG = MagnificationJoystickPreferenceController.class.getSimpleName(); @@ -40,7 +40,7 @@ public class MagnificationJoystickPreferenceController extends TogglePreferenceC @Override public int getAvailabilityStatus() { - return AVAILABLE; + return isInSetupWizard() ? CONDITIONALLY_UNAVAILABLE : AVAILABLE; } @Override diff --git a/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java b/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java index a76dcf1bbb8..0821b87c2f0 100644 --- a/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java +++ b/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java @@ -333,6 +333,7 @@ public class ToggleScreenMagnificationPreferenceFragment extends getContext(), MagnificationJoystickPreferenceController.PREF_KEY ); + joystickPreferenceController.setInSetupWizard(mInSetupWizard); joystickPreferenceController.displayPreference(getPreferenceScreen()); addPreferenceController(joystickPreferenceController); } diff --git a/tests/robotests/src/com/android/settings/accessibility/MagnificationJoystickPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/MagnificationJoystickPreferenceControllerTest.java index 11258bb6041..3fe3ed6d6d1 100644 --- a/tests/robotests/src/com/android/settings/accessibility/MagnificationJoystickPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/accessibility/MagnificationJoystickPreferenceControllerTest.java @@ -17,6 +17,8 @@ package com.android.settings.accessibility; import static com.android.settings.accessibility.AccessibilityUtil.State.OFF; +import static com.android.settings.core.BasePreferenceController.AVAILABLE; +import static com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE; import static com.google.common.truth.Truth.assertThat; @@ -80,4 +82,15 @@ public class MagnificationJoystickPreferenceControllerTest { assertThat(mController.isChecked()).isFalse(); assertThat(mSwitchPreference.isChecked()).isFalse(); } + + @Test + public void getAvailableStatus_notInSetupWizard_returnAvailable() { + assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE); + } + + @Test + public void getAvailableStatus_inSetupWizard_returnConditionallyUnavailable() { + mController.setInSetupWizard(true); + assertThat(mController.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE); + } } From 83aeb45f3c6c79187698db331a3f5e34eb10bd4a Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Thu, 16 May 2024 17:15:37 +0800 Subject: [PATCH 13/18] Fix default mode for AlarmsAndRemindersAppList When app ops permission's app ops mode is default, should check if permission is grant. Fix: 339846642 Test: manual - on AlarmsAndRemindersAppList Test: unit test Change-Id: Ia0f05211f5774637304502ead79dd98a1cf89886 --- .../AlarmsAndRemindersAppList.kt | 9 ++-- .../spa/app/WifiControlAppListModelTest.kt | 35 +++++++------- .../specialaccess/MediaRoutingControlTest.kt | 47 +++++++++---------- 3 files changed, 42 insertions(+), 49 deletions(-) diff --git a/src/com/android/settings/spa/app/specialaccess/AlarmsAndRemindersAppList.kt b/src/com/android/settings/spa/app/specialaccess/AlarmsAndRemindersAppList.kt index b15675ed14f..a411c90ea02 100644 --- a/src/com/android/settings/spa/app/specialaccess/AlarmsAndRemindersAppList.kt +++ b/src/com/android/settings/spa/app/specialaccess/AlarmsAndRemindersAppList.kt @@ -29,7 +29,7 @@ import com.android.settings.overlay.FeatureFactory.Companion.featureFactory import com.android.settingslib.R import com.android.settingslib.spa.lifecycle.collectAsCallbackWithLifecycle import com.android.settingslib.spaprivileged.model.app.AppOps -import com.android.settingslib.spaprivileged.model.app.AppOpsController +import com.android.settingslib.spaprivileged.model.app.AppOpsPermissionController import com.android.settingslib.spaprivileged.model.app.AppRecord import com.android.settingslib.spaprivileged.model.app.IPackageManagers import com.android.settingslib.spaprivileged.model.app.PackageManagers @@ -49,7 +49,7 @@ data class AlarmsAndRemindersAppRecord( override val app: ApplicationInfo, val isTrumped: Boolean, val isChangeable: Boolean, - var controller: AppOpsController, + val controller: AppOpsPermissionController, ) : AppRecord class AlarmsAndRemindersAppListModel( @@ -84,7 +84,7 @@ class AlarmsAndRemindersAppListModel( @Composable override fun isAllowed(record: AlarmsAndRemindersAppRecord): () -> Boolean? = when { record.isTrumped -> ({ true }) - else -> record.controller.isAllowed.collectAsCallbackWithLifecycle() + else -> record.controller.isAllowedFlow.collectAsCallbackWithLifecycle() } override fun isChangeable(record: AlarmsAndRemindersAppRecord) = record.isChangeable @@ -114,10 +114,11 @@ class AlarmsAndRemindersAppListModel( app = app, isTrumped = isTrumped, isChangeable = hasRequestPermission && !isTrumped, - controller = AppOpsController( + controller = AppOpsPermissionController( context = context, app = app, appOps = APP_OPS, + permission = PERMISSION, ), ) } diff --git a/tests/spa_unit/src/com/android/settings/spa/app/WifiControlAppListModelTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/WifiControlAppListModelTest.kt index 863a6e2e97b..7e5f830d644 100644 --- a/tests/spa_unit/src/com/android/settings/spa/app/WifiControlAppListModelTest.kt +++ b/tests/spa_unit/src/com/android/settings/spa/app/WifiControlAppListModelTest.kt @@ -17,14 +17,13 @@ package com.android.settings.spa.app.specialaccess import android.Manifest -import android.app.AppOpsManager import android.content.Context import android.content.pm.ApplicationInfo import androidx.compose.ui.test.junit4.createComposeRule import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull -import com.android.settingslib.spaprivileged.model.app.IAppOpsController +import com.android.settingslib.spaprivileged.model.app.IAppOpsPermissionController import com.android.settingslib.spaprivileged.model.app.IPackageManagers import com.android.settingslib.spaprivileged.template.app.AppOpPermissionRecord import com.google.common.truth.Truth.assertThat @@ -117,14 +116,14 @@ class WifiControlAppListModelTest { app = APP_NOT_REQUEST_PERMISSION, hasRequestPermission = false, hasRequestBroaderPermission = false, - appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT), + appOpsPermissionController = FakeAppOpsPermissionController(false), ) val appRequestedNetworkSettingsRecord = AppOpPermissionRecord( app = APP_REQUESTED_NETWORK_SETTINGS, hasRequestPermission = true, hasRequestBroaderPermission = false, - appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT) + appOpsPermissionController = FakeAppOpsPermissionController(false), ) val recordListFlow = @@ -144,7 +143,7 @@ class WifiControlAppListModelTest { app = APP, hasRequestPermission = false, hasRequestBroaderPermission = true, - appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT), + appOpsPermissionController = FakeAppOpsPermissionController(false), ) val isAllowed = getIsAllowed(record) @@ -159,7 +158,7 @@ class WifiControlAppListModelTest { app = APP, hasRequestPermission = true, hasRequestBroaderPermission = false, - appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_ALLOWED), + appOpsPermissionController = FakeAppOpsPermissionController(true), ) val isAllowed = getIsAllowed(record) @@ -174,7 +173,7 @@ class WifiControlAppListModelTest { app = APP, hasRequestPermission = true, hasRequestBroaderPermission = false, - appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_IGNORED), + appOpsPermissionController = FakeAppOpsPermissionController(false), ) val isAllowed = getIsAllowed(record) @@ -189,7 +188,7 @@ class WifiControlAppListModelTest { app = APP, hasRequestPermission = false, hasRequestBroaderPermission = false, - appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT), + appOpsPermissionController = FakeAppOpsPermissionController(false), ) val isChangeable = listModel.isChangeable(record) @@ -198,13 +197,13 @@ class WifiControlAppListModelTest { } @Test - fun isChangeable_notChangableWhenRequestedNetworkSettingPermissions() { + fun isChangeable_notChangeableWhenRequestedNetworkSettingPermissions() { val record = AppOpPermissionRecord( app = APP, hasRequestPermission = false, hasRequestBroaderPermission = true, - appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT), + appOpsPermissionController = FakeAppOpsPermissionController(false), ) val isChangeable = listModel.isChangeable(record) @@ -213,13 +212,13 @@ class WifiControlAppListModelTest { } @Test - fun isChangeable_changableWhenRequestedChangeWifiStatePermission() { + fun isChangeable_changeableWhenRequestedChangeWifiStatePermission() { val record = AppOpPermissionRecord( app = APP, hasRequestPermission = true, hasRequestBroaderPermission = false, - appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT), + appOpsPermissionController = FakeAppOpsPermissionController(false), ) val isChangeable = listModel.isChangeable(record) @@ -229,18 +228,18 @@ class WifiControlAppListModelTest { @Test fun setAllowed_shouldCallController() { - val appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT) + val appOpsPermissionController = FakeAppOpsPermissionController(false) val record = AppOpPermissionRecord( app = APP, hasRequestPermission = true, hasRequestBroaderPermission = false, - appOpsController = appOpsController, + appOpsPermissionController = appOpsPermissionController, ) listModel.setAllowed(record = record, newAllowed = true) - assertThat(appOpsController.setAllowedCalledWith).isTrue() + assertThat(appOpsPermissionController.setAllowedCalledWith).isTrue() } private fun getIsAllowed(record: AppOpPermissionRecord): Boolean? { @@ -266,14 +265,12 @@ class WifiControlAppListModelTest { } } -private class FakeAppOpsController(private val fakeMode: Int) : IAppOpsController { +private class FakeAppOpsPermissionController(allowed: Boolean) : IAppOpsPermissionController { var setAllowedCalledWith: Boolean? = null - override val modeFlow = flowOf(fakeMode) + override val isAllowedFlow = flowOf(allowed) override fun setAllowed(allowed: Boolean) { setAllowedCalledWith = allowed } - - override fun getMode() = fakeMode } diff --git a/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/MediaRoutingControlTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/MediaRoutingControlTest.kt index ec070fdde05..a5603931568 100644 --- a/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/MediaRoutingControlTest.kt +++ b/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/MediaRoutingControlTest.kt @@ -30,10 +30,10 @@ import com.android.media.flags.Flags import com.android.settings.R import com.android.settings.testutils.FakeFeatureFactory import com.android.settingslib.spaprivileged.model.app.AppOps -import com.android.settingslib.spaprivileged.model.app.IAppOpsController +import com.android.settingslib.spaprivileged.model.app.IAppOpsPermissionController import com.android.settingslib.spaprivileged.template.app.AppOpPermissionRecord import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf import org.junit.Before import org.junit.Rule import org.junit.Test @@ -50,7 +50,7 @@ class MediaRoutingControlTest { @get:Rule val mockito: MockitoRule = MockitoJUnit.rule() - @get:Rule val setFlagsRule: SetFlagsRule = SetFlagsRule(); + @get:Rule val setFlagsRule: SetFlagsRule = SetFlagsRule() @Spy private val context: Context = ApplicationProvider.getApplicationContext() @@ -86,29 +86,29 @@ class MediaRoutingControlTest { @Test fun setAllowed_callWithNewStatusAsTrue_shouldChangeAppControllerModeToAllowed() { - val fakeAppOpController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT) + val fakeAppOpsPermissionController = FakeAppOpsPermissionController(false) val permissionRequestedRecord = AppOpPermissionRecord( app = ApplicationInfo().apply { packageName = PACKAGE_NAME }, hasRequestPermission = true, hasRequestBroaderPermission = false, - appOpsController = fakeAppOpController, + appOpsPermissionController = fakeAppOpsPermissionController, ) listModel.setAllowed(permissionRequestedRecord, true) - assertThat(fakeAppOpController.getMode()).isEqualTo(AppOpsManager.MODE_ALLOWED) + assertThat(fakeAppOpsPermissionController.setAllowedCalledWith).isTrue() } @Test fun setAllowed_callWithNewStatusAsTrue_shouldLogPermissionToggleActionAsAllowed() { - val fakeAppOpController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT) + val fakeAppOpsPermissionController = FakeAppOpsPermissionController(false) val permissionRequestedRecord = AppOpPermissionRecord( app = ApplicationInfo().apply { packageName = PACKAGE_NAME }, hasRequestPermission = true, hasRequestBroaderPermission = false, - appOpsController = fakeAppOpController, + appOpsPermissionController = fakeAppOpsPermissionController, ) listModel.setAllowed(permissionRequestedRecord, true) @@ -119,29 +119,29 @@ class MediaRoutingControlTest { @Test fun setAllowed_callWithNewStatusAsFalse_shouldChangeAppControllerModeToErrored() { - val fakeAppOpController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT) + val fakeAppOpsPermissionController = FakeAppOpsPermissionController(false) val permissionRequestedRecord = AppOpPermissionRecord( app = ApplicationInfo().apply { packageName = PACKAGE_NAME }, hasRequestPermission = true, hasRequestBroaderPermission = false, - appOpsController = fakeAppOpController, + appOpsPermissionController = fakeAppOpsPermissionController, ) listModel.setAllowed(permissionRequestedRecord, false) - assertThat(fakeAppOpController.getMode()).isEqualTo(AppOpsManager.MODE_ERRORED) + assertThat(fakeAppOpsPermissionController.setAllowedCalledWith).isFalse() } @Test fun setAllowed_callWithNewStatusAsFalse_shouldLogPermissionToggleActionAsDenied() { - val fakeAppOpController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT) + val fakeAppOpsPermissionController = FakeAppOpsPermissionController(false) val permissionRequestedRecord = AppOpPermissionRecord( app = ApplicationInfo().apply { packageName = PACKAGE_NAME }, hasRequestPermission = true, hasRequestBroaderPermission = false, - appOpsController = fakeAppOpController, + appOpsPermissionController = fakeAppOpsPermissionController, ) listModel.setAllowed(permissionRequestedRecord, false) @@ -158,8 +158,7 @@ class MediaRoutingControlTest { app = ApplicationInfo().apply { packageName = PACKAGE_NAME }, hasRequestPermission = true, hasRequestBroaderPermission = false, - appOpsController = - FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT), + appOpsPermissionController = FakeAppOpsPermissionController(false), ) whenever(mockRoleManager.getRoleHolders(AssociationRequest.DEVICE_PROFILE_WATCH)) .thenReturn(listOf(PACKAGE_NAME)) @@ -177,8 +176,7 @@ class MediaRoutingControlTest { app = ApplicationInfo().apply { packageName = PACKAGE_NAME }, hasRequestPermission = false, hasRequestBroaderPermission = false, - appOpsController = - FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT), + appOpsPermissionController = FakeAppOpsPermissionController(false), ) whenever(mockRoleManager.getRoleHolders(AssociationRequest.DEVICE_PROFILE_WATCH)) .thenReturn(listOf(PACKAGE_NAME)) @@ -196,8 +194,7 @@ class MediaRoutingControlTest { app = ApplicationInfo().apply { packageName = PACKAGE_NAME }, hasRequestPermission = true, hasRequestBroaderPermission = false, - appOpsController = - FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT), + appOpsPermissionController = FakeAppOpsPermissionController(false), ) whenever(mockRoleManager.getRoleHolders(AssociationRequest.DEVICE_PROFILE_WATCH)) .thenReturn(listOf("other.package.name")) @@ -215,8 +212,7 @@ class MediaRoutingControlTest { app = ApplicationInfo().apply { packageName = PACKAGE_NAME }, hasRequestPermission = true, hasRequestBroaderPermission = false, - appOpsController = - FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT), + appOpsPermissionController = FakeAppOpsPermissionController(false), ) whenever(mockRoleManager.getRoleHolders(AssociationRequest.DEVICE_PROFILE_WATCH)) .thenReturn(listOf(PACKAGE_NAME)) @@ -226,15 +222,14 @@ class MediaRoutingControlTest { assertThat(isSpecialAccessChangeable).isFalse() } - private class FakeAppOpsController(fakeMode: Int) : IAppOpsController { + private class FakeAppOpsPermissionController(allowed: Boolean) : IAppOpsPermissionController { + var setAllowedCalledWith: Boolean? = null - override val modeFlow = MutableStateFlow(fakeMode) + override val isAllowedFlow = flowOf(allowed) override fun setAllowed(allowed: Boolean) { - modeFlow.value = if (allowed) AppOpsManager.MODE_ALLOWED else AppOpsManager.MODE_ERRORED + setAllowedCalledWith = allowed } - - override fun getMode(): Int = modeFlow.value } companion object { From 431c14fa1828cbe410c74ee9548f2d7e90dedafd Mon Sep 17 00:00:00 2001 From: mxyyiyi Date: Tue, 21 May 2024 11:41:58 +0800 Subject: [PATCH 14/18] Update view alpha to improve text color constract in Battery usage - For light themes, first set the text color of title/summary/widget_summary to be consistent, then apply alpha = 0.65 [Before] https://screenshot.googleplex.com/4nVMMFBK7odhQju A11y Scanner result: https://screenshot.googleplex.com/4nVMMFBK7odhQju [After] https://screenshot.googleplex.com/B4r79v89HnWr4tw A11y Scanner result: https://screenshot.googleplex.com/3zQfRMe4HToc8Xm Dark mode: [before] https://screenshot.googleplex.com/BfLxR6tqQRgP4mQ [after] https://screenshot.googleplex.com/8KeG62i7R5hW8Ks Bug: 317139415 Bug: 265364351 Test: Accessibility Scanner Change-Id: I65a71525bbce41c37ee364f3f73c0d2d1d9ace38 --- .../fuelgauge/batteryusage/PowerGaugePreference.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/com/android/settings/fuelgauge/batteryusage/PowerGaugePreference.java b/src/com/android/settings/fuelgauge/batteryusage/PowerGaugePreference.java index ba2f89e3b48..1fc9abd4c5e 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/PowerGaugePreference.java +++ b/src/com/android/settings/fuelgauge/batteryusage/PowerGaugePreference.java @@ -44,6 +44,8 @@ public class PowerGaugePreference extends AppPreference { private static final float UNSELECTABLE_ALPHA_LIGHT_MODE = 0.65f; private static final float UNSELECTABLE_ALPHA_DARK_MODE = 0.65f; + private final int mTitleColorNormal; + private BatteryEntry mInfo; private BatteryDiffEntry mBatteryDiffEntry; private CharSequence mContentDescription; @@ -78,6 +80,8 @@ public class PowerGaugePreference extends AppPreference { mInfo = info; mContentDescription = contentDescription; mShowAnomalyIcon = false; + mTitleColorNormal = + Utils.getColorAttrDefaultColor(context, android.R.attr.textColorPrimary); } /** Sets the content description. */ @@ -155,6 +159,13 @@ public class PowerGaugePreference extends AppPreference { final TextView titleView = (TextView) view.findViewById(android.R.id.title); titleView.setContentDescription(mContentDescription); } + + if (!isSelectable()) { + // Set colors consistently to meet contrast requirements for non-selectable items + ((TextView) view.findViewById(android.R.id.title)).setTextColor(mTitleColorNormal); + ((TextView) view.findViewById(android.R.id.summary)).setTextColor(mTitleColorNormal); + subtitle.setTextColor(mTitleColorNormal); + } } private static void setViewAlpha(View view, float alpha) { From 92d77ea26bf411c81ea435a8edd96da2e78f897e Mon Sep 17 00:00:00 2001 From: songferngwang Date: Tue, 21 May 2024 03:53:02 +0000 Subject: [PATCH 15/18] Fix UiccSlotUtilTest's test case The onReceiveSimCardStateChangeReceiver_receiveAction_timerCountDown is failed since the action is wrong. Bug: 337417975 Test: atest UiccSlotUtilTest (pass) Change-Id: I0cb12432553155d69f985693e4ef911455c81652 --- .../src/com/android/settings/network/UiccSlotUtilTest.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/unit/src/com/android/settings/network/UiccSlotUtilTest.java b/tests/unit/src/com/android/settings/network/UiccSlotUtilTest.java index 1a895b3d0fd..2aa15733e45 100644 --- a/tests/unit/src/com/android/settings/network/UiccSlotUtilTest.java +++ b/tests/unit/src/com/android/settings/network/UiccSlotUtilTest.java @@ -41,7 +41,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.common.collect.ImmutableList; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -754,12 +753,11 @@ public class UiccSlotUtilTest { } @Test - @Ignore("b/337417975") public void onReceiveSimCardStateChangeReceiver_receiveAction_timerCountDown() { CountDownLatch latch = spy(new CountDownLatch(1)); UiccSlotUtil.SimCardStateChangeReceiver receive = new UiccSlotUtil.SimCardStateChangeReceiver(latch); - Intent intent = new Intent(TelephonyManager.ACTION_SIM_SLOT_STATUS_CHANGED); + Intent intent = new Intent(TelephonyManager.ACTION_SIM_CARD_STATE_CHANGED); intent.putExtra(TelephonyManager.EXTRA_SIM_STATE, TelephonyManager.SIM_STATE_PRESENT); receive.onReceive(mContext, intent); From 997438d5379c83a19024add88ec1824f75c95e79 Mon Sep 17 00:00:00 2001 From: chelseahao Date: Tue, 21 May 2024 12:30:20 +0800 Subject: [PATCH 16/18] [Audiosharing] Create dialog for unsupported device. Test: atest -c com.android.settings.connecteddevice.audiosharing.audiostreams Bug: 308368124 Change-Id: Ibe421d29aa1ee05ee736fc8a2665794984a8b6e1 --- res-product/values/strings.xml | 4 ++++ .../AudioStreamConfirmDialog.java | 21 ++++++++++++++++--- .../AudioStreamConfirmDialogActivity.java | 4 ---- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/res-product/values/strings.xml b/res-product/values/strings.xml index a42153eca7f..a6db4d9f1dd 100644 --- a/res-product/values/strings.xml +++ b/res-product/values/strings.xml @@ -758,4 +758,8 @@ To listen to an audio stream, first connect headphones that support LE Audio to this phone. To listen to an audio stream, first connect headphones that support LE Audio to this tablet. To listen to an audio stream, first connect headphones that support LE Audio to this device. + + This phone doesn\'t support LE Audio, which is needed to listen to audio streams. + This tablet doesn\'t support LE Audio, which is needed to listen to audio streams. + This device doesn\'t support LE Audio, which is needed to listen to audio streams. diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialog.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialog.java index 8e2795811ce..f29405dd698 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialog.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialog.java @@ -54,9 +54,6 @@ public class AudioStreamConfirmDialog extends InstrumentedDialogFragment { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - if (!AudioSharingUtils.isFeatureEnabled()) { - return; - } setShowsDialog(true); mActivity = getActivity(); if (mActivity == null) { @@ -84,6 +81,9 @@ public class AudioStreamConfirmDialog extends InstrumentedDialogFragment { @Override public Dialog onCreateDialog(Bundle savedInstanceState) { + if (!AudioSharingUtils.isFeatureEnabled()) { + return getUnsupporteDialog(); + } if (AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) { CachedBluetoothDevice connectedLeDevice = AudioStreamsHelper.getCachedBluetoothDeviceInSharingOrLeConnected( @@ -137,6 +137,21 @@ public class AudioStreamConfirmDialog extends InstrumentedDialogFragment { .build(); } + private Dialog getUnsupporteDialog() { + return new AudioStreamsDialogFragment.DialogBuilder(getActivity()) + .setTitle(getString(R.string.audio_streams_dialog_cannot_listen)) + .setSubTitle2(getString(R.string.audio_streams_dialog_unsupported_device_subtitle)) + .setRightButtonText(getString(R.string.audio_streams_dialog_close)) + .setRightButtonOnClickListener( + unused -> { + dismiss(); + if (mActivity != null) { + mActivity.finish(); + } + }) + .build(); + } + private Dialog getErrorDialog(String name) { return new AudioStreamsDialogFragment.DialogBuilder(getActivity()) .setTitle(getString(R.string.audio_streams_dialog_cannot_listen)) diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialogActivity.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialogActivity.java index 695ad939d8c..ddb0b425d71 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialogActivity.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialogActivity.java @@ -19,16 +19,12 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams; import android.os.Bundle; import com.android.settings.SettingsActivity; -import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils; public class AudioStreamConfirmDialogActivity extends SettingsActivity { @Override protected void onCreate(Bundle savedState) { super.onCreate(savedState); - if (!AudioSharingUtils.isFeatureEnabled()) { - finish(); - } } @Override From 6d5dadfb0d82ab8d06d8bba81909e57e3ac02d6b Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Tue, 21 May 2024 13:59:27 +0800 Subject: [PATCH 17/18] Fix BackupTasksAppsTest Fix: 341832628 Test: atest BackupTasksAppsTest Change-Id: I8731604d896d58a9b46b4d0af660a40c34bb905b Merged-In: Ib01bd6556768a943ae7fae013c6ccb82b1b96697 --- .../spa/app/specialaccess/BackupTasksAppsTest.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/BackupTasksAppsTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/BackupTasksAppsTest.kt index d68f0515841..33e4b53ab22 100644 --- a/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/BackupTasksAppsTest.kt +++ b/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/BackupTasksAppsTest.kt @@ -23,6 +23,7 @@ import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import com.android.settings.R +import com.android.settingslib.spaprivileged.model.app.AppOps import org.junit.Test import org.junit.runner.RunWith @@ -37,8 +38,12 @@ class BackupTasksAppsTest { assertThat(listModel.pageTitleResId).isEqualTo(R.string.run_backup_tasks_title) assertThat(listModel.switchTitleResId).isEqualTo(R.string.run_backup_tasks_switch_title) assertThat(listModel.footerResId).isEqualTo(R.string.run_backup_tasks_footer_title) - assertThat(listModel.appOp).isEqualTo(AppOpsManager.OP_RUN_BACKUP_JOBS) + assertThat(listModel.appOps).isEqualTo( + AppOps( + op = AppOpsManager.OP_RUN_BACKUP_JOBS, + setModeByUid = true, + ) + ) assertThat(listModel.permission).isEqualTo(Manifest.permission.RUN_BACKUP_JOBS) - assertThat(listModel.setModeByUid).isTrue() } } \ No newline at end of file From 3f2bcf973a1969716aeaeb9a0ee1053cc98b3ec9 Mon Sep 17 00:00:00 2001 From: Julia Reynolds Date: Mon, 20 May 2024 16:29:44 -0400 Subject: [PATCH 18/18] Migrate alarms and other interruptions to modes_ui Also add missing tests for repeat callers Flag: android.app.modes_ui Test: ZenModesSummaryHelperTest Test: ZenModeOtherPreferenceControllerTest Test: ZenModeOtherLinkPreferenceControllerTest Bug: 337086854 Change-Id: Ie0a849de61c32b5f9feacae9a1bac457f9dd526c --- res/xml/modes_other_settings.xml | 49 ++++ res/xml/modes_rule_settings.xml | 4 + .../notification/modes/ZenModeFragment.java | 2 + .../modes/ZenModeOtherFragment.java | 64 ++++ .../ZenModeOtherLinkPreferenceController.java | 52 ++++ .../ZenModeOtherPreferenceController.java | 74 +++++ ...ModeOtherLinkPreferenceControllerTest.java | 77 +++++ .../ZenModeOtherPreferenceControllerTest.java | 277 ++++++++++++++++++ ...RepeatCallersPreferenceControllerTest.java | 133 +++++++++ .../modes/ZenModesSummaryHelperTest.java | 80 +++++ 10 files changed, 812 insertions(+) create mode 100644 res/xml/modes_other_settings.xml create mode 100644 src/com/android/settings/notification/modes/ZenModeOtherFragment.java create mode 100644 src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceController.java create mode 100644 src/com/android/settings/notification/modes/ZenModeOtherPreferenceController.java create mode 100644 tests/robotests/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceControllerTest.java create mode 100644 tests/robotests/src/com/android/settings/notification/modes/ZenModeOtherPreferenceControllerTest.java create mode 100644 tests/robotests/src/com/android/settings/notification/modes/ZenModeRepeatCallersPreferenceControllerTest.java diff --git a/res/xml/modes_other_settings.xml b/res/xml/modes_other_settings.xml new file mode 100644 index 00000000000..2dc2c7e0f61 --- /dev/null +++ b/res/xml/modes_other_settings.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/res/xml/modes_rule_settings.xml b/res/xml/modes_rule_settings.xml index 12a683e6bec..7a08a68d8c0 100644 --- a/res/xml/modes_rule_settings.xml +++ b/res/xml/modes_rule_settings.xml @@ -26,4 +26,8 @@ android:key="zen_mode_people" android:title="@string/zen_category_people"/> + + \ 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 1626d91bd85..51772f00a45 100644 --- a/src/com/android/settings/notification/modes/ZenModeFragment.java +++ b/src/com/android/settings/notification/modes/ZenModeFragment.java @@ -41,6 +41,8 @@ public class ZenModeFragment extends ZenModeFragmentBase { prefControllers.add(new ZenModeHeaderController(context, "header", this, mBackend)); prefControllers.add(new ZenModePeopleLinkPreferenceController( context, "zen_mode_people", mBackend)); + prefControllers.add(new ZenModeOtherLinkPreferenceController( + context, "zen_other_settings", mBackend)); return prefControllers; } diff --git a/src/com/android/settings/notification/modes/ZenModeOtherFragment.java b/src/com/android/settings/notification/modes/ZenModeOtherFragment.java new file mode 100644 index 00000000000..1149cd1312f --- /dev/null +++ b/src/com/android/settings/notification/modes/ZenModeOtherFragment.java @@ -0,0 +1,64 @@ +/* + * 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.service.notification.ZenPolicy.PRIORITY_CATEGORY_ALARMS; +import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_EVENTS; +import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_MEDIA; +import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_REMINDERS; +import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_SYSTEM; + +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; + +/** + * Mode > Alarms & Other Interruptions + */ +public class ZenModeOtherFragment extends ZenModeFragmentBase { + + @Override + protected List createPreferenceControllers(Context context) { + List controllers = new ArrayList<>(); + controllers.add(new ZenModeOtherPreferenceController( + context, "modes_category_alarm", mBackend)); + controllers.add(new ZenModeOtherPreferenceController( + context, "modes_category_media", mBackend)); + controllers.add(new ZenModeOtherPreferenceController( + context, "modes_category_system", mBackend)); + controllers.add(new ZenModeOtherPreferenceController( + context, "modes_category_reminders", mBackend)); + controllers.add(new ZenModeOtherPreferenceController( + context, "modes_category_events", mBackend)); + return controllers; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.modes_other_settings; + } + + @Override + public int getMetricsCategory() { + // TODO: b/332937635 - make this the correct metrics category + return SettingsEnums.NOTIFICATION_ZEN_MODE_PRIORITY; + } +} diff --git a/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceController.java new file mode 100644 index 00000000000..a43f8b056e1 --- /dev/null +++ b/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceController.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 static com.android.settings.notification.modes.ZenModeFragmentBase.MODE_ID; + +import android.content.Context; +import android.os.Bundle; +import androidx.preference.Preference; +import com.android.settings.core.SubSettingLauncher; + +/** + * Preference with a link and summary about what other sounds can break through the mode + */ +public class ZenModeOtherLinkPreferenceController extends AbstractZenModePreferenceController { + + ZenModeSummaryHelper mSummaryHelper; + + public ZenModeOtherLinkPreferenceController(Context context, String key, + ZenModesBackend backend) { + super(context, key, backend); + mSummaryHelper = new ZenModeSummaryHelper(mContext, mBackend); + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + Bundle bundle = new Bundle(); + bundle.putString(MODE_ID, getMode().getId()); + preference.setIntent(new SubSettingLauncher(mContext) + .setDestination(ZenModeOtherFragment.class.getName()) + .setSourceMetricsCategory(0) + .setArguments(bundle) + .toIntent()); + preference.setSummary(mSummaryHelper.getOtherSoundCategoriesSummary(getMode())); + } +} diff --git a/src/com/android/settings/notification/modes/ZenModeOtherPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeOtherPreferenceController.java new file mode 100644 index 00000000000..e31fa0fa08b --- /dev/null +++ b/src/com/android/settings/notification/modes/ZenModeOtherPreferenceController.java @@ -0,0 +1,74 @@ +/* + * 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.service.notification.ZenPolicy.PRIORITY_CATEGORY_ALARMS; +import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_EVENTS; +import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_MEDIA; +import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_REMINDERS; +import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_SYSTEM; + +import android.content.Context; +import android.service.notification.ZenPolicy; +import androidx.preference.Preference; +import androidx.preference.TwoStatePreference; + +public class ZenModeOtherPreferenceController extends AbstractZenModePreferenceController + implements Preference.OnPreferenceChangeListener { + + public ZenModeOtherPreferenceController(Context context, String key, + ZenModesBackend backend) { + super(context, key, backend); + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + + TwoStatePreference pref = (TwoStatePreference) preference; + pref.setChecked(getMode().getPolicy().isCategoryAllowed(getCategory(), true)); + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + final boolean allow = (Boolean) newValue; + + ZenPolicy diffPolicy = new ZenPolicy.Builder() + .allowCategory(getCategory(), allow) + .build(); + getMode().setPolicy(diffPolicy); + mBackend.updateMode(getMode()); + + return true; + } + + private int getCategory() { + switch (getPreferenceKey()) { + case "modes_category_alarm": + return PRIORITY_CATEGORY_ALARMS; + case "modes_category_media": + return PRIORITY_CATEGORY_MEDIA; + case "modes_category_system": + return PRIORITY_CATEGORY_SYSTEM; + case "modes_category_reminders": + return PRIORITY_CATEGORY_REMINDERS; + case "modes_category_events": + return PRIORITY_CATEGORY_EVENTS; + } + return -1; + } +} diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceControllerTest.java new file mode 100644 index 00000000000..26da6ab8dc6 --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceControllerTest.java @@ -0,0 +1,77 @@ +/* + * 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.NotificationManager.INTERRUPTION_FILTER_PRIORITY; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +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.ZenPolicy; +import androidx.preference.Preference; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +@RunWith(RobolectricTestRunner.class) +@EnableFlags(Flags.FLAG_MODES_UI) +public final class ZenModeOtherLinkPreferenceControllerTest { + + private ZenModeOtherLinkPreferenceController mController; + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + private Context mContext; + @Mock + private ZenModesBackend mBackend; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + mContext = RuntimeEnvironment.application; + + mController = new ZenModeOtherLinkPreferenceController( + mContext, "something", mBackend); + } + + @Test + @EnableFlags(Flags.FLAG_MODES_UI) + public void testHasSummary() { + Preference pref = mock(Preference.class); + ZenMode zenMode = new ZenMode("id", + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build()) + .build(), true); + mController.updateZenMode(pref, zenMode); + verify(pref).setSummary(any()); + } +} \ No newline at end of file diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeOtherPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeOtherPreferenceControllerTest.java new file mode 100644 index 00000000000..6dd918a145f --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeOtherPreferenceControllerTest.java @@ -0,0 +1,277 @@ +/* + * 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.NotificationManager.INTERRUPTION_FILTER_PRIORITY; +import static android.service.notification.ZenPolicy.STATE_ALLOW; +import static android.service.notification.ZenPolicy.STATE_UNSET; +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +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.ZenPolicy; +import androidx.preference.TwoStatePreference; +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; +import org.robolectric.RuntimeEnvironment; + +@RunWith(RobolectricTestRunner.class) +@EnableFlags(Flags.FLAG_MODES_UI) +public final class ZenModeOtherPreferenceControllerTest { + + private Context mContext; + @Mock + private ZenModesBackend mBackend; + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mContext = RuntimeEnvironment.application; + } + + @Test + public void testUpdateState_alarms() { + TwoStatePreference preference = mock(TwoStatePreference.class); + 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()) + .build(), true); + + ZenModeOtherPreferenceController controller = + new ZenModeOtherPreferenceController(mContext, "modes_category_alarm", mBackend); + + controller.updateZenMode(preference, zenMode); + + verify(preference).setChecked(true); + } + + @Test + public void testOnPreferenceChange_alarms() { + TwoStatePreference preference = mock(TwoStatePreference.class); + 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(false).build()) + .build(), true); + + ZenModeOtherPreferenceController controller = + new ZenModeOtherPreferenceController(mContext, "modes_category_alarm", mBackend); + + controller.updateZenMode(preference, zenMode); + + controller.onPreferenceChange(preference, true); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ZenMode.class); + verify(mBackend).updateMode(captor.capture()); + assertThat(captor.getValue().getPolicy().getPriorityCategoryAlarms()) + .isEqualTo(STATE_ALLOW); + assertThat(captor.getValue().getPolicy().getPriorityCategoryEvents()) + .isEqualTo(STATE_UNSET); + } + + @Test + public void testUpdateState_media() { + TwoStatePreference preference = mock(TwoStatePreference.class); + ZenMode zenMode = new ZenMode("id", + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().allowMedia(true).build()) + .build(), true); + + ZenModeOtherPreferenceController controller = + new ZenModeOtherPreferenceController(mContext, "modes_category_media", mBackend); + + controller.updateZenMode(preference, zenMode); + + verify(preference).setChecked(true); + } + + @Test + public void testOnPreferenceChange_media() { + TwoStatePreference preference = mock(TwoStatePreference.class); + ZenMode zenMode = new ZenMode("id", + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().allowMedia(false).build()) + .build(), true); + + ZenModeOtherPreferenceController controller = + new ZenModeOtherPreferenceController(mContext, "modes_category_media", mBackend); + + controller.updateZenMode(preference, zenMode); + + controller.onPreferenceChange(preference, true); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ZenMode.class); + verify(mBackend).updateMode(captor.capture()); + assertThat(captor.getValue().getPolicy().getPriorityCategoryMedia()) + .isEqualTo(STATE_ALLOW); + assertThat(captor.getValue().getPolicy().getPriorityCategoryEvents()) + .isEqualTo(STATE_UNSET); + } + + @Test + public void testUpdateState_system() { + TwoStatePreference preference = mock(TwoStatePreference.class); + ZenMode zenMode = new ZenMode("id", + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().allowSystem(true).build()) + .build(), true); + + ZenModeOtherPreferenceController controller = + new ZenModeOtherPreferenceController(mContext, "modes_category_system", mBackend); + + controller.updateZenMode(preference, zenMode); + + verify(preference).setChecked(true); + } + + @Test + public void testOnPreferenceChange_system() { + TwoStatePreference preference = mock(TwoStatePreference.class); + ZenMode zenMode = new ZenMode("id", + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().allowSystem(false).build()) + .build(), true); + + ZenModeOtherPreferenceController controller = + new ZenModeOtherPreferenceController(mContext, "modes_category_system", mBackend); + + controller.updateZenMode(preference, zenMode); + + controller.onPreferenceChange(preference, true); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ZenMode.class); + verify(mBackend).updateMode(captor.capture()); + assertThat(captor.getValue().getPolicy().getPriorityCategorySystem()) + .isEqualTo(STATE_ALLOW); + assertThat(captor.getValue().getPolicy().getPriorityCategoryEvents()) + .isEqualTo(STATE_UNSET); + } + + @Test + public void testUpdateState_reminders() { + TwoStatePreference preference = mock(TwoStatePreference.class); + ZenMode zenMode = new ZenMode("id", + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().allowReminders(true).build()) + .build(), true); + + ZenModeOtherPreferenceController controller = + new ZenModeOtherPreferenceController(mContext, "modes_category_reminders", + mBackend); + + controller.updateZenMode(preference, zenMode); + + verify(preference).setChecked(true); + } + + @Test + public void testOnPreferenceChange_reminders() { + TwoStatePreference preference = mock(TwoStatePreference.class); + ZenMode zenMode = new ZenMode("id", + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().allowReminders(false).build()) + .build(), true); + + ZenModeOtherPreferenceController controller = + new ZenModeOtherPreferenceController(mContext, "modes_category_reminders", + mBackend); + + controller.updateZenMode(preference, zenMode); + + controller.onPreferenceChange(preference, true); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ZenMode.class); + verify(mBackend).updateMode(captor.capture()); + assertThat(captor.getValue().getPolicy().getPriorityCategoryReminders()) + .isEqualTo(STATE_ALLOW); + assertThat(captor.getValue().getPolicy().getPriorityCategoryEvents()) + .isEqualTo(STATE_UNSET); + } + + @Test + public void testUpdateState_events() { + TwoStatePreference preference = mock(TwoStatePreference.class); + ZenMode zenMode = new ZenMode("id", + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().allowEvents(true).build()) + .build(), true); + + ZenModeOtherPreferenceController controller = + new ZenModeOtherPreferenceController(mContext, "modes_category_events", mBackend); + + controller.updateZenMode(preference, zenMode); + + verify(preference).setChecked(true); + } + + @Test + public void testOnPreferenceChange_events() { + TwoStatePreference preference = mock(TwoStatePreference.class); + ZenMode zenMode = new ZenMode("id", + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().allowEvents(false).build()) + .build(), true); + + ZenModeOtherPreferenceController controller = + new ZenModeOtherPreferenceController(mContext, "modes_category_events", mBackend); + + controller.updateZenMode(preference, zenMode); + + controller.onPreferenceChange(preference, true); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ZenMode.class); + verify(mBackend).updateMode(captor.capture()); + assertThat(captor.getValue().getPolicy().getPriorityCategoryEvents()) + .isEqualTo(STATE_ALLOW); + assertThat(captor.getValue().getPolicy().getPriorityCategoryAlarms()) + .isEqualTo(STATE_UNSET); + } +} \ No newline at end of file diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeRepeatCallersPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeRepeatCallersPreferenceControllerTest.java new file mode 100644 index 00000000000..7bbb042c471 --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeRepeatCallersPreferenceControllerTest.java @@ -0,0 +1,133 @@ +/* + * 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.NotificationManager.INTERRUPTION_FILTER_PRIORITY; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_ANYONE; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_STARRED; +import static android.service.notification.ZenPolicy.STATE_ALLOW; +import static android.service.notification.ZenPolicy.STATE_DISALLOW; +import static android.service.notification.ZenPolicy.STATE_UNSET; +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +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.ZenPolicy; +import androidx.preference.TwoStatePreference; +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; +import org.robolectric.RuntimeEnvironment; + +@RunWith(RobolectricTestRunner.class) +@EnableFlags(Flags.FLAG_MODES_UI) +public final class ZenModeRepeatCallersPreferenceControllerTest { + + private Context mContext; + @Mock + private ZenModesBackend mBackend; + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mContext = RuntimeEnvironment.application; + } + + @Test + public void testUpdateState_allCalls() { + TwoStatePreference preference = mock(TwoStatePreference.class); + ZenMode zenMode = new ZenMode("id", + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder() + .allowCalls(PEOPLE_TYPE_ANYONE) + .build()) + .build(), true); + + ZenModeRepeatCallersPreferenceController controller = + new ZenModeRepeatCallersPreferenceController(mContext, "repeat", mBackend, 1); + + controller.updateZenMode(preference, zenMode); + + verify(preference).setChecked(true); + verify(preference).setEnabled(false); + } + + @Test + public void testUpdateState_someCalls() { + TwoStatePreference preference = mock(TwoStatePreference.class); + ZenMode zenMode = new ZenMode("id", + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder() + .allowCalls(PEOPLE_TYPE_STARRED) + .allowRepeatCallers(true) + .build()) + .build(), true); + + ZenModeRepeatCallersPreferenceController controller = + new ZenModeRepeatCallersPreferenceController(mContext, "repeat", mBackend, 1); + + controller.updateZenMode(preference, zenMode); + + verify(preference).setChecked(true); + verify(preference).setEnabled(true); + } + + @Test + public void testOnPreferenceChange() { + TwoStatePreference preference = mock(TwoStatePreference.class); + ZenMode zenMode = new ZenMode("id", + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().allowRepeatCallers(true).build()) + .build(), true); + + ZenModeRepeatCallersPreferenceController controller = + new ZenModeRepeatCallersPreferenceController(mContext, "repeat", mBackend, 1); + + controller.updateZenMode(preference, zenMode); + + controller.onPreferenceChange(preference, false); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ZenMode.class); + verify(mBackend).updateMode(captor.capture()); + assertThat(captor.getValue().getPolicy().getPriorityCategoryRepeatCallers()) + .isEqualTo(STATE_DISALLOW); + assertThat(captor.getValue().getPolicy().getPriorityCategoryEvents()) + .isEqualTo(STATE_UNSET); + assertThat(captor.getValue().getPolicy().getPriorityCallSenders()) + .isEqualTo(STATE_UNSET); + } +} \ No newline at end of file diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModesSummaryHelperTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModesSummaryHelperTest.java index 621c5b06f9d..67be82f9032 100644 --- a/tests/robotests/src/com/android/settings/notification/modes/ZenModesSummaryHelperTest.java +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModesSummaryHelperTest.java @@ -85,4 +85,84 @@ public class ZenModesSummaryHelperTest { assertThat(mSummaryHelper.getPeopleSummary(zenMode)).isEqualTo("All people can interrupt"); } + + @Test + public void getOtherSoundCategoriesSummary_single() { + AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed")) + .setType(AutomaticZenRule.TYPE_BEDTIME) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().allowAlarms(true).build()) + .build(); + ZenMode zenMode = new ZenMode("id", rule, true); + + assertThat(mSummaryHelper.getOtherSoundCategoriesSummary(zenMode)).isEqualTo( + "Alarms can interrupt"); + } + + @Test + public void getOtherSoundCategoriesSummary_duo() { + AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed")) + .setType(AutomaticZenRule.TYPE_BEDTIME) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().allowAlarms(true).allowMedia(true).build()) + .build(); + ZenMode zenMode = new ZenMode("id", rule, true); + + assertThat(mSummaryHelper.getOtherSoundCategoriesSummary(zenMode)).isEqualTo( + "Alarms and media can interrupt"); + } + + @Test + public void getOtherSoundCategoriesSummary_trio() { + AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed")) + .setType(AutomaticZenRule.TYPE_BEDTIME) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder() + .allowAlarms(true) + .allowMedia(true) + .allowSystem(true) + .build()) + .build(); + ZenMode zenMode = new ZenMode("id", rule, true); + + assertThat(mSummaryHelper.getOtherSoundCategoriesSummary(zenMode)).isEqualTo( + "Alarms, media, and touch sounds can interrupt"); + } + + @Test + public void getOtherSoundCategoriesSummary_quad() { + AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed")) + .setType(AutomaticZenRule.TYPE_BEDTIME) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder() + .allowAlarms(true) + .allowMedia(true) + .allowSystem(true) + .allowReminders(true) + .build()) + .build(); + ZenMode zenMode = new ZenMode("id", rule, true); + + assertThat(mSummaryHelper.getOtherSoundCategoriesSummary(zenMode)).isEqualTo( + "Alarms, media, and 2 more can interrupt"); + } + + @Test + public void getOtherSoundCategoriesSummary_all() { + AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed")) + .setType(AutomaticZenRule.TYPE_BEDTIME) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder() + .allowAlarms(true) + .allowMedia(true) + .allowSystem(true) + .allowReminders(true) + .allowEvents(true) + .build()) + .build(); + ZenMode zenMode = new ZenMode("id", rule, true); + + assertThat(mSummaryHelper.getOtherSoundCategoriesSummary(zenMode)).isEqualTo( + "Alarms, media, and 3 more can interrupt"); + } }