Merge "Fix a11y issues in the schedule editor" into main
diff --git a/src/com/android/settings/notification/modes/AbstractZenModePreferenceController.java b/src/com/android/settings/notification/modes/AbstractZenModePreferenceController.java
index c740847..c473456 100644
--- a/src/com/android/settings/notification/modes/AbstractZenModePreferenceController.java
+++ b/src/com/android/settings/notification/modes/AbstractZenModePreferenceController.java
@@ -25,8 +25,8 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
-import androidx.preference.PreferenceScreen;
import com.android.settingslib.core.AbstractPreferenceController;
import com.android.settingslib.notification.modes.ZenMode;
@@ -92,29 +92,14 @@
return true;
}
- // Called by parent Fragment onAttach, for any methods (such as isAvailable()) that need
- // zen mode info before onStart. Most callers should use updateZenMode instead, which will
- // do any further necessary propagation.
- protected final void setZenMode(@NonNull ZenMode zenMode) {
+ /**
+ * Assigns the {@link ZenMode} of this controller, so that it can be used later from
+ * {@link #isAvailable()} and {@link #updateState(Preference)}.
+ */
+ final void setZenMode(@NonNull ZenMode zenMode) {
mZenMode = zenMode;
}
- // Called by the parent Fragment onStart, which means it will happen before resume.
- public void updateZenMode(@NonNull Preference preference, @NonNull ZenMode zenMode) {
- mZenMode = zenMode;
- updateState(preference);
- }
-
- @Override
- public void displayPreference(PreferenceScreen screen) {
- super.displayPreference(screen);
- if (mZenMode != null) {
- displayPreference(screen, mZenMode);
- }
- }
-
- public void displayPreference(PreferenceScreen screen, @NonNull ZenMode zenMode) {}
-
@Override
public final void updateState(Preference preference) {
super.updateState(preference);
@@ -167,4 +152,20 @@
return mode;
});
}
+
+ @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+ @Nullable
+ ZenMode getZenMode() {
+ return mZenMode;
+ }
+
+ /**
+ * Convenience method for tests. Assigns the {@link ZenMode} of this controller, and calls
+ * {@link #updateState(Preference)} immediately.
+ */
+ @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+ final void updateZenMode(@NonNull Preference preference, @NonNull ZenMode zenMode) {
+ mZenMode = zenMode;
+ updateState(preference);
+ }
}
diff --git a/src/com/android/settings/notification/modes/ManualDurationPreferenceController.java b/src/com/android/settings/notification/modes/ManualDurationPreferenceController.java
index 073f8ab..28aac63 100644
--- a/src/com/android/settings/notification/modes/ManualDurationPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ManualDurationPreferenceController.java
@@ -18,6 +18,7 @@
import android.content.Context;
+import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
@@ -49,12 +50,12 @@
return zenMode.isManualDnd();
}
- // Called by parent fragment onAttach().
+ // Called by parent fragment onStart().
void registerSettingsObserver() {
mSettingsObserver.register();
}
- // Called by parent fragment onDetach().
+ // Called by parent fragment onStop().
void unregisterSettingsObserver() {
mSettingsObserver.unregister();
}
@@ -69,7 +70,7 @@
}
@Override
- public void updateState(Preference preference, ZenMode unusedZenMode) {
+ public void updateState(Preference preference, @NonNull ZenMode unusedZenMode) {
// This controller is a link between a Settings value (ZEN_DURATION) and the manual DND
// mode. The status of the zen mode object itself doesn't affect the preference
// value, as that comes from settings; that value from settings will determine the
diff --git a/src/com/android/settings/notification/modes/ZenModeEditNameIconFragmentBase.java b/src/com/android/settings/notification/modes/ZenModeEditNameIconFragmentBase.java
index d666254..96cbf91 100644
--- a/src/com/android/settings/notification/modes/ZenModeEditNameIconFragmentBase.java
+++ b/src/com/android/settings/notification/modes/ZenModeEditNameIconFragmentBase.java
@@ -21,14 +21,11 @@
import android.content.Context;
import android.os.Bundle;
-import android.util.Log;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
-import androidx.preference.Preference;
-import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settings.dashboard.DashboardFragment;
@@ -39,7 +36,6 @@
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
-import java.util.Collection;
import java.util.List;
/**
@@ -79,7 +75,11 @@
? icicle.getParcelable(MODE_KEY, ZenMode.class)
: onCreateInstantiateZenMode();
- if (mZenMode == null) {
+ if (mZenMode != null) {
+ for (var controller : getZenPreferenceControllers()) {
+ controller.setZenMode(mZenMode);
+ }
+ } else {
finish();
}
}
@@ -110,58 +110,32 @@
);
}
+ private Iterable<AbstractZenModePreferenceController> getZenPreferenceControllers() {
+ return getPreferenceControllers().stream()
+ .flatMap(List::stream)
+ .filter(AbstractZenModePreferenceController.class::isInstance)
+ .map(AbstractZenModePreferenceController.class::cast)
+ .toList();
+ }
+
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
@Nullable
ZenMode getZenMode() {
return mZenMode;
}
- @Override
- public void onStart() {
- super.onStart();
- updateControllers();
- }
-
@VisibleForTesting
final void setModeName(String name) {
checkNotNull(mZenMode).getRule().setName(Strings.nullToEmpty(name));
- updateControllers(); // Updates confirmation button.
+ forceUpdatePreferences(); // Updates confirmation button.
}
@VisibleForTesting
final void setModeIcon(@DrawableRes int iconResId) {
checkNotNull(mZenMode).getRule().setIconResId(iconResId);
- updateControllers(); // Updates icon at the top.
+ forceUpdatePreferences(); // Updates icon at the top.
}
- protected void updateControllers() {
- PreferenceScreen screen = getPreferenceScreen();
- Collection<List<AbstractPreferenceController>> controllers = getPreferenceControllers();
- if (mZenMode == null || screen == null || controllers == null) {
- return;
- }
- for (List<AbstractPreferenceController> list : controllers) {
- for (AbstractPreferenceController controller : list) {
- try {
- final String key = controller.getPreferenceKey();
- final Preference preference = screen.findPreference(key);
- if (preference != null) {
- AbstractZenModePreferenceController zenController =
- (AbstractZenModePreferenceController) controller;
- zenController.updateZenMode(preference, mZenMode);
- } else {
- Log.d(getLogTag(),
- String.format("Cannot find preference with key %s in Controller %s",
- key, controller.getClass().getSimpleName()));
- }
- controller.displayPreference(screen);
- } catch (ClassCastException e) {
- // Skip any controllers that aren't AbstractZenModePreferenceController.
- Log.d(getLogTag(), "Could not cast: " + controller.getClass().getSimpleName());
- }
- }
- }
- }
@VisibleForTesting
final void saveMode() {
diff --git a/src/com/android/settings/notification/modes/ZenModeFragment.java b/src/com/android/settings/notification/modes/ZenModeFragment.java
index 5aeb34d..1b7e344 100644
--- a/src/com/android/settings/notification/modes/ZenModeFragment.java
+++ b/src/com/android/settings/notification/modes/ZenModeFragment.java
@@ -80,14 +80,6 @@
}
@Override
- public void onAttach(@NonNull Context context) {
- super.onAttach(context);
-
- // allow duration preference controller to listen for settings changes
- use(ManualDurationPreferenceController.class).registerSettingsObserver();
- }
-
- @Override
public void onStart() {
super.onStart();
@@ -99,6 +91,9 @@
mModeMenuProvider = new ModeMenuProvider(mode);
activity.addMenuProvider(mModeMenuProvider);
}
+
+ // allow duration preference controller to listen for settings changes
+ use(ManualDurationPreferenceController.class).registerSettingsObserver();
}
@Override
@@ -106,13 +101,8 @@
if (getActivity() != null) {
getActivity().removeMenuProvider(mModeMenuProvider);
}
- super.onStop();
- }
-
- @Override
- public void onDetach() {
use(ManualDurationPreferenceController.class).unregisterSettingsObserver();
- super.onDetach();
+ super.onStop();
}
@Override
@@ -122,13 +112,13 @@
}
@Override
- protected void updateZenModeState() {
+ protected void onUpdatedZenModeState() {
// Because this fragment may be asked to finish by the delete menu but not be done doing
// so yet, ignore any attempts to update info in that case.
if (getActivity() != null && getActivity().isFinishing()) {
return;
}
- super.updateZenModeState();
+ super.onUpdatedZenModeState();
}
private class ModeMenuProvider implements MenuProvider {
diff --git a/src/com/android/settings/notification/modes/ZenModeFragmentBase.java b/src/com/android/settings/notification/modes/ZenModeFragmentBase.java
index f461fc3..c63b3a8 100644
--- a/src/com/android/settings/notification/modes/ZenModeFragmentBase.java
+++ b/src/com/android/settings/notification/modes/ZenModeFragmentBase.java
@@ -18,24 +18,18 @@
import static android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID;
-import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.preference.Preference;
-import androidx.preference.PreferenceScreen;
+import androidx.lifecycle.Lifecycle;
import com.android.settings.R;
-import com.android.settingslib.core.AbstractPreferenceController;
import com.android.settingslib.notification.modes.ZenMode;
-import com.google.common.base.Preconditions;
-
import java.util.List;
-import java.util.function.Consumer;
/**
* Base class for Settings pages used to configure individual modes.
@@ -43,13 +37,27 @@
abstract class ZenModeFragmentBase extends ZenModesFragmentBase {
static final String TAG = "ZenModeSettings";
- @Nullable // only until reloadMode() is called
- private ZenMode mZenMode;
+ @Nullable private ZenMode mZenMode;
+ @Nullable private ZenMode mModeOnLastControllerUpdate;
@Override
- public void onAttach(@NonNull Context context) {
- super.onAttach(context);
+ public void onCreate(Bundle icicle) {
+ mZenMode = loadModeFromArguments();
+ if (mZenMode != null) {
+ // Propagate mode info through to controllers. Must be done before super.onCreate(),
+ // because that one calls AbstractPreferenceController.isAvailable().
+ for (var controller : getZenPreferenceControllers()) {
+ controller.setZenMode(mZenMode);
+ }
+ } else {
+ toastAndFinish();
+ }
+ super.onCreate(icicle);
+ }
+
+ @Nullable
+ private ZenMode loadModeFromArguments() {
String id = null;
if (getActivity() != null && getActivity().getIntent() != null) {
id = getActivity().getIntent().getStringExtra(EXTRA_AUTOMATIC_ZEN_RULE_ID);
@@ -60,93 +68,65 @@
}
if (id == null) {
Log.d(TAG, "No id provided");
- toastAndFinish();
- return;
+ return null;
}
- if (!reloadMode(id)) {
- Log.d(TAG, "Mode id " + id + " not found");
- toastAndFinish();
- return;
+
+ ZenMode mode = mBackend.getMode(id);
+ if (mode == null) {
+ Log.d(TAG, "Mode with id " + id + " not found");
+ return null;
}
- if (mZenMode != null) {
- // Propagate mode info through to controllers.
- for (List<AbstractPreferenceController> list : getPreferenceControllers()) {
- try {
- for (AbstractPreferenceController controller : list) {
- // mZenMode guaranteed non-null from reloadMode() above
- ((AbstractZenModePreferenceController) controller).setZenMode(mZenMode);
- }
- } catch (ClassCastException e) {
- // ignore controllers that aren't AbstractZenModePreferenceController
- }
- }
- }
+ return mode;
}
- /**
- * Refresh stored ZenMode data.
- * @param id the mode ID
- * @return whether we successfully got mode data from the backend.
- */
- private boolean reloadMode(String id) {
- mZenMode = mBackend.getMode(id);
- if (mZenMode == null) {
- return false;
- }
- return true;
+ private Iterable<AbstractZenModePreferenceController> getZenPreferenceControllers() {
+ return getPreferenceControllers().stream()
+ .flatMap(List::stream)
+ .filter(AbstractZenModePreferenceController.class::isInstance)
+ .map(AbstractZenModePreferenceController.class::cast)
+ .toList();
}
- /**
- * Refresh ZenMode data any time the system's zen mode state changes (either the zen mode value
- * itself, or the config), and also (once updated) update the info for all controllers.
- */
@Override
- protected void updateZenModeState() {
+ protected void onUpdatedZenModeState() {
if (mZenMode == null) {
- // This shouldn't happen, but guard against it in case
+ Log.wtf(TAG, "mZenMode is null in onUpdatedZenModeState");
toastAndFinish();
return;
}
+
String id = mZenMode.getId();
- if (!reloadMode(id)) {
+ ZenMode mode = mBackend.getMode(id);
+ if (mode == null) {
Log.d(TAG, "Mode id=" + id + " not found");
toastAndFinish();
return;
}
- updateControllers();
+
+ mZenMode = mode;
+ maybeUpdateControllersState(mode);
}
- private void updateControllers() {
- if (getPreferenceControllers() == null || mZenMode == null) {
- return;
+ /**
+ * Updates all {@link AbstractZenModePreferenceController} based on the loaded mode info.
+ * For each controller, {@link AbstractZenModePreferenceController#setZenMode} will be called.
+ * Then, {@link AbstractZenModePreferenceController#updateState} will be called as well, unless
+ * we determine it's not necessary (for example, if we know that {@code DashboardFragment} will
+ * do it soon).
+ */
+ private void maybeUpdateControllersState(@NonNull ZenMode zenMode) {
+ boolean needsFullUpdate =
+ getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED)
+ && (mModeOnLastControllerUpdate == null
+ || !mModeOnLastControllerUpdate.equals(zenMode));
+ mModeOnLastControllerUpdate = zenMode.copy();
+
+ for (var controller : getZenPreferenceControllers()) {
+ controller.setZenMode(zenMode);
}
- final PreferenceScreen screen = getPreferenceScreen();
- if (screen == null) {
- Log.d(TAG, "PreferenceScreen not found");
- return;
- }
- for (List<AbstractPreferenceController> list : getPreferenceControllers()) {
- for (AbstractPreferenceController controller : list) {
- try {
- // Find preference associated with controller
- final String key = controller.getPreferenceKey();
- final Preference preference = screen.findPreference(key);
- if (preference != null) {
- AbstractZenModePreferenceController zenController =
- (AbstractZenModePreferenceController) controller;
- zenController.updateZenMode(preference, mZenMode);
- } else {
- Log.d(TAG,
- String.format("Cannot find preference with key %s in Controller %s",
- key, controller.getClass().getSimpleName()));
- }
- controller.displayPreference(screen);
- } catch (ClassCastException e) {
- // Skip any controllers that aren't AbstractZenModePreferenceController.
- Log.d(TAG, "Could not cast: " + controller.getClass().getSimpleName());
- }
- }
+ if (needsFullUpdate) {
+ forceUpdatePreferences();
}
}
@@ -163,16 +143,4 @@
public ZenMode getMode() {
return mZenMode;
}
-
- protected final boolean saveMode(Consumer<ZenMode> updater) {
- Preconditions.checkState(mBackend != null);
- ZenMode mode = mZenMode;
- if (mode == null) {
- Log.wtf(TAG, "Cannot save mode, it hasn't been loaded (" + getClass() + ")");
- return false;
- }
- updater.accept(mode);
- mBackend.updateMode(mode);
- return true;
- }
}
diff --git a/src/com/android/settings/notification/modes/ZenModeTriggerUpdatePreferenceController.java b/src/com/android/settings/notification/modes/ZenModeTriggerUpdatePreferenceController.java
index 043a38c..3ee6d94 100644
--- a/src/com/android/settings/notification/modes/ZenModeTriggerUpdatePreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModeTriggerUpdatePreferenceController.java
@@ -37,7 +37,6 @@
import androidx.annotation.StringRes;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
-import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settingslib.PrimarySwitchPreference;
@@ -78,13 +77,6 @@
}
@Override
- public void displayPreference(PreferenceScreen screen, @NonNull ZenMode zenMode) {
- // Preload approved components, but only for the package that owns the rule (since it's the
- // only package that can have a valid configurationActivity).
- mServiceListing.loadApprovedComponents(zenMode.getRule().getPackageName());
- }
-
- @Override
void updateState(Preference preference, @NonNull ZenMode zenMode) {
if (!isAvailable(zenMode)) {
return;
@@ -137,6 +129,7 @@
@SuppressLint("SwitchIntDef")
private void setUpForAppTrigger(Preference preference, ZenMode mode) {
// App-owned mode may have triggerDescription, configurationActivity, or both/neither.
+ mServiceListing.loadApprovedComponents(mode.getRule().getPackageName());
Intent configurationIntent =
mConfigurationActivityHelper.getConfigurationActivityIntentForMode(
mode, mServiceListing::findService);
diff --git a/src/com/android/settings/notification/modes/ZenModesFragmentBase.java b/src/com/android/settings/notification/modes/ZenModesFragmentBase.java
index 0bc0617..652415b 100644
--- a/src/com/android/settings/notification/modes/ZenModesFragmentBase.java
+++ b/src/com/android/settings/notification/modes/ZenModesFragmentBase.java
@@ -16,14 +16,11 @@
package com.android.settings.notification.modes;
+import static com.google.common.base.Preconditions.checkNotNull;
+
import android.annotation.NonNull;
-import android.annotation.Nullable;
import android.content.Context;
-import android.database.ContentObserver;
-import android.net.Uri;
-import android.os.Handler;
import android.os.UserManager;
-import android.provider.Settings.Global;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
@@ -38,17 +35,10 @@
protected static final String TAG = "ZenModesSettings";
protected static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
- private final Handler mHandler = new Handler();
- private final SettingsObserver mSettingsObserver = new SettingsObserver();
-
protected Context mContext;
-
protected ZenModesBackend mBackend;
protected ZenHelperBackend mHelperBackend;
-
- // Individual pages must implement this method based on what they should do when
- // the device's zen mode state changes.
- protected abstract void updateZenModeState();
+ private ZenSettingsObserver mSettingsObserver;
ZenModesFragmentBase() {
super(UserManager.DISALLOW_ADJUST_VOLUME);
@@ -69,8 +59,8 @@
mContext = context;
mBackend = ZenModesBackend.getInstance(context);
mHelperBackend = ZenHelperBackend.getInstance(context);
+ mSettingsObserver = new ZenSettingsObserver(context, this::onUpdatedZenModeState);
super.onAttach(context);
- mSettingsObserver.register();
}
@Override
@@ -83,45 +73,20 @@
finish();
}
}
+
+ onUpdatedZenModeState(); // Maybe, while we weren't observing.
+ checkNotNull(mSettingsObserver).register();
}
+ /**
+ * Called by this fragment when we know or suspect that Zen Modes data or state has changed.
+ * Individual pages must implement this method to refresh whatever they're displaying.
+ */
+ protected abstract void onUpdatedZenModeState();
+
@Override
- public void onResume() {
- super.onResume();
- updateZenModeState();
- }
-
- @Override
- public void onDetach() {
- super.onDetach();
- mSettingsObserver.unregister();
- }
-
- private final class SettingsObserver extends ContentObserver {
- private static final Uri ZEN_MODE_URI = Global.getUriFor(Global.ZEN_MODE);
- private static final Uri ZEN_MODE_CONFIG_ETAG_URI = Global.getUriFor(
- Global.ZEN_MODE_CONFIG_ETAG);
-
- private SettingsObserver() {
- super(mHandler);
- }
-
- public void register() {
- getContentResolver().registerContentObserver(ZEN_MODE_URI, false, this);
- getContentResolver().registerContentObserver(ZEN_MODE_CONFIG_ETAG_URI, false, this);
- }
-
- public void unregister() {
- getContentResolver().unregisterContentObserver(this);
- }
-
- @Override
- public void onChange(boolean selfChange, @Nullable Uri uri) {
- super.onChange(selfChange, uri);
- // Shouldn't have any other URIs trigger this method, but check just in case.
- if (ZEN_MODE_URI.equals(uri) || ZEN_MODE_CONFIG_ETAG_URI.equals(uri)) {
- updateZenModeState();
- }
- }
+ public void onStop() {
+ checkNotNull(mSettingsObserver).unregister();
+ super.onStop();
}
}
diff --git a/src/com/android/settings/notification/modes/ZenModesListFragment.java b/src/com/android/settings/notification/modes/ZenModesListFragment.java
index be458b3..a45ca17 100644
--- a/src/com/android/settings/notification/modes/ZenModesListFragment.java
+++ b/src/com/android/settings/notification/modes/ZenModesListFragment.java
@@ -65,7 +65,7 @@
}
@Override
- protected void updateZenModeState() {
+ protected void onUpdatedZenModeState() {
// TODO: b/322373473 -- update any overall description of modes state here if necessary.
// Note the preferences linking to individual rules do not need to be updated, as
// updateState() is called on all preference controllers whenever the page is resumed.
diff --git a/src/com/android/settings/notification/modes/ZenModesListPreferenceController.java b/src/com/android/settings/notification/modes/ZenModesListPreferenceController.java
index ba12b9a..12b7278 100644
--- a/src/com/android/settings/notification/modes/ZenModesListPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModesListPreferenceController.java
@@ -38,7 +38,8 @@
* containing links to each individual mode. This is a central controller that populates and updates
* all the preferences that then lead to a mode configuration page.
*/
-class ZenModesListPreferenceController extends BasePreferenceController {
+class ZenModesListPreferenceController extends BasePreferenceController
+ implements BasePreferenceController.UiBlocker {
protected static final String KEY = "zen_modes_list";
protected ZenModesBackend mBackend;
@@ -49,11 +50,6 @@
}
@Override
- public String getPreferenceKey() {
- return KEY;
- }
-
- @Override
@AvailabilityStatus
public int getAvailabilityStatus() {
return Flags.modesUi() ? AVAILABLE_UNSEARCHABLE : UNSUPPORTED_ON_DEVICE;
@@ -97,6 +93,8 @@
for (String key : originalPreferences.keySet()) {
category.removePreferenceRecursively(key);
}
+
+ setUiBlockerFinished(true);
}
// Provide search data for the modes, which will allow users to reach the modes page if they
diff --git a/src/com/android/settings/notification/modes/ZenSettingsObserver.java b/src/com/android/settings/notification/modes/ZenSettingsObserver.java
new file mode 100644
index 0000000..a853646
--- /dev/null
+++ b/src/com/android/settings/notification/modes/ZenSettingsObserver.java
@@ -0,0 +1,68 @@
+/*
+ * 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.content.Context;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.provider.Settings;
+
+import androidx.annotation.Nullable;
+
+class ZenSettingsObserver extends ContentObserver {
+ private static final Uri ZEN_MODE_URI = Settings.Global.getUriFor(Settings.Global.ZEN_MODE);
+ private static final Uri ZEN_MODE_CONFIG_ETAG_URI = Settings.Global.getUriFor(
+ Settings.Global.ZEN_MODE_CONFIG_ETAG);
+
+ private final Context mContext;
+ @Nullable private Runnable mCallback;
+
+ ZenSettingsObserver(Context context) {
+ this(context, null);
+ }
+
+ ZenSettingsObserver(Context context, @Nullable Runnable callback) {
+ super(context.getMainExecutor(), 0);
+ mContext = context;
+ setOnChangeListener(callback);
+ }
+
+ void register() {
+ mContext.getContentResolver().registerContentObserver(ZEN_MODE_URI, false, this);
+ mContext.getContentResolver().registerContentObserver(ZEN_MODE_CONFIG_ETAG_URI, false,
+ this);
+ }
+
+ void unregister() {
+ mContext.getContentResolver().unregisterContentObserver(this);
+ }
+
+ void setOnChangeListener(@Nullable Runnable callback) {
+ mCallback = callback;
+ }
+
+ @Override
+ public void onChange(boolean selfChange, @Nullable Uri uri) {
+ super.onChange(selfChange, uri);
+ // Shouldn't have any other URIs trigger this method, but check just in case.
+ if (ZEN_MODE_URI.equals(uri) || ZEN_MODE_CONFIG_ETAG_URI.equals(uri)) {
+ if (mCallback != null) {
+ mCallback.run();
+ }
+ }
+ }
+}
diff --git a/tests/robotests/res/xml/modes_fake_settings.xml b/tests/robotests/res/xml/modes_fake_settings.xml
new file mode 100644
index 0000000..a5602dc
--- /dev/null
+++ b/tests/robotests/res/xml/modes_fake_settings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
+ <Preference android:key="pref_id" />
+ <Preference android:key="pref_name" />
+ <Preference android:key="pref_enabled" />
+</PreferenceScreen>
\ No newline at end of file
diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeFragmentBaseTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeFragmentBaseTest.java
new file mode 100644
index 0000000..21f19ff
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeFragmentBaseTest.java
@@ -0,0 +1,364 @@
+/*
+ * 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.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID;
+
+import static com.android.settings.notification.modes.CharSequenceTruth.assertThat;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.app.Flags;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Bundle;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.provider.Settings;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.testing.FragmentScenario;
+import androidx.lifecycle.Lifecycle.State;
+import androidx.preference.Preference;
+
+import com.android.settings.R;
+import com.android.settingslib.core.AbstractPreferenceController;
+import com.android.settingslib.notification.modes.TestModeBuilder;
+import com.android.settingslib.notification.modes.ZenMode;
+import com.android.settingslib.notification.modes.ZenModesBackend;
+
+import com.google.common.collect.ImmutableList;
+
+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.shadows.ShadowContentResolver;
+import org.robolectric.shadows.ShadowLooper;
+
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+@EnableFlags(Flags.FLAG_MODES_UI)
+public class ZenModeFragmentBaseTest {
+
+ private static final Uri SETTINGS_URI = Settings.Global.getUriFor(
+ Settings.Global.ZEN_MODE_CONFIG_ETAG);
+
+ @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+ @Mock ZenModesBackend mBackend;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ public void fragment_noArguments_finishes() {
+ when(mBackend.getMode(any())).thenReturn(TestModeBuilder.EXAMPLE);
+
+ FragmentScenario<TestableFragment> scenario = createScenario(null);
+
+ scenario.moveToState(State.RESUMED).onFragment(fragment -> {
+ assertThat(fragment.requireActivity().isFinishing()).isTrue();
+ });
+
+ scenario.close();
+ }
+
+ @Test
+ public void fragment_modeDoesNotExist_finishes() {
+ when(mBackend.getMode(any())).thenReturn(null);
+
+ FragmentScenario<TestableFragment> scenario = createScenario("mode_id");
+
+ scenario.moveToState(State.RESUMED).onFragment(fragment -> {
+ assertThat(fragment.requireActivity().isFinishing()).isTrue();
+ });
+
+ scenario.close();
+ }
+
+ @Test
+ public void fragment_validMode_updatesControllersOnce() {
+ ZenMode mode = new TestModeBuilder().setId("mode_id").build();
+ when(mBackend.getMode("mode_id")).thenReturn(mode);
+
+ FragmentScenario<TestableFragment> scenario = createScenario("mode_id");
+
+ scenario.moveToState(State.CREATED).onFragment(fragment -> {
+ assertThat(fragment.mShowsId.getZenMode()).isEqualTo(mode);
+ assertThat(fragment.mShowsId.isAvailable()).isTrue();
+ assertThat(fragment.mAvailableIfEnabled.getZenMode()).isEqualTo(mode);
+ assertThat(fragment.mAvailableIfEnabled.isAvailable()).isTrue();
+
+ verify(fragment.mShowsId, never()).updateState(any(), any());
+ verify(fragment.mAvailableIfEnabled, never()).updateState(any(), any());
+ });
+
+ scenario.moveToState(State.RESUMED).onFragment(fragment -> {
+ Preference preferenceOne = fragment.requirePreference("pref_id");
+ assertThat(preferenceOne.getSummary()).isEqualTo("Id is mode_id");
+
+ verify(fragment.mShowsId).updateState(any(), eq(mode));
+ verify(fragment.mAvailableIfEnabled).updateState(any(), eq(mode));
+ });
+
+ scenario.close();
+ }
+
+ @Test
+ public void fragment_onStartToOnStop_hasRegisteredContentObserver() {
+ when(mBackend.getMode(any())).thenReturn(TestModeBuilder.EXAMPLE);
+ FragmentScenario<TestableFragment> scenario = createScenario("id");
+
+ scenario.moveToState(State.CREATED).onFragment(fragment ->
+ assertThat(getSettingsContentObservers(fragment)).isEmpty());
+
+ scenario.moveToState(State.STARTED).onFragment(fragment ->
+ assertThat(getSettingsContentObservers(fragment)).hasSize(1));
+
+ scenario.moveToState(State.RESUMED).onFragment(fragment ->
+ assertThat(getSettingsContentObservers(fragment)).hasSize(1));
+
+ scenario.moveToState(State.STARTED).onFragment(fragment ->
+ assertThat(getSettingsContentObservers(fragment)).hasSize(1));
+
+ scenario.moveToState(State.CREATED).onFragment(fragment ->
+ assertThat(getSettingsContentObservers(fragment)).isEmpty());
+
+ scenario.close();
+ }
+
+ @Test
+ public void fragment_onModeUpdatedWithDifferences_updatesControllers() {
+ ZenMode originalMode = new TestModeBuilder().setId("id").setName("Original").build();
+ when(mBackend.getMode("id")).thenReturn(originalMode);
+
+ FragmentScenario<TestableFragment> scenario = createScenario("id");
+ scenario.moveToState(State.RESUMED).onFragment(fragment -> {
+ Preference preference = fragment.requirePreference("pref_name");
+ assertThat(preference.getSummary()).isEqualTo("Original");
+ verify(fragment.mShowsName, times(1)).updateState(any(), eq(originalMode));
+
+ // Now, we get a message saying something changed.
+ ZenMode updatedMode = new TestModeBuilder().setId("id").setName("Updated").build();
+ when(mBackend.getMode("id")).thenReturn(updatedMode);
+ getSettingsContentObservers(fragment).stream().findFirst().get()
+ .dispatchChange(false, SETTINGS_URI);
+ ShadowLooper.idleMainLooper();
+
+ // The screen was updated, and only updated once.
+ assertThat(preference.getSummary()).isEqualTo("Updated");
+ verify(fragment.mShowsName, times(1)).updateState(any(), eq(updatedMode));
+ });
+
+ scenario.close();
+ }
+
+ @Test
+ public void fragment_onModeUpdatedWithoutDifferences_setsModeInControllersButNothingElse() {
+ ZenMode originalMode = new TestModeBuilder().setId("id").setName("Original").build();
+ when(mBackend.getMode("id")).thenReturn(originalMode);
+
+ FragmentScenario<TestableFragment> scenario = createScenario("id");
+ scenario.moveToState(State.RESUMED).onFragment(fragment -> {
+ Preference preference = fragment.requirePreference("pref_name");
+ assertThat(preference.getSummary()).isEqualTo("Original");
+ verify(fragment.mShowsName, times(1)).updateState(any(), eq(originalMode));
+
+ // Now, we get a message saying something changed, but it was for a different mode.
+ ZenMode notUpdatedMode = new TestModeBuilder(originalMode).build();
+ when(mBackend.getMode("id")).thenReturn(notUpdatedMode);
+ getSettingsContentObservers(fragment).stream().findFirst().get()
+ .dispatchChange(false, SETTINGS_URI);
+ ShadowLooper.idleMainLooper();
+
+ // The mode instance was updated, but updateState() was not called.
+ assertThat(preference.getSummary()).isEqualTo("Original");
+ assertThat(fragment.mShowsName.getZenMode()).isSameInstanceAs(notUpdatedMode);
+ verify(fragment.mShowsName, never()).updateState(any(), same(notUpdatedMode));
+ });
+
+ scenario.close();
+ }
+
+ @Test
+ public void fragment_onFragmentRestart_reloadsMode() {
+ ZenMode originalMode = new TestModeBuilder().setId("id").setName("Original").build();
+ when(mBackend.getMode("id")).thenReturn(originalMode);
+
+ FragmentScenario<TestableFragment> scenario = createScenario("id");
+ scenario.moveToState(State.RESUMED).onFragment(fragment -> {
+ Preference preference = fragment.requirePreference("pref_name");
+ assertThat(preference.getSummary()).isEqualTo("Original");
+ verify(fragment.mShowsName, times(1)).updateState(any(), eq(originalMode));
+ });
+
+ ZenMode updatedMode = new TestModeBuilder().setId("id").setName("Updated").build();
+ when(mBackend.getMode("id")).thenReturn(updatedMode);
+
+ scenario.moveToState(State.CREATED).moveToState(State.RESUMED).onFragment(fragment -> {
+ Preference preference = fragment.requirePreference("pref_name");
+ assertThat(preference.getSummary()).isEqualTo("Updated");
+ assertThat(fragment.mShowsName.getZenMode()).isSameInstanceAs(updatedMode);
+ });
+
+ scenario.close();
+ }
+
+ @Test
+ public void fragment_onModeDeleted_finishes() {
+ ZenMode originalMode = new TestModeBuilder().setId("mode_id").build();
+ when(mBackend.getMode("mode_id")).thenReturn(originalMode);
+
+ FragmentScenario<TestableFragment> scenario = createScenario("mode_id");
+ scenario.moveToState(State.RESUMED).onFragment(fragment -> {
+ assertThat(fragment.requireActivity().isFinishing()).isFalse();
+
+ // Now it's no longer there...
+ when(mBackend.getMode(any())).thenReturn(null);
+ getSettingsContentObservers(fragment).stream().findFirst().get()
+ .dispatchChange(false, SETTINGS_URI);
+ ShadowLooper.idleMainLooper();
+
+ assertThat(fragment.requireActivity().isFinishing()).isTrue();
+ });
+
+ scenario.close();
+ }
+
+ private FragmentScenario<TestableFragment> createScenario(@Nullable String modeId) {
+ Bundle fragmentArgs = null;
+ if (modeId != null) {
+ fragmentArgs = new Bundle();
+ fragmentArgs.putString(EXTRA_AUTOMATIC_ZEN_RULE_ID, modeId);
+ }
+
+ FragmentScenario<TestableFragment> scenario = FragmentScenario.launch(
+ TestableFragment.class, fragmentArgs, 0, State.INITIALIZED);
+
+ scenario.onFragment(fragment -> {
+ fragment.setBackend(mBackend); // Before onCreate().
+ });
+
+ return scenario;
+ }
+
+ public static class TestableFragment extends ZenModeFragmentBase {
+
+ private ShowsIdPreferenceController mShowsId;
+ private ShowsNamePreferenceController mShowsName;
+ private AvailableIfEnabledPreferenceController mAvailableIfEnabled;
+
+ @Override
+ protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
+ mShowsId = spy(new ShowsIdPreferenceController(context, "pref_id"));
+ mShowsName = spy(new ShowsNamePreferenceController(context, "pref_name"));
+ mAvailableIfEnabled = spy(
+ new AvailableIfEnabledPreferenceController(context, "pref_enabled"));
+ return ImmutableList.of(mShowsId, mShowsName, mAvailableIfEnabled);
+ }
+
+ @NonNull
+ Preference requirePreference(String key) {
+ Preference preference = getPreferenceScreen().findPreference(key);
+ checkNotNull(preference, "Didn't find preference with key " + key);
+ return preference;
+ }
+
+ ShadowContentResolver getShadowContentResolver() {
+ return shadowOf(requireActivity().getContentResolver());
+ }
+
+ @Override
+ protected int getPreferenceScreenResId() {
+ return R.xml.modes_fake_settings;
+ }
+
+ @Override
+ public int getMetricsCategory() {
+ return 0;
+ }
+ }
+
+ private static class ShowsIdPreferenceController extends AbstractZenModePreferenceController {
+
+ ShowsIdPreferenceController(@NonNull Context context, @NonNull String key) {
+ super(context, key);
+ }
+
+ @Override
+ void updateState(Preference preference, @NonNull ZenMode zenMode) {
+ preference.setSummary("Id is " + zenMode.getId());
+ }
+ }
+
+ private static class ShowsNamePreferenceController extends AbstractZenModePreferenceController {
+
+ ShowsNamePreferenceController(@NonNull Context context, @NonNull String key) {
+ super(context, key);
+ }
+
+ @Override
+ void updateState(Preference preference, @NonNull ZenMode zenMode) {
+ preference.setSummary(zenMode.getName());
+ }
+ }
+
+ private static class AvailableIfEnabledPreferenceController extends
+ AbstractZenModePreferenceController {
+
+ AvailableIfEnabledPreferenceController(@NonNull Context context, @NonNull String key) {
+ super(context, key);
+ }
+
+ @Override
+ public boolean isAvailable(@NonNull ZenMode zenMode) {
+ return zenMode.isEnabled();
+ }
+
+ @Override
+ void updateState(Preference preference, @NonNull ZenMode zenMode) {
+ preference.setSummary("Enabled is " + zenMode.isEnabled());
+ }
+ }
+
+ private ImmutableList<ContentObserver> getSettingsContentObservers(Fragment fragment) {
+ return ImmutableList.copyOf(
+ shadowOf(fragment.requireActivity().getContentResolver())
+ .getContentObservers(SETTINGS_URI));
+ }
+}