diff --git a/res/values/strings.xml b/res/values/strings.xml index 4d843fb85cf..e50cfecb471 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -3943,6 +3943,16 @@ Location time zone detection changes are not allowed If your device location is available, it may be used to set your time zone + + + Notifications + + Time zone change + + Receive a notification when your time zone is automatically updated + + notification, time, zone, timezone + View legal info, status, software version diff --git a/res/xml/date_time_prefs.xml b/res/xml/date_time_prefs.xml index fe0fd7e2c52..678b1e2d2f7 100644 --- a/res/xml/date_time_prefs.xml +++ b/res/xml/date_time_prefs.xml @@ -75,18 +75,33 @@ + + + + + + + + android:key="time_feedback_preference_category" + android:title="@string/time_feedback_category_title" + settings:controller="com.android.settings.datetime.TimeFeedbackPreferenceCategoryController" + settings:keywords="@string/keywords_time_feedback_category"> + android:key="time_feedback" + android:title="@string/time_feedback_title" + settings:controller="com.android.settings.datetime.TimeFeedbackPreferenceController" + settings:keywords="@string/keywords_time_feedback" /> diff --git a/src/com/android/settings/datetime/TimeFeedbackLaunchUtils.java b/src/com/android/settings/datetime/DateTimeLaunchUtils.java similarity index 85% rename from src/com/android/settings/datetime/TimeFeedbackLaunchUtils.java rename to src/com/android/settings/datetime/DateTimeLaunchUtils.java index 8a023cbfb68..aef0ff2080b 100644 --- a/src/com/android/settings/datetime/TimeFeedbackLaunchUtils.java +++ b/src/com/android/settings/datetime/DateTimeLaunchUtils.java @@ -22,7 +22,7 @@ import android.provider.DeviceConfig; import com.android.settings.flags.Flags; /** A class to avoid duplication of launch-control logic for "time feedback" support. */ -final class TimeFeedbackLaunchUtils { +final class DateTimeLaunchUtils { /** * A {@link DeviceConfig} flag that influences whether the settings entries related to help and * feedback are supported on this device / for this user. @@ -30,21 +30,21 @@ final class TimeFeedbackLaunchUtils { public static final String KEY_HELP_AND_FEEDBACK_FEATURE_SUPPORTED = "time_help_and_feedback_feature_supported"; - private TimeFeedbackLaunchUtils() {} + private DateTimeLaunchUtils() {} static boolean isFeedbackFeatureSupported() { // Support is determined according to: // 1) A build-time flag to determine release feature availability. // 2) A runtime / server-side flag to determine which devices / who gets to see the feature. // This is launch control for limiting the feedback to droidfooding. - return isFeatureSupportedThisRelease() && isFeatureSupportedOnThisDevice(); + return isFeedbackFeatureSupportedThisRelease() && isFeedbackFeatureSupportedOnThisDevice(); } - private static boolean isFeatureSupportedThisRelease() { + private static boolean isFeedbackFeatureSupportedThisRelease() { return Flags.datetimeFeedback(); } - private static boolean isFeatureSupportedOnThisDevice() { + private static boolean isFeedbackFeatureSupportedOnThisDevice() { boolean defaultIsSupported = false; return DeviceConfig.getBoolean( NAMESPACE_SETTINGS_UI, KEY_HELP_AND_FEEDBACK_FEATURE_SUPPORTED, defaultIsSupported); diff --git a/src/com/android/settings/datetime/DateTimeSettings.java b/src/com/android/settings/datetime/DateTimeSettings.java index f3c11d43ac3..f57adc8dd8f 100644 --- a/src/com/android/settings/datetime/DateTimeSettings.java +++ b/src/com/android/settings/datetime/DateTimeSettings.java @@ -74,6 +74,15 @@ public class DateTimeSettings extends DashboardFragment implements use(TimeFeedbackPreferenceCategoryController.class); use(TimeFeedbackPreferenceController.class) .registerWithOptionalCategoryController(helpAndFeedbackCategoryController); + + // All the elements in the category are optional, so we must ensure the category is only + // available if any of the elements are available. + NotificationsPreferenceCategoryController + notificationsPreferenceCategoryController = + use(NotificationsPreferenceCategoryController.class); + use(TimeZoneNotificationsPreferenceController.class) + .registerIn( + notificationsPreferenceCategoryController); } @Override diff --git a/src/com/android/settings/datetime/NotificationsPreferenceCategoryController.java b/src/com/android/settings/datetime/NotificationsPreferenceCategoryController.java new file mode 100644 index 00000000000..02a2b9e7671 --- /dev/null +++ b/src/com/android/settings/datetime/NotificationsPreferenceCategoryController.java @@ -0,0 +1,65 @@ +/* + * 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.datetime; + +import android.content.Context; + +import androidx.annotation.NonNull; + +import com.android.server.flags.Flags; +import com.android.settings.core.BasePreferenceController; +import com.android.settingslib.core.AbstractPreferenceController; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + + +/** + * A controller for the Settings category for "time notifications". + */ +public class NotificationsPreferenceCategoryController extends BasePreferenceController { + + private final List mChildControllers = new ArrayList<>(); + + public NotificationsPreferenceCategoryController(@NonNull Context context, + @NonNull String preferenceKey) { + super(context, preferenceKey); + } + + /** + * Adds a controller whose own availability can determine the category's availability status. + */ + void addChildController(@NonNull AbstractPreferenceController childController) { + mChildControllers.add(Objects.requireNonNull(childController)); + } + + @Override + public int getAvailabilityStatus() { + // Firstly, hide the category if it is not enabled by flags. + if (!Flags.datetimeNotifications()) { + return UNSUPPORTED_ON_DEVICE; + } + + // Secondly, only show the category if there's one or more controllers available within it. + for (AbstractPreferenceController childController : mChildControllers) { + if (childController.isAvailable()) { + return AVAILABLE; + } + } + return UNSUPPORTED_ON_DEVICE; + } +} diff --git a/src/com/android/settings/datetime/TimeFeedbackPreferenceCategoryController.java b/src/com/android/settings/datetime/TimeFeedbackPreferenceCategoryController.java index 0b70af7fc01..372eee89d7d 100644 --- a/src/com/android/settings/datetime/TimeFeedbackPreferenceCategoryController.java +++ b/src/com/android/settings/datetime/TimeFeedbackPreferenceCategoryController.java @@ -62,6 +62,6 @@ public class TimeFeedbackPreferenceCategoryController extends BasePreferenceCont } protected boolean isTimeFeedbackFeatureEnabled() { - return TimeFeedbackLaunchUtils.isFeedbackFeatureSupported(); + return DateTimeLaunchUtils.isFeedbackFeatureSupported(); } } diff --git a/src/com/android/settings/datetime/TimeFeedbackPreferenceController.java b/src/com/android/settings/datetime/TimeFeedbackPreferenceController.java index 5abe4af33cb..907c202e09f 100644 --- a/src/com/android/settings/datetime/TimeFeedbackPreferenceController.java +++ b/src/com/android/settings/datetime/TimeFeedbackPreferenceController.java @@ -67,7 +67,7 @@ public class TimeFeedbackPreferenceController @Override public int getAvailabilityStatus() { - if (!TimeFeedbackLaunchUtils.isFeedbackFeatureSupported()) { + if (!DateTimeLaunchUtils.isFeedbackFeatureSupported()) { return UNSUPPORTED_ON_DEVICE; } return mAvailabilityStatus; diff --git a/src/com/android/settings/datetime/TimeZoneNotificationsPreferenceController.java b/src/com/android/settings/datetime/TimeZoneNotificationsPreferenceController.java new file mode 100644 index 00000000000..105a3766eff --- /dev/null +++ b/src/com/android/settings/datetime/TimeZoneNotificationsPreferenceController.java @@ -0,0 +1,189 @@ +/* + * 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.datetime; + +import static android.app.time.Capabilities.CAPABILITY_NOT_ALLOWED; +import static android.app.time.Capabilities.CAPABILITY_NOT_APPLICABLE; +import static android.app.time.Capabilities.CAPABILITY_NOT_SUPPORTED; +import static android.app.time.Capabilities.CAPABILITY_POSSESSED; + +import android.app.time.TimeManager; +import android.app.time.TimeZoneCapabilities; +import android.app.time.TimeZoneCapabilitiesAndConfig; +import android.app.time.TimeZoneConfiguration; +import android.content.Context; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settings.core.TogglePreferenceController; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnStart; +import com.android.settingslib.core.lifecycle.events.OnStop; + +import java.util.concurrent.Executor; + +public final class TimeZoneNotificationsPreferenceController + extends TogglePreferenceController + implements LifecycleObserver, OnStart, OnStop, TimeManager.TimeZoneDetectorListener { + + private static final String TAG = "TZNotificationsSettings"; + + private final TimeManager mTimeManager; + private @Nullable TimeZoneCapabilitiesAndConfig mTimeZoneCapabilitiesAndConfig; + private @Nullable Preference mPreference; + + public TimeZoneNotificationsPreferenceController(@NonNull Context context, + @NonNull String preferenceKey) { + super(context, preferenceKey); + mTimeManager = context.getSystemService(TimeManager.class); + } + + /** + * Registers this controller with a category controller so that the category can be optionally + * displayed, i.e. if all the child controllers are not available, the category heading won't be + * available. + */ + public void registerIn(@NonNull NotificationsPreferenceCategoryController categoryController) { + categoryController.addChildController(this); + } + + @Override + public boolean isChecked() { + if (!isAutoTimeZoneEnabled()) { + return false; + } + + // forceRefresh set to true as the notifications toggle may have been turned off by + // switching off automatic time zone + TimeZoneCapabilitiesAndConfig capabilitiesAndConfig = + getTimeZoneCapabilitiesAndConfig(/* forceRefresh= */ true); + TimeZoneConfiguration configuration = capabilitiesAndConfig.getConfiguration(); + return configuration.areNotificationsEnabled(); + } + + + @Override + public boolean setChecked(boolean isChecked) { + TimeZoneConfiguration configuration = new TimeZoneConfiguration.Builder() + .setNotificationsEnabled(isChecked) + .build(); + return mTimeManager.updateTimeZoneConfiguration(configuration); + } + + @Override + public void displayPreference(@NonNull PreferenceScreen screen) { + super.displayPreference(screen); + mPreference = screen.findPreference(getPreferenceKey()); + } + + @Override + public void onStart() { + // Register for updates to the user's time zone capabilities or configuration which could + // require UI changes. + Executor mainExecutor = mContext.getMainExecutor(); + mTimeManager.addTimeZoneDetectorListener(mainExecutor, this); + // Setup the initial state. + refreshUi(); + } + + @Override + public void onStop() { + mTimeManager.removeTimeZoneDetectorListener(this); + } + + @Override + public boolean isSliceable() { + return true; + } + + @Override + public int getSliceHighlightMenuRes() { + return R.string.menu_key_system; + } + + @Override + public void updateState(@NonNull Preference preference) { + super.updateState(preference); + + // enable / disable the toggle based on automatic time zone being enabled or not + preference.setEnabled(isAutoTimeZoneEnabled()); + } + + + @Override + public int getAvailabilityStatus() { + TimeZoneCapabilities timeZoneCapabilities = + getTimeZoneCapabilitiesAndConfig(/* forceRefresh= */ false).getCapabilities(); + int capability = timeZoneCapabilities.getConfigureNotificationsEnabledCapability(); + + // The preference can be present and enabled, present and disabled or not present. + if (capability == CAPABILITY_NOT_SUPPORTED || capability == CAPABILITY_NOT_ALLOWED) { + return UNSUPPORTED_ON_DEVICE; + } else if (capability == CAPABILITY_NOT_APPLICABLE || capability == CAPABILITY_POSSESSED) { + return isAutoTimeZoneEnabled() ? AVAILABLE : DISABLED_DEPENDENT_SETTING; + } else { + Log.e(TAG, "Unknown capability=" + capability); + return UNSUPPORTED_ON_DEVICE; + } + } + + /** + * Implementation of {@link TimeManager.TimeZoneDetectorListener#onChange()}. Called by the + * system server after a change that affects {@link TimeZoneCapabilitiesAndConfig}. + */ + @Override + public void onChange() { + refreshUi(); + } + + @Override + @NonNull + public CharSequence getSummary() { + return mContext.getString(R.string.time_zone_change_notifications_toggle_summary); + } + + private void refreshUi() { + // Force a refresh of cached user capabilities and config. + getTimeZoneCapabilitiesAndConfig(/* forceRefresh= */ true); + refreshSummary(mPreference); + } + + /** + * Returns the current user capabilities and configuration. {@code forceRefresh} can be {@code + * true} to discard any cached copy. + */ + private TimeZoneCapabilitiesAndConfig getTimeZoneCapabilitiesAndConfig(boolean forceRefresh) { + if (forceRefresh || mTimeZoneCapabilitiesAndConfig == null) { + mTimeZoneCapabilitiesAndConfig = mTimeManager.getTimeZoneCapabilitiesAndConfig(); + } + return mTimeZoneCapabilitiesAndConfig; + } + + /** + * Returns whether the user can select this preference or not, as it is a sub toggle of + * automatic time zone. + */ + private boolean isAutoTimeZoneEnabled() { + return mTimeManager.getTimeZoneCapabilitiesAndConfig().getConfiguration() + .isAutoDetectionEnabled(); + } +} diff --git a/tests/robotests/src/com/android/settings/datetime/AutoTimeZonePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/datetime/AutoTimeZonePreferenceControllerTest.java index 2961935d4b1..643523daaef 100644 --- a/tests/robotests/src/com/android/settings/datetime/AutoTimeZonePreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/datetime/AutoTimeZonePreferenceControllerTest.java @@ -282,10 +282,12 @@ public class AutoTimeZonePreferenceControllerTest { locationSupported ? Capabilities.CAPABILITY_POSSESSED : Capabilities.CAPABILITY_NOT_SUPPORTED) .setSetManualTimeZoneCapability(Capabilities.CAPABILITY_POSSESSED) + .setConfigureNotificationsEnabledCapability(Capabilities.CAPABILITY_POSSESSED) .build(); TimeZoneConfiguration config = new TimeZoneConfiguration.Builder() .setAutoDetectionEnabled(autoEnabled) .setGeoDetectionEnabled(locationSupported) + .setNotificationsEnabled(true) .build(); return new TimeZoneCapabilitiesAndConfig(status, capabilities, config); } diff --git a/tests/robotests/src/com/android/settings/datetime/LocationProviderStatusPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/datetime/LocationProviderStatusPreferenceControllerTest.java index 9ace56387b8..f2f05779c81 100644 --- a/tests/robotests/src/com/android/settings/datetime/LocationProviderStatusPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/datetime/LocationProviderStatusPreferenceControllerTest.java @@ -261,6 +261,7 @@ public class LocationProviderStatusPreferenceControllerTest { .setUseLocationEnabled(true) .setConfigureGeoDetectionEnabledCapability(configureGeoDetectionEnabledCapability) .setSetManualTimeZoneCapability(Capabilities.CAPABILITY_POSSESSED) + .setConfigureNotificationsEnabledCapability(Capabilities.CAPABILITY_POSSESSED) .build(); return new TimeZoneCapabilitiesAndConfig(status, capabilities, diff --git a/tests/robotests/src/com/android/settings/datetime/LocationTimeZoneDetectionPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/datetime/LocationTimeZoneDetectionPreferenceControllerTest.java index 2f232574760..8f942531052 100644 --- a/tests/robotests/src/com/android/settings/datetime/LocationTimeZoneDetectionPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/datetime/LocationTimeZoneDetectionPreferenceControllerTest.java @@ -236,11 +236,13 @@ public class LocationTimeZoneDetectionPreferenceControllerTest { .setUseLocationEnabled(useLocationEnabled) .setConfigureGeoDetectionEnabledCapability(configureGeoDetectionEnabledCapability) .setSetManualTimeZoneCapability(CAPABILITY_NOT_APPLICABLE) + .setConfigureNotificationsEnabledCapability(CAPABILITY_POSSESSED) .build(); TimeZoneConfiguration configuration = new TimeZoneConfiguration.Builder() .setAutoDetectionEnabled(setAutoDetectionEnabled) .setGeoDetectionEnabled(true) + .setNotificationsEnabled(true) .build(); return new TimeZoneCapabilitiesAndConfig(status, capabilities, configuration); diff --git a/tests/robotests/src/com/android/settings/datetime/NotificationsPreferenceCategoryControllerTest.java b/tests/robotests/src/com/android/settings/datetime/NotificationsPreferenceCategoryControllerTest.java new file mode 100644 index 00000000000..98bdfff2c63 --- /dev/null +++ b/tests/robotests/src/com/android/settings/datetime/NotificationsPreferenceCategoryControllerTest.java @@ -0,0 +1,83 @@ +/* + * 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.datetime; + +import static com.android.settings.core.BasePreferenceController.AVAILABLE; +import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; + +import com.android.server.flags.Flags; +import com.android.settingslib.core.AbstractPreferenceController; + +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 class NotificationsPreferenceCategoryControllerTest { + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + + private NotificationsPreferenceCategoryController mController; + @Mock + private AbstractPreferenceController mChildController; + + @Before + @EnableFlags({Flags.FLAG_DATETIME_NOTIFICATIONS}) + public void setUp() { + MockitoAnnotations.initMocks(this); + Context context = RuntimeEnvironment.getApplication(); + + mController = new NotificationsPreferenceCategoryController(context, "test_key"); + mController.addChildController(mChildController); + } + + @Test + @DisableFlags({Flags.FLAG_DATETIME_NOTIFICATIONS}) + public void getAvailabilityStatus_featureDisabled() { + when(mChildController.isAvailable()).thenReturn(true); + + assertThat(mController.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE); + } + + @Test + @EnableFlags({Flags.FLAG_DATETIME_NOTIFICATIONS}) + public void getAvailabilityStatus_featureEnabled() { + when(mChildController.isAvailable()).thenReturn(false); + + assertThat(mController.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE); + + when(mChildController.isAvailable()).thenReturn(true); + + assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE); + } +} diff --git a/tests/robotests/src/com/android/settings/datetime/TimeFeedbackPreferenceCategoryControllerTest.java b/tests/robotests/src/com/android/settings/datetime/TimeFeedbackPreferenceCategoryControllerTest.java index 1747f170bf4..2e603e38d37 100644 --- a/tests/robotests/src/com/android/settings/datetime/TimeFeedbackPreferenceCategoryControllerTest.java +++ b/tests/robotests/src/com/android/settings/datetime/TimeFeedbackPreferenceCategoryControllerTest.java @@ -74,7 +74,7 @@ public class TimeFeedbackPreferenceCategoryControllerTest { /** * Extend class under test to change {@link #isTimeFeedbackFeatureEnabled} to not call - * {@link TimeFeedbackLaunchUtils} because that's non-trivial to fake. + * {@link DateTimeLaunchUtils} because that's non-trivial to fake. */ private static class TestTimeFeedbackPreferenceCategoryController extends TimeFeedbackPreferenceCategoryController { diff --git a/tests/robotests/src/com/android/settings/datetime/TimeZonePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/datetime/TimeZonePreferenceControllerTest.java index fec410b29db..649201b695f 100644 --- a/tests/robotests/src/com/android/settings/datetime/TimeZonePreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/datetime/TimeZonePreferenceControllerTest.java @@ -16,6 +16,7 @@ package com.android.settings.datetime; +import static android.app.time.Capabilities.CAPABILITY_POSSESSED; import static android.app.time.DetectorStatusTypes.DETECTION_ALGORITHM_STATUS_RUNNING; import static android.app.time.DetectorStatusTypes.DETECTOR_STATUS_RUNNING; import static android.app.time.LocationTimeZoneAlgorithmStatus.PROVIDER_STATUS_NOT_PRESENT; @@ -113,10 +114,12 @@ public class TimeZonePreferenceControllerTest { .setUseLocationEnabled(useLocationEnabled) .setConfigureGeoDetectionEnabledCapability(Capabilities.CAPABILITY_NOT_SUPPORTED) .setSetManualTimeZoneCapability(suggestManualCapability) + .setConfigureNotificationsEnabledCapability(CAPABILITY_POSSESSED) .build(); TimeZoneConfiguration config = new TimeZoneConfiguration.Builder() .setAutoDetectionEnabled(!suggestManualAllowed) .setGeoDetectionEnabled(false) + .setNotificationsEnabled(true) .build(); return new TimeZoneCapabilitiesAndConfig(status, capabilities, config); }