Merge "Add button for activating modes manually" into main
diff --git a/res/layout/modes_activation_button.xml b/res/layout/modes_activation_button.xml
new file mode 100644
index 0000000..e8ed824
--- /dev/null
+++ b/res/layout/modes_activation_button.xml
@@ -0,0 +1,30 @@
+<?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.
+  -->
+
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content">
+
+    <Button
+        android:id="@+id/activate_mode"
+        style="@style/ActionPrimaryButton"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"/>
+
+</FrameLayout>
\ No newline at end of file
diff --git a/res/xml/modes_rule_settings.xml b/res/xml/modes_rule_settings.xml
index a380987..f1ff977 100644
--- a/res/xml/modes_rule_settings.xml
+++ b/res/xml/modes_rule_settings.xml
@@ -22,6 +22,11 @@
             android:key="header"
             android:layout="@layout/settings_entity_header" />
 
+    <com.android.settingslib.widget.LayoutPreference
+            android:key="activate"
+            android:selectable="false"
+            android:layout="@layout/modes_activation_button"/>
+
     <PreferenceCategory
             android:title="@string/mode_interruption_filter_title"
             android:key="modes_filters">
diff --git a/src/com/android/settings/notification/modes/AbstractZenModePreferenceController.java b/src/com/android/settings/notification/modes/AbstractZenModePreferenceController.java
index 587f640..aebc4eb 100644
--- a/src/com/android/settings/notification/modes/AbstractZenModePreferenceController.java
+++ b/src/com/android/settings/notification/modes/AbstractZenModePreferenceController.java
@@ -41,7 +41,7 @@
     @Nullable
     protected ZenModesBackend mBackend;
 
-    @Nullable  // only until updateZenMode() is called
+    @Nullable  // only until setZenMode() is called
     private ZenMode mZenMode;
 
     @NonNull
@@ -65,7 +65,22 @@
 
     @Override
     public boolean isAvailable() {
-        return Flags.modesUi();
+        if (mZenMode != null) {
+            return Flags.modesUi() && isAvailable(mZenMode);
+        } else {
+            return Flags.modesUi();
+        }
+    }
+
+    public boolean isAvailable(@NonNull ZenMode zenMode) {
+        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) {
+        mZenMode = zenMode;
     }
 
     // Called by the parent Fragment onStart, which means it will happen before resume.
diff --git a/src/com/android/settings/notification/modes/ZenModeButtonPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeButtonPreferenceController.java
new file mode 100644
index 0000000..1846dfc
--- /dev/null
+++ b/src/com/android/settings/notification/modes/ZenModeButtonPreferenceController.java
@@ -0,0 +1,59 @@
+/*
+ * 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.annotation.NonNull;
+import android.content.Context;
+import android.widget.Button;
+
+import androidx.preference.Preference;
+
+import com.android.settings.R;
+import com.android.settingslib.widget.LayoutPreference;
+
+public class ZenModeButtonPreferenceController extends AbstractZenModePreferenceController {
+
+    private Button mZenButton;
+
+    public ZenModeButtonPreferenceController(Context context, String key, ZenModesBackend backend) {
+        super(context, key, backend);
+    }
+
+    @Override
+    public boolean isAvailable(ZenMode zenMode) {
+        return zenMode.getRule().isManualInvocationAllowed() && zenMode.getRule().isEnabled();
+    }
+
+    @Override
+    public void updateState(Preference preference, @NonNull ZenMode zenMode) {
+        if (mZenButton == null) {
+            mZenButton = ((LayoutPreference) preference).findViewById(R.id.activate_mode);
+        }
+        mZenButton.setOnClickListener(v -> {
+            if (zenMode.isActive()) {
+                mBackend.deactivateMode(zenMode);
+            } else {
+                mBackend.activateMode(zenMode, null);
+            }
+        });
+        if (zenMode.isActive()) {
+            mZenButton.setText(R.string.zen_mode_button_turn_off);
+        } else {
+            mZenButton.setText(R.string.zen_mode_button_turn_on);
+        }
+    }
+}
diff --git a/src/com/android/settings/notification/modes/ZenModeFragment.java b/src/com/android/settings/notification/modes/ZenModeFragment.java
index 7f805ee..b8666bd 100644
--- a/src/com/android/settings/notification/modes/ZenModeFragment.java
+++ b/src/com/android/settings/notification/modes/ZenModeFragment.java
@@ -37,6 +37,7 @@
     protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
         List<AbstractPreferenceController> prefControllers = new ArrayList<>();
         prefControllers.add(new ZenModeHeaderController(context, "header", this, mBackend));
+        prefControllers.add(new ZenModeButtonPreferenceController(context, "activate", mBackend));
         prefControllers.add(new ZenModePeopleLinkPreferenceController(
                 context, "zen_mode_people", mBackend));
         prefControllers.add(new ZenModeOtherLinkPreferenceController(
diff --git a/src/com/android/settings/notification/modes/ZenModeFragmentBase.java b/src/com/android/settings/notification/modes/ZenModeFragmentBase.java
index 71fbc02..ff75afc 100644
--- a/src/com/android/settings/notification/modes/ZenModeFragmentBase.java
+++ b/src/com/android/settings/notification/modes/ZenModeFragmentBase.java
@@ -53,10 +53,25 @@
             if (!reloadMode(id)) {
                 Log.d(TAG, "Mode id " + id + " not found");
                 toastAndFinish();
+                return;
             }
         } else {
             Log.d(TAG, "Mode id required to set mode config settings");
             toastAndFinish();
+            return;
+        }
+        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
+                }
+            }
         }
     }
 
diff --git a/src/com/android/settings/notification/modes/ZenModeHeaderController.java b/src/com/android/settings/notification/modes/ZenModeHeaderController.java
index f791519..fc20710 100644
--- a/src/com/android/settings/notification/modes/ZenModeHeaderController.java
+++ b/src/com/android/settings/notification/modes/ZenModeHeaderController.java
@@ -51,6 +51,7 @@
         if (mFragment == null) {
             return;
         }
+        preference.setSelectable(false);
 
         if (mHeaderController == null) {
             final LayoutPreference pref = (LayoutPreference) preference;
diff --git a/src/com/android/settings/notification/modes/ZenModesFragmentBase.java b/src/com/android/settings/notification/modes/ZenModesFragmentBase.java
index 595f1d0..3f33b02 100644
--- a/src/com/android/settings/notification/modes/ZenModesFragmentBase.java
+++ b/src/com/android/settings/notification/modes/ZenModesFragmentBase.java
@@ -60,17 +60,15 @@
         mContext = context;
         mBackend = ZenModesBackend.getInstance(context);
         super.onAttach(context);
+        mSettingsObserver.register();
     }
 
     @Override
     public void onStart() {
         super.onStart();
-        updateZenModeState();
-        mSettingsObserver.register();
         if (isUiRestricted()) {
             if (isUiRestrictedByOnlyAdmin()) {
                 getPreferenceScreen().removeAll();
-                return;
             } else {
                 finish();
             }
@@ -84,8 +82,8 @@
     }
 
     @Override
-    public void onStop() {
-        super.onStop();
+    public void onDetach() {
+        super.onDetach();
         mSettingsObserver.unregister();
     }
 
diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeButtonPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeButtonPreferenceControllerTest.java
new file mode 100644
index 0000000..bda3843
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeButtonPreferenceControllerTest.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.notification.modes;
+
+import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.AutomaticZenRule;
+import android.app.Flags;
+import android.content.Context;
+import android.net.Uri;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.service.notification.ZenPolicy;
+import android.widget.Button;
+
+import com.android.settingslib.widget.LayoutPreference;
+
+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;
+
+@EnableFlags(Flags.FLAG_MODES_UI)
+@RunWith(RobolectricTestRunner.class)
+public final class ZenModeButtonPreferenceControllerTest {
+
+    private ZenModeButtonPreferenceController mController;
+
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+
+    private Context mContext;
+    @Mock
+    private ZenModesBackend mBackend;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+
+        mContext = RuntimeEnvironment.application;
+
+        mController = new ZenModeButtonPreferenceController(
+                mContext, "something", mBackend);
+    }
+
+    @Test
+    public void isAvailable_notIfAppOptsOut() {
+        ZenMode zenMode = new ZenMode("id",
+                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
+                .setType(AutomaticZenRule.TYPE_DRIVING)
+                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+                .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build())
+                .setManualInvocationAllowed(false)
+                .setEnabled(true)
+                .build(), false);
+        mController.setZenMode(zenMode);
+        assertThat(mController.isAvailable()).isFalse();
+    }
+
+    @Test
+    public void isAvailable_notIfModeDisabled() {
+        ZenMode zenMode = new ZenMode("id",
+                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
+                        .setType(AutomaticZenRule.TYPE_DRIVING)
+                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+                        .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build())
+                        .setManualInvocationAllowed(true)
+                        .setEnabled(false)
+                        .build(), false);
+        mController.setZenMode(zenMode);
+        assertThat(mController.isAvailable()).isFalse();
+    }
+
+    @Test
+    public void isAvailable_appOptedIn_modeEnabled() {
+        ZenMode zenMode = new ZenMode("id",
+                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
+                        .setType(AutomaticZenRule.TYPE_DRIVING)
+                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+                        .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build())
+                        .setManualInvocationAllowed(true)
+                        .setEnabled(true)
+                        .build(), false);
+        mController.setZenMode(zenMode);
+        assertThat(mController.isAvailable()).isTrue();
+    }
+
+    @Test
+    public void updateState_ruleActive() {
+        Button button = new Button(mContext);
+        LayoutPreference pref = mock(LayoutPreference.class);
+        when(pref.findViewById(anyInt())).thenReturn(button);
+        ZenMode zenMode = new ZenMode("id",
+                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
+                        .setType(AutomaticZenRule.TYPE_DRIVING)
+                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+                        .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build())
+                        .setManualInvocationAllowed(true)
+                        .setEnabled(true)
+                        .build(), true);
+        mController.updateZenMode(pref, zenMode);
+        assertThat(button.getText().toString()).contains("off");
+        assertThat(button.hasOnClickListeners()).isTrue();
+    }
+
+    @Test
+    public void updateState_ruleNotActive() {
+        Button button = new Button(mContext);
+        LayoutPreference pref = mock(LayoutPreference.class);
+        when(pref.findViewById(anyInt())).thenReturn(button);
+        ZenMode zenMode = new ZenMode("id",
+                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
+                        .setType(AutomaticZenRule.TYPE_DRIVING)
+                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+                        .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build())
+                        .setManualInvocationAllowed(true)
+                        .setEnabled(true)
+                        .build(), false);
+        mController.updateZenMode(pref, zenMode);
+        assertThat(button.getText().toString()).contains("on");
+        assertThat(button.hasOnClickListeners()).isTrue();
+    }
+
+    @Test
+    public void updateStateThenClick_ruleActive() {
+        Button button = new Button(mContext);
+        LayoutPreference pref = mock(LayoutPreference.class);
+        when(pref.findViewById(anyInt())).thenReturn(button);
+        ZenMode zenMode = new ZenMode("id",
+                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
+                        .setType(AutomaticZenRule.TYPE_DRIVING)
+                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+                        .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build())
+                        .setManualInvocationAllowed(true)
+                        .setEnabled(true)
+                        .build(), true);
+        mController.updateZenMode(pref, zenMode);
+
+        button.callOnClick();
+        verify(mBackend).deactivateMode(zenMode);
+    }
+
+    @Test
+    public void updateStateThenClick_ruleNotActive() {
+        Button button = new Button(mContext);
+        LayoutPreference pref = mock(LayoutPreference.class);
+        when(pref.findViewById(anyInt())).thenReturn(button);
+        ZenMode zenMode = new ZenMode("id",
+                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
+                        .setType(AutomaticZenRule.TYPE_DRIVING)
+                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+                        .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build())
+                        .setManualInvocationAllowed(true)
+                        .setEnabled(true)
+                        .build(), false);
+        mController.updateZenMode(pref, zenMode);
+
+        button.callOnClick();
+        verify(mBackend).activateMode(zenMode, null);
+    }
+}
\ No newline at end of file