Add interstitial for modes that are disabled but not by user

This covers both the case of navigating to the mode page through the mode aggregator menu as well as if an app sends an intent to go to the mode.

The interstitial visuals are not done yet; in particular, the image is just a gray box for now.

Manual tests done:
- visually verifying the interstitial page in both portrait and landscape
- accessing the mode page from modes list, confirming interstitial pops up
- accessing enabled and disabled-by-user mode page from modes list (no interstitial)
- getting to the mode page from intent through an app
- accessing enabled and disabled-by-user mode pages from app intent (no interstitial)
- adjusted display and font size (it looks bad with max display & font size, maybe not much to be done)

Bug: 332730534
Test: manual, ZenModeFragmentTest, SetupInterstitialActivityTest
Flag: android.app.modes_ui
Change-Id: I21f13b0842d5b118a341f7d85e8fcac947ca3a06
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 223b473f..7b79611 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -1370,6 +1370,12 @@
         </activity>
 
         <activity
+            android:name=".notification.modes.SetupInterstitialActivity"
+            android:exported="false"
+            android:theme="@style/Theme.Settings.NoActionBar">
+        </activity>
+
+        <activity
             android:name=".notification.zen.ZenSuggestionActivity"
             android:label="@string/zen_mode_settings_title"
             android:icon="@drawable/ic_suggestion_dnd"
diff --git a/res/layout-land/mode_interstitial_layout.xml b/res/layout-land/mode_interstitial_layout.xml
new file mode 100644
index 0000000..bd63611
--- /dev/null
+++ b/res/layout-land/mode_interstitial_layout.xml
@@ -0,0 +1,113 @@
+<?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.
+  -->
+<LinearLayout
+    android:id="@+id/interstitial_page"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:fitsSystemWindows="true"
+    android:transitionGroup="true"
+    android:orientation="vertical">
+
+    <Toolbar
+        android:id="@+id/action_bar"
+        style="?android:attr/actionBarStyle"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:theme="?android:attr/actionBarTheme"
+        android:elevation="0dp"
+        android:background="@android:color/transparent"/>
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:paddingTop="12dp"
+        android:paddingBottom="24dp"
+        android:paddingHorizontal="24dp"
+        android:clipChildren="true">
+
+        <ScrollView
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            app:layout_constrainedHeight="true"
+            app:layout_constraintBottom_toTopOf="@+id/enable_mode_button"
+            app:layout_constraintEnd_toStartOf="@+id/guideline"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent">
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:orientation="vertical"
+                android:paddingEnd="12dp"
+                android:paddingStart="12dp">
+
+                <TextView
+                    android:id="@+id/mode_name_title"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:clickable="false"
+                    android:paddingVertical="12dp"
+                    android:textSize="36sp"
+                    android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Title" />
+
+                <TextView
+                    android:id="@+id/mode_name_subtitle"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:clickable="false"
+                    android:paddingBottom="12dp"
+                    android:text="@string/zen_mode_setup_page_summary"
+                    android:textSize="18sp"
+                    android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Subhead" />
+
+            </LinearLayout>
+
+        </ScrollView>
+
+        <Button
+            android:id="@+id/enable_mode_button"
+            style="@style/ActionPrimaryButton"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toStartOf="@+id/guideline"
+            android:paddingEnd="12dp" />
+
+        <!-- guideline to have text/button side & image side take up half the page each -->
+        <androidx.constraintlayout.widget.Guideline
+            android:id="@+id/guideline"
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent"
+            android:orientation="vertical"
+            app:layout_constraintGuide_percent="0.5" />
+
+        <ImageView
+            android:id="@+id/image"
+            android:layout_height="0dp"
+            android:layout_width="0dp"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintStart_toEndOf="@+id/guideline"
+            app:layout_constraintEnd_toEndOf="parent"
+            android:paddingStart="12dp" />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/mode_interstitial_layout.xml b/res/layout/mode_interstitial_layout.xml
new file mode 100644
index 0000000..74cabdb
--- /dev/null
+++ b/res/layout/mode_interstitial_layout.xml
@@ -0,0 +1,86 @@
+<?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.
+  -->
+<LinearLayout
+    android:id="@+id/interstitial_page"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:fitsSystemWindows="true"
+    android:transitionGroup="true"
+    android:orientation="vertical">
+
+    <Toolbar
+        android:id="@+id/action_bar"
+        style="?android:attr/actionBarStyle"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:theme="?android:attr/actionBarTheme"
+        android:elevation="0dp"
+        android:background="@android:color/transparent" />
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:paddingTop="12dp"
+        android:paddingBottom="64dp"
+        android:paddingLeft="24dp"
+        android:paddingRight="24dp"
+        android:orientation="vertical">
+
+        <!-- image goes here -->
+        <ImageView
+            android:id="@+id/image"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:clickable="false"
+            app:layout_constraintBottom_toTopOf="@+id/mode_name_title"
+            app:layout_constraintTop_toTopOf="parent" />
+
+        <TextView
+            android:id="@+id/mode_name_title"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:clickable="false"
+            android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Title"
+            app:layout_constraintBottom_toTopOf="@+id/mode_name_subtitle"
+            android:textSize="36sp"
+            android:paddingVertical="12dp" />
+
+        <TextView
+            android:id="@+id/mode_name_subtitle"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:clickable="false"
+            android:text="@string/zen_mode_setup_page_summary"
+            android:textSize="18sp"
+            android:paddingBottom="12dp"
+            android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Body1"
+            app:layout_constraintBottom_toTopOf="@+id/enable_mode_button" />
+
+        <Button
+            android:id="@+id/enable_mode_button"
+            style="@style/ActionPrimaryButton"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:paddingVertical="12dp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintVertical_bias="1" />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 311b56f..1d67db2 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -7995,6 +7995,12 @@
     <!-- Subtitle for the Do not Disturb slice. [CHAR LIMIT=50]-->
     <string name="zen_mode_slice_subtitle">Limit interruptions</string>
 
+    <!-- Priority Modes: Summary on a page prompting the user to set up/enable a mode [CHAR_LIMIT=NONE] -->
+    <string name="zen_mode_setup_page_summary">Block interruptions and distractions</string>
+
+    <!-- Priority Modes: Label on a button prompting the user to set up the mode with the given name. [CHAR_LIMIT=40] -->
+    <string name="zen_mode_setup_button_label">Set up <xliff:g id="mode" example="My Mode">%1$s</xliff:g></string>
+
     <!-- Do not disturb: Title for the Do not Disturb dialog to turn on Do not disturb. [CHAR LIMIT=50]-->
     <string name="zen_mode_settings_turn_on_dialog_title">Turn on Do Not Disturb</string>
 
diff --git a/src/com/android/settings/notification/modes/SetupInterstitialActivity.java b/src/com/android/settings/notification/modes/SetupInterstitialActivity.java
new file mode 100644
index 0000000..9d76abb
--- /dev/null
+++ b/src/com/android/settings/notification/modes/SetupInterstitialActivity.java
@@ -0,0 +1,177 @@
+/*
+ * 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 android.app.ActionBar;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.Toast;
+import android.widget.Toolbar;
+
+import androidx.activity.EdgeToEdge;
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+import androidx.fragment.app.FragmentActivity;
+
+import com.android.settings.R;
+import com.android.settings.Utils;
+import com.android.settingslib.notification.modes.ZenMode;
+import com.android.settingslib.notification.modes.ZenModesBackend;
+
+/**
+ * Interstitial page for modes that are disabled, but not disabled by the user. This page
+ * provides a button to enable the mode, and then goes to the mode setup page.
+ */
+public class SetupInterstitialActivity extends FragmentActivity {
+    private static final String TAG = "ModeSetupInterstitial";
+    private ZenModesBackend mBackend;
+
+    /**
+     * Returns an intent leading to this page for the given mode and context.
+     */
+    public static @NonNull Intent getIntent(@NonNull Context context, @NonNull ZenMode mode) {
+        return new Intent(Intent.ACTION_MAIN)
+                .setClass(context, SetupInterstitialActivity.class)
+                .setPackage(context.getPackageName())
+                .setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT)
+                .putExtra(EXTRA_AUTOMATIC_ZEN_RULE_ID, mode.getId());
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        EdgeToEdge.enable(this);
+        Utils.setupEdgeToEdge(this);
+        super.onCreate(savedInstanceState);
+        mBackend = ZenModesBackend.getInstance(this);
+        setContentView(R.layout.mode_interstitial_layout);
+
+        // Set up toolbar to only have a back button & no title
+        Toolbar toolbar = findViewById(R.id.action_bar);
+        setActionBar(toolbar);
+        ActionBar actionBar = getActionBar();
+        if (actionBar != null) {
+            actionBar.setDisplayHomeAsUpEnabled(true);
+            actionBar.setHomeButtonEnabled(true);
+            actionBar.setDisplayShowTitleEnabled(false);
+        }
+    }
+
+    @Override
+    public boolean onNavigateUp() {
+        // have the home button on the action bar go back
+        getOnBackPressedDispatcher().onBackPressed();
+        return true;
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+
+        // See if we have mode data
+        final Intent intent = getIntent();
+        if (intent == null) {
+            Log.w(TAG, "no intent found for modes interstitial");
+            finish();
+        }
+
+        String modeId = intent.getStringExtra(EXTRA_AUTOMATIC_ZEN_RULE_ID);
+        if (modeId == null) {
+            Log.w(TAG, "no mode id included in intent: " + intent);
+            finish();
+        }
+
+        ZenMode mode = mBackend.getMode(modeId);
+        if (mode == null) {
+            Log.w(TAG, "mode not found for mode id: " + modeId);
+            finish();
+        }
+        setTitle(mode.getName());
+
+        TextView title = findViewById(R.id.mode_name_title);
+        if (title != null) {
+            title.setText(mode.getName());
+        }
+
+        ImageView img = findViewById(R.id.image);
+        if (img != null) {
+            setImage(img, mode);
+        }
+
+        Button button = findViewById(R.id.enable_mode_button);
+        if (button != null) {
+            setupButton(button, mode);
+        }
+    }
+
+    private void setImage(ImageView img, @NonNull ZenMode mode) {
+        // TODO: b/332730534 - set actual images depending on mode type (asynchronously?)
+        img.setImageDrawable(new ColorDrawable(Color.GRAY));
+    }
+
+    private void setupButton(Button button, @NonNull ZenMode mode) {
+        button.setText(getString(R.string.zen_mode_setup_button_label, mode.getName()));
+        button.setOnClickListener(enableButtonListener(mode.getId()));
+    }
+
+    @VisibleForTesting
+    View.OnClickListener enableButtonListener(String modeId) {
+        return unused -> {
+            // When clicked, we first reload mode info in case it has changed in the interim,
+            // then enable the mode and then send the user to the mode's configuration page.
+            boolean updated = enableMode(modeId);
+
+            // Don't come back to this activity after sending the user to the modes page, if
+            // they happen to go back. Forward the activity result in case we got here (indirectly)
+            // from some app that is waiting for the result.
+            finish();
+            if (updated) {
+                ZenSubSettingLauncher.forMode(this, modeId)
+                        .addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT).launch();
+            }
+        };
+    }
+
+    // Enables the given mode after first refreshing its data from the backend. Returns true if
+    // the update went through, and false if for some reason the mode wasn't found.
+    private boolean enableMode(@NonNull String modeId) {
+        if (mBackend == null) {
+            return false;
+        }
+
+        ZenMode modeToUpdate = mBackend.getMode(modeId);
+        if (modeToUpdate == null) {
+            // tell the user the mode isn't found, for some reason
+            Toast.makeText(this, R.string.zen_mode_rule_not_found_text, Toast.LENGTH_SHORT)
+                    .show();
+            return false;
+        }
+
+        modeToUpdate.getRule().setEnabled(true);
+        mBackend.updateMode(modeToUpdate);
+        return true;
+    }
+}
diff --git a/src/com/android/settings/notification/modes/ZenModeFragment.java b/src/com/android/settings/notification/modes/ZenModeFragment.java
index 1b7e344..0a80977 100644
--- a/src/com/android/settings/notification/modes/ZenModeFragment.java
+++ b/src/com/android/settings/notification/modes/ZenModeFragment.java
@@ -16,6 +16,8 @@
 
 package com.android.settings.notification.modes;
 
+import static com.android.settingslib.notification.modes.ZenMode.Status.DISABLED_BY_OTHER;
+
 import android.app.AlertDialog;
 import android.app.settings.SettingsEnums;
 import android.content.Context;
@@ -25,6 +27,7 @@
 
 import androidx.activity.ComponentActivity;
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.core.view.MenuProvider;
 
 import com.android.settings.R;
@@ -41,6 +44,7 @@
     private static final int DELETE_MODE = 2;
 
     private ModeMenuProvider mModeMenuProvider;
+    private boolean mSettingsObserverRegistered = false; // for ManualDurationPreferenceController
 
     @Override
     protected int getPreferenceScreenResId() {
@@ -82,9 +86,14 @@
     @Override
     public void onStart() {
         super.onStart();
+        ZenMode mode = getMode();
+
+        // Consider redirecting to the interstitial if the mode is disabled (but not by the user).
+        if (maybeRedirectToInterstitial(mode)) {
+            return;
+        }
 
         // Set title for the entire screen
-        ZenMode mode = getMode();
         ComponentActivity activity = getActivity();
         if (mode != null && activity != null) {
             activity.setTitle(mode.getName());
@@ -94,14 +103,27 @@
 
         // allow duration preference controller to listen for settings changes
         use(ManualDurationPreferenceController.class).registerSettingsObserver();
+        mSettingsObserverRegistered = true;
+    }
+
+    private boolean maybeRedirectToInterstitial(@Nullable ZenMode mode) {
+        if (mode == null || mode.getStatus() != DISABLED_BY_OTHER) {
+            return false;
+        }
+        // don't come back here from the interstitial
+        finish();
+        mContext.startActivity(SetupInterstitialActivity.getIntent(mContext, mode));
+        return true;
     }
 
     @Override
     public void onStop() {
-        if (getActivity() != null) {
+        if (getActivity() != null && mModeMenuProvider != null) {
             getActivity().removeMenuProvider(mModeMenuProvider);
         }
-        use(ManualDurationPreferenceController.class).unregisterSettingsObserver();
+        if (mSettingsObserverRegistered) {
+            use(ManualDurationPreferenceController.class).unregisterSettingsObserver();
+        }
         super.onStop();
     }
 
diff --git a/tests/robotests/src/com/android/settings/notification/modes/SetupInterstitialActivityTest.java b/tests/robotests/src/com/android/settings/notification/modes/SetupInterstitialActivityTest.java
new file mode 100644
index 0000000..c3ac0fd
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/notification/modes/SetupInterstitialActivityTest.java
@@ -0,0 +1,99 @@
+/*
+ * 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.SettingsActivity.EXTRA_SHOW_FRAGMENT;
+import static com.android.settings.SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+
+import androidx.test.core.app.ActivityScenario;
+
+import com.android.settingslib.notification.modes.TestModeBuilder;
+import com.android.settingslib.notification.modes.ZenMode;
+import com.android.settingslib.notification.modes.ZenModesBackend;
+
+import org.junit.Before;
+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)
+public class SetupInterstitialActivityTest {
+    private static final String MODE_ID = "modeId";
+
+    @Mock
+    private ZenModesBackend mBackend;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        // set global backend instance so that when the interstitial activity launches, it'll get
+        // this mock backend
+        ZenModesBackend.setInstance(mBackend);
+    }
+
+    @Test
+    public void enableButton_enablesModeAndRedirectsToModePage() {
+        ZenMode mode = new TestModeBuilder().setId(MODE_ID).setEnabled(false).build();
+        when(mBackend.getMode(MODE_ID)).thenReturn(mode);
+
+        // Set up scenario with this mode information
+        ActivityScenario<SetupInterstitialActivity> scenario =
+                ActivityScenario.launch(new Intent(Intent.ACTION_MAIN)
+                        .setClass(RuntimeEnvironment.getApplication(),
+                                SetupInterstitialActivity.class)
+                        .putExtra(EXTRA_AUTOMATIC_ZEN_RULE_ID, MODE_ID));
+        scenario.onActivity(activity -> {
+            View.OnClickListener listener = activity.enableButtonListener(MODE_ID);
+
+            // simulate button press even though we don't actually have a button
+            listener.onClick(null);
+
+            // verify that the backend got a request to enable the mode
+            ArgumentCaptor<ZenMode> captor = ArgumentCaptor.forClass(ZenMode.class);
+            verify(mBackend).updateMode(captor.capture());
+            ZenMode updatedMode = captor.getValue();
+            assertThat(updatedMode.getId()).isEqualTo(MODE_ID);
+            assertThat(updatedMode.isEnabled()).isTrue();
+
+            // confirm that the next activity is the mode page
+            Intent openModePageIntent = shadowOf(activity).getNextStartedActivity();
+            assertThat(openModePageIntent.getStringExtra(EXTRA_SHOW_FRAGMENT))
+                    .isEqualTo(ZenModeFragment.class.getName());
+            Bundle fragmentArgs = openModePageIntent.getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS);
+            assertThat(fragmentArgs).isNotNull();
+            assertThat(fragmentArgs.getString(EXTRA_AUTOMATIC_ZEN_RULE_ID)).isEqualTo(MODE_ID);
+        });
+        scenario.close();
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeFragmentTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeFragmentTest.java
new file mode 100644
index 0000000..576e32a
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeFragmentTest.java
@@ -0,0 +1,125 @@
+/*
+ * 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.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.content.Intent;
+import android.os.Bundle;
+
+import androidx.fragment.app.testing.FragmentScenario;
+import androidx.lifecycle.Lifecycle;
+
+import com.android.settingslib.notification.modes.TestModeBuilder;
+import com.android.settingslib.notification.modes.ZenMode;
+import com.android.settingslib.notification.modes.ZenModesBackend;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class ZenModeFragmentTest {
+    private static final String MODE_ID = "modeId";
+
+    @Mock
+    private ZenModesBackend mBackend;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        // set up static instance so that the fragment will get a mock version of the backend
+        ZenModesBackend.setInstance(mBackend);
+    }
+
+    // Sets up the scenario's fragment by passing in arguments setting the provided mode ID.
+    // After running this method, users can then use scenario.onFragment(fragment -> {...}) on the
+    // returned scenario to test fragment behavior.
+    private FragmentScenario<ZenModeFragment> setUpScenarioForModeId(String modeId) {
+        Bundle args = new Bundle();
+        args.putString(EXTRA_AUTOMATIC_ZEN_RULE_ID, modeId);
+        return FragmentScenario.launch(
+                ZenModeFragment.class, /* bundle= */ args, 0, Lifecycle.State.INITIALIZED);
+    }
+
+    @Test
+    public void disabledMode_redirectsToInterstitial() {
+        // Mode is disabled, and not by the user
+        ZenMode mode = new TestModeBuilder().setId(MODE_ID).setEnabled(false, false)
+                .build();
+
+        when(mBackend.getMode(MODE_ID)).thenReturn(mode);
+
+        // actually set up fragment for testing
+        FragmentScenario scenario = setUpScenarioForModeId(MODE_ID);
+        scenario.moveToState(Lifecycle.State.STARTED);
+
+        scenario.onFragment(fragment -> {
+            // since the mode is disabled & not by the user, we should go to the next activity
+            Intent nextIntent = shadowOf(fragment.getActivity()).getNextStartedActivity();
+            assertThat(nextIntent).isNotNull();
+            assertThat(nextIntent.getComponent().getClassName()).isEqualTo(
+                    SetupInterstitialActivity.class.getCanonicalName());
+            assertThat(nextIntent.getStringExtra(EXTRA_AUTOMATIC_ZEN_RULE_ID)).isEqualTo(MODE_ID);
+        });
+        scenario.close();
+    }
+
+    @Test
+    public void disabledMode_byUser_noRedirect() {
+        // Mode is disabled by the user
+        ZenMode mode = new TestModeBuilder().setId(MODE_ID).setEnabled(false, true)
+                .build();
+
+        when(mBackend.getMode(MODE_ID)).thenReturn(mode);
+        FragmentScenario scenario = setUpScenarioForModeId(MODE_ID);
+        scenario.moveToState(Lifecycle.State.STARTED);
+
+        scenario.onFragment(fragment -> {
+            // there shouldn't be a next started activity, because we don't redirect
+            Intent nextIntent = shadowOf(fragment.getActivity()).getNextStartedActivity();
+            assertThat(nextIntent).isNull();
+        });
+        scenario.close();
+    }
+
+    @Test
+    public void enabledMode_noRedirect() {
+        // enabled rule
+        ZenMode mode = new TestModeBuilder().setId(MODE_ID).setEnabled(true)
+                .build();
+
+        when(mBackend.getMode(MODE_ID)).thenReturn(mode);
+        FragmentScenario scenario = setUpScenarioForModeId(MODE_ID);
+        scenario.moveToState(Lifecycle.State.STARTED);
+
+        scenario.onFragment(fragment -> {
+            // there shouldn't be a next started activity, because we don't redirect
+            Intent nextIntent = shadowOf(fragment.getActivity()).getNextStartedActivity();
+            assertThat(nextIntent).isNull();
+        });
+        scenario.close();
+    }
+}