diff --git a/src/com/android/settings/notification/ZenModeSliceBuilder.java b/src/com/android/settings/notification/ZenModeSliceBuilder.java new file mode 100644 index 00000000000..0edf214179c --- /dev/null +++ b/src/com/android/settings/notification/ZenModeSliceBuilder.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2018 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; + +import static android.app.slice.Slice.EXTRA_TOGGLE_STATE; + +import android.annotation.ColorInt; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.Uri; +import android.provider.Settings; +import android.provider.SettingsSlicesContract; + +import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.settings.R; +import com.android.settings.SubSettings; +import com.android.settings.Utils; +import com.android.settings.search.DatabaseIndexingUtils; +import com.android.settings.slices.SettingsSliceProvider; +import com.android.settings.slices.SliceBroadcastReceiver; +import com.android.settings.slices.SliceBuilderUtils; + +import androidx.slice.Slice; +import androidx.slice.builders.ListBuilder; +import androidx.slice.builders.SliceAction; + +import android.support.v4.graphics.drawable.IconCompat; + +public class ZenModeSliceBuilder { + + private static final String TAG = "ZenModeSliceBuilder"; + + private static final String ZEN_MODE_KEY = "zen_mode"; + + /** + * Backing Uri for the Zen Mode Slice. + */ + public static final Uri ZEN_MODE_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SettingsSliceProvider.SLICE_AUTHORITY) + .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) + .appendPath(ZEN_MODE_KEY) + .build(); + + /** + * Action notifying a change on the Zen Mode Slice. + */ + public static final String ACTION_ZEN_MODE_SLICE_CHANGED = + "com.android.settings.notification.ZEN_MODE_CHANGED"; + + public static final IntentFilter INTENT_FILTER = new IntentFilter(); + + static { + INTENT_FILTER.addAction(NotificationManager.ACTION_NOTIFICATION_POLICY_CHANGED); + INTENT_FILTER.addAction(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED); + INTENT_FILTER.addAction(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED_INTERNAL); + } + + private ZenModeSliceBuilder() { + } + + /** + * Return a ZenMode Slice bound to {@link #ZEN_MODE_URI}. + *

+ * Note that you should register a listener for {@link #INTENT_FILTER} to get changes for + * ZenMode. + */ + public static Slice getSlice(Context context) { + final boolean isZenModeEnabled = isZenModeEnabled(context); + final CharSequence title = context.getText(R.string.zen_mode_settings_title); + @ColorInt final int color = Utils.getColorAccent(context); + final PendingIntent toggleAction = getBroadcastIntent(context); + final PendingIntent primaryAction = getPrimaryAction(context); + final SliceAction primarySliceAction = new SliceAction(primaryAction, + (IconCompat) null /* icon */, title); + final SliceAction toggleSliceAction = new SliceAction(toggleAction, null /* actionTitle */, + isZenModeEnabled); + + return new ListBuilder(context, ZEN_MODE_URI, ListBuilder.INFINITY) + .setAccentColor(color) + .addRow(b -> b + .setTitle(title) + .addEndItem(toggleSliceAction) + .setPrimaryAction(primarySliceAction)) + .build(); + } + + /** + * Update the current ZenMode status to the boolean value keyed by + * {@link android.app.slice.Slice#EXTRA_TOGGLE_STATE} on {@param intent}. + */ + public static void handleUriChange(Context context, Intent intent) { + final boolean zenModeOn = intent.getBooleanExtra(EXTRA_TOGGLE_STATE, false); + final int zenMode; + if (zenModeOn) { + zenMode = Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; + } else { + zenMode = Settings.Global.ZEN_MODE_OFF; + } + NotificationManager.from(context).setZenMode(zenMode, null /* conditionId */, TAG); + // Do not notifyChange on Uri. The service takes longer to update the current value than it + // does for the Slice to check the current value again. Let {@link SliceBroadcastRelay} + // handle it. + } + + private static boolean isZenModeEnabled(Context context) { + final NotificationManager manager = context.getSystemService(NotificationManager.class); + final int zenMode = manager.getZenMode(); + + switch (zenMode) { + case Settings.Global.ZEN_MODE_ALARMS: + case Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS: + case Settings.Global.ZEN_MODE_NO_INTERRUPTIONS: + return true; + case Settings.Global.ZEN_MODE_OFF: + default: + return false; + } + } + + private static PendingIntent getPrimaryAction(Context context) { + final String screenTitle = context.getText(R.string.zen_mode_settings_title).toString(); + final Uri contentUri = new Uri.Builder().appendPath(ZEN_MODE_KEY).build(); + final Intent intent = DatabaseIndexingUtils.buildSearchResultPageIntent(context, + ZenModeSettings.class.getName(), ZEN_MODE_KEY, screenTitle, + MetricsEvent.NOTIFICATION_ZEN_MODE) + .setClassName(context.getPackageName(), SubSettings.class.getName()) + .setData(contentUri); + + return PendingIntent.getActivity(context, 0 /* requestCode */, + intent, 0 /* flags */); + } + + private static PendingIntent getBroadcastIntent(Context context) { + final Intent intent = new Intent(ACTION_ZEN_MODE_SLICE_CHANGED) + .setClass(context, SliceBroadcastReceiver.class); + return PendingIntent.getBroadcast(context, 0 /* requestCode */, intent, + PendingIntent.FLAG_CANCEL_CURRENT); + } +} diff --git a/src/com/android/settings/slices/SettingsSliceProvider.java b/src/com/android/settings/slices/SettingsSliceProvider.java index 0ebf8c001cb..44f57f8d6f5 100644 --- a/src/com/android/settings/slices/SettingsSliceProvider.java +++ b/src/com/android/settings/slices/SettingsSliceProvider.java @@ -35,6 +35,7 @@ import android.util.Pair; import com.android.settings.overlay.FeatureFactory; import com.android.settings.wifi.WifiSliceBuilder; import com.android.settings.wifi.calling.WifiCallingSliceHelper; +import com.android.settings.notification.ZenModeSliceBuilder; import com.android.settingslib.SliceBroadcastRelay; import com.android.settingslib.utils.ThreadUtils; @@ -105,8 +106,6 @@ public class SettingsSliceProvider extends SliceProvider { public static final String EXTRA_SLICE_PLATFORM_DEFINED = "com.android.settings.slice.extra.platform"; - // TODO -- Associate slice URI with search result instead of separate hardcoded thing - @VisibleForTesting SlicesDatabaseAccessor mSlicesDatabaseAccessor; @@ -147,6 +146,10 @@ public class SettingsSliceProvider extends SliceProvider { // TODO (b/) Register IntentFilters for database entries. mRegisteredUris.add(sliceUri); return; + } else if (ZenModeSliceBuilder.ZEN_MODE_URI.equals(sliceUri)) { + registerIntentToUri(ZenModeSliceBuilder.INTENT_FILTER, sliceUri); + mRegisteredUris.add(sliceUri); + return; } // Start warming the slice, we expect someone will want it soon. @@ -173,6 +176,8 @@ public class SettingsSliceProvider extends SliceProvider { .createWifiCallingSlice(sliceUri); } else if (WifiSliceBuilder.WIFI_URI.equals(sliceUri)) { return WifiSliceBuilder.getSlice(getContext()); + } else if (ZenModeSliceBuilder.ZEN_MODE_URI.equals(sliceUri)) { + return ZenModeSliceBuilder.getSlice(getContext()); } SliceData cachedSliceData = mSliceWeakDataCache.get(sliceUri); @@ -311,11 +316,15 @@ public class SettingsSliceProvider extends SliceProvider { } private List getSpecialCasePlatformUris() { - return Arrays.asList(WifiSliceBuilder.WIFI_URI); + return Arrays.asList( + WifiSliceBuilder.WIFI_URI + ); } private List getSpecialCaseOemUris() { - return new ArrayList<>(); + return Arrays.asList( + ZenModeSliceBuilder.ZEN_MODE_URI + ); } @VisibleForTesting diff --git a/src/com/android/settings/slices/SliceBroadcastReceiver.java b/src/com/android/settings/slices/SliceBroadcastReceiver.java index 3e349ff9b7e..b9f3b0051ea 100644 --- a/src/com/android/settings/slices/SliceBroadcastReceiver.java +++ b/src/com/android/settings/slices/SliceBroadcastReceiver.java @@ -16,6 +16,7 @@ package com.android.settings.slices; +import static com.android.settings.notification.ZenModeSliceBuilder.ACTION_ZEN_MODE_SLICE_CHANGED; import static com.android.settings.slices.SettingsSliceProvider.ACTION_SLIDER_CHANGED; import static com.android.settings.slices.SettingsSliceProvider.ACTION_TOGGLE_CHANGED; import static com.android.settings.slices.SettingsSliceProvider.EXTRA_SLICE_KEY; @@ -37,6 +38,7 @@ import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.core.BasePreferenceController; import com.android.settings.core.SliderPreferenceController; import com.android.settings.core.TogglePreferenceController; +import com.android.settings.notification.ZenModeSliceBuilder; import com.android.settings.overlay.FeatureFactory; import com.android.settings.wifi.WifiSliceBuilder; import com.android.settingslib.SliceBroadcastRelay; @@ -48,9 +50,6 @@ public class SliceBroadcastReceiver extends BroadcastReceiver { private static String TAG = "SettSliceBroadcastRec"; - /** - * TODO (b/) move wifi action into generalized case. - */ @Override public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); @@ -76,6 +75,9 @@ public class SliceBroadcastReceiver extends BroadcastReceiver { .getNewWifiCallingSliceHelper(context) .handleWifiCallingChanged(intent); break; + case ACTION_ZEN_MODE_SLICE_CHANGED: + ZenModeSliceBuilder.handleUriChange(context, intent); + break; default: final String uriString = intent.getStringExtra(SliceBroadcastRelay.EXTRA_URI); if (!TextUtils.isEmpty(uriString)) { diff --git a/src/com/android/settings/wifi/WifiSliceBuilder.java b/src/com/android/settings/wifi/WifiSliceBuilder.java index 2ebba67c8f5..96d1b82b86a 100644 --- a/src/com/android/settings/wifi/WifiSliceBuilder.java +++ b/src/com/android/settings/wifi/WifiSliceBuilder.java @@ -16,6 +16,7 @@ package com.android.settings.wifi; +import static android.app.slice.Slice.EXTRA_TOGGLE_STATE; import static android.provider.SettingsSlicesContract.KEY_WIFI; import android.annotation.ColorInt; @@ -79,8 +80,7 @@ public class WifiSliceBuilder { /** * Return a Wifi Slice bound to {@link #WIFI_URI}. *

- * Note that you should register a listener with {@link #registerIntentFilter(Context, Uri)} - * to get changes from Wifi. + * Note that you should register a listener for {@link #INTENT_FILTER} to get changes for Wifi. */ public static Slice getSlice(Context context) { final boolean isWifiEnabled = isWifiEnabled(context); @@ -111,7 +111,7 @@ public class WifiSliceBuilder { */ public static void handleUriChange(Context context, Intent intent) { final WifiManager wifiManager = context.getSystemService(WifiManager.class); - final boolean newState = intent.getBooleanExtra(android.app.slice.Slice.EXTRA_TOGGLE_STATE, + final boolean newState = intent.getBooleanExtra(EXTRA_TOGGLE_STATE, wifiManager.isWifiEnabled()); wifiManager.setWifiEnabled(newState); // Do not notifyChange on Uri. The service takes longer to update the current value than it diff --git a/tests/robotests/src/com/android/settings/notification/ZenModeSliceBuilderTest.java b/tests/robotests/src/com/android/settings/notification/ZenModeSliceBuilderTest.java new file mode 100644 index 00000000000..ba5ab82691f --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/ZenModeSliceBuilderTest.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2018 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; + +import static android.app.slice.Slice.EXTRA_TOGGLE_STATE; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; + +import android.app.NotificationManager; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.provider.Settings; +import android.support.v4.graphics.drawable.IconCompat; + +import com.android.settings.R; +import com.android.settings.testutils.SettingsRobolectricTestRunner; +import com.android.settings.testutils.SliceTester; +import com.android.settings.testutils.shadow.ShadowNotificationManager; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +import java.util.List; + +import androidx.slice.Slice; +import androidx.slice.SliceItem; +import androidx.slice.SliceMetadata; +import androidx.slice.SliceProvider; +import androidx.slice.core.SliceAction; +import androidx.slice.widget.SliceLiveData; + +@Config(shadows = ShadowNotificationManager.class) +@RunWith(SettingsRobolectricTestRunner.class) +public class ZenModeSliceBuilderTest { + + private Context mContext; + + @Before + public void setUp() { + mContext = spy(RuntimeEnvironment.application); + + // Prevent crash in SliceMetadata. + Resources resources = spy(mContext.getResources()); + doReturn(60).when(resources).getDimensionPixelSize(anyInt()); + doReturn(resources).when(mContext).getResources(); + + // Set-up specs for SliceMetadata. + SliceProvider.setSpecs(SliceLiveData.SUPPORTED_SPECS); + } + + @Test + public void getZenModeSlice_correctSliceContent() { + final Slice dndSlice = ZenModeSliceBuilder.getSlice(mContext); + final SliceMetadata metadata = SliceMetadata.from(mContext, dndSlice); + + final List toggles = metadata.getToggles(); + assertThat(toggles).hasSize(1); + + final SliceAction primaryAction = metadata.getPrimaryAction(); + assertThat(primaryAction.getIcon()).isNull(); + + final List sliceItems = dndSlice.getItems(); + SliceTester.assertTitle(sliceItems, mContext.getString(R.string.zen_mode_settings_title)); + } + + @Test + public void handleUriChange_turnOn_zenModeTurnsOn() { + final Intent intent = new Intent(); + intent.putExtra(EXTRA_TOGGLE_STATE, true); + NotificationManager.from(mContext).setZenMode(Settings.Global.ZEN_MODE_OFF, null, ""); + + ZenModeSliceBuilder.handleUriChange(mContext, intent); + + final int zenMode = NotificationManager.from(mContext).getZenMode(); + assertThat(zenMode).isEqualTo(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS); + } + + @Test + public void handleUriChange_turnOff_zenModeTurnsOff() { + final Intent intent = new Intent(); + intent.putExtra(EXTRA_TOGGLE_STATE, false); + NotificationManager.from(mContext).setZenMode( + Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, ""); + + ZenModeSliceBuilder.handleUriChange(mContext, intent); + + final int zenMode = NotificationManager.from(mContext).getZenMode(); + assertThat(zenMode).isEqualTo(Settings.Global.ZEN_MODE_OFF); + } +} diff --git a/tests/robotests/src/com/android/settings/slices/SettingsSliceProviderTest.java b/tests/robotests/src/com/android/settings/slices/SettingsSliceProviderTest.java index 60fb5f9c685..9d8e6b4b918 100644 --- a/tests/robotests/src/com/android/settings/slices/SettingsSliceProviderTest.java +++ b/tests/robotests/src/com/android/settings/slices/SettingsSliceProviderTest.java @@ -33,6 +33,7 @@ import android.os.StrictMode; import android.provider.SettingsSlicesContract; import com.android.settings.wifi.WifiSliceBuilder; +import com.android.settings.notification.ZenModeSliceBuilder; import com.android.settings.testutils.DatabaseTestUtils; import com.android.settings.testutils.FakeToggleController; import com.android.settings.testutils.SettingsRobolectricTestRunner; @@ -78,6 +79,10 @@ public class SettingsSliceProviderTest { WifiSliceBuilder.WIFI_URI ); + private static final List SPECIAL_CASE_OEM_URIS = Arrays.asList( + ZenModeSliceBuilder.ZEN_MODE_URI + ); + @Before public void setUp() { mContext = spy(RuntimeEnvironment.application); @@ -221,6 +226,7 @@ public class SettingsSliceProviderTest { .build(); final Collection descendants = mProvider.onGetSliceDescendants(uri); + descendants.removeAll(SPECIAL_CASE_OEM_URIS); assertThat(descendants).isEmpty(); } @@ -248,6 +254,7 @@ public class SettingsSliceProviderTest { .build(); final Collection descendants = mProvider.onGetSliceDescendants(uri); + descendants.removeAll(SPECIAL_CASE_OEM_URIS); assertThat(descendants).isEmpty(); } @@ -261,16 +268,18 @@ public class SettingsSliceProviderTest { .authority(SettingsSliceProvider.SLICE_AUTHORITY) .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) .build(); - final Uri expectedUri = new Uri.Builder() + final Collection expectedUris = new HashSet<>(); + expectedUris.addAll(SPECIAL_CASE_OEM_URIS); + expectedUris.add(new Uri.Builder() .scheme(SCHEME_CONTENT) .authority(SettingsSliceProvider.SLICE_AUTHORITY) .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) .appendPath(key) - .build(); + .build()); final Collection descendants = mProvider.onGetSliceDescendants(uri); - assertThat(descendants).containsExactly(expectedUri); + assertThat(descendants).containsExactlyElementsIn(expectedUris); } @Test @@ -281,16 +290,18 @@ public class SettingsSliceProviderTest { .scheme(SCHEME_CONTENT) .authority(SettingsSliceProvider.SLICE_AUTHORITY) .build(); - final Uri expectedUri = new Uri.Builder() + final Collection expectedUris = new HashSet<>(); + expectedUris.addAll(SPECIAL_CASE_OEM_URIS); + expectedUris.add(new Uri.Builder() .scheme(SCHEME_CONTENT) .authority(SettingsSliceProvider.SLICE_AUTHORITY) .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) .appendPath(key) - .build(); + .build()); final Collection descendants = mProvider.onGetSliceDescendants(uri); - assertThat(descendants).containsExactly(expectedUri); + assertThat(descendants).containsExactlyElementsIn(expectedUris); } @Test @@ -349,6 +360,7 @@ public class SettingsSliceProviderTest { .build(); final Collection expectedUris = new HashSet<>(); expectedUris.addAll(SPECIAL_CASE_PLATFORM_URIS); + expectedUris.addAll(SPECIAL_CASE_OEM_URIS); expectedUris.add(new Uri.Builder() .scheme(SCHEME_CONTENT) .authority(SettingsSlicesContract.AUTHORITY) diff --git a/tests/robotests/src/com/android/settings/wifi/WifiSliceBuilderTest.java b/tests/robotests/src/com/android/settings/wifi/WifiSliceBuilderTest.java index f1ac5545d30..e4a3dfb4fab 100644 --- a/tests/robotests/src/com/android/settings/wifi/WifiSliceBuilderTest.java +++ b/tests/robotests/src/com/android/settings/wifi/WifiSliceBuilderTest.java @@ -72,7 +72,6 @@ public class WifiSliceBuilderTest { final Slice wifiSlice = WifiSliceBuilder.getSlice(mContext); final SliceMetadata metadata = SliceMetadata.from(mContext, wifiSlice); - final List toggles = metadata.getToggles(); assertThat(toggles).hasSize(1);