diff --git a/res/values/strings.xml b/res/values/strings.xml
index e19cfdda14c..8521fefcac5 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -10137,6 +10137,12 @@
Got it
+
+ Try Dark theme
+
+
+ Helps extend battery life
+
Quick settings developer tiles
diff --git a/src/com/android/settings/homepage/contextualcards/SettingsContextualCardProvider.java b/src/com/android/settings/homepage/contextualcards/SettingsContextualCardProvider.java
index 385f8cda010..1494293341f 100644
--- a/src/com/android/settings/homepage/contextualcards/SettingsContextualCardProvider.java
+++ b/src/com/android/settings/homepage/contextualcards/SettingsContextualCardProvider.java
@@ -78,6 +78,12 @@ public class SettingsContextualCardProvider extends ContextualCardProvider {
.setCardName(CustomSliceRegistry.FACE_ENROLL_SLICE_URI.toString())
.setCardCategory(ContextualCard.Category.DEFAULT)
.build();
+ final ContextualCard darkThemeCard =
+ ContextualCard.newBuilder()
+ .setSliceUri(CustomSliceRegistry.DARK_THEME_SLICE_URI.toString())
+ .setCardName(CustomSliceRegistry.DARK_THEME_SLICE_URI.toString())
+ .setCardCategory(ContextualCard.Category.IMPORTANT)
+ .build();
final ContextualCardList cards = ContextualCardList.newBuilder()
.addCard(wifiCard)
.addCard(connectedDeviceCard)
@@ -86,6 +92,7 @@ public class SettingsContextualCardProvider extends ContextualCardProvider {
.addCard(notificationChannelCard)
.addCard(contextualAdaptiveSleepCard)
.addCard(contextualFaceSettingsCard)
+ .addCard(darkThemeCard)
.build();
return cards;
diff --git a/src/com/android/settings/homepage/contextualcards/slices/DarkThemeSlice.java b/src/com/android/settings/homepage/contextualcards/slices/DarkThemeSlice.java
new file mode 100644
index 00000000000..36a39802945
--- /dev/null
+++ b/src/com/android/settings/homepage/contextualcards/slices/DarkThemeSlice.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2019 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.homepage.contextualcards.slices;
+
+import static androidx.slice.builders.ListBuilder.ICON_IMAGE;
+
+import android.annotation.ColorInt;
+import android.app.PendingIntent;
+import android.app.UiModeManager;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.BatteryManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.slice.Slice;
+import androidx.slice.builders.ListBuilder;
+import androidx.slice.builders.SliceAction;
+
+import com.android.settings.R;
+import com.android.settings.Utils;
+import com.android.settings.overlay.FeatureFactory;
+import com.android.settings.slices.CustomSliceRegistry;
+import com.android.settings.slices.CustomSliceable;
+
+public class DarkThemeSlice implements CustomSliceable {
+ private static final String TAG = "DarkThemeSlice";
+ private static final int BATTERY_LEVEL_THRESHOLD = 50;
+ private static final int DELAY_TIME_EXECUTING_DARK_THEME = 200;
+
+ // Keep the slice even Dark theme mode changed when it is on HomePage
+ @VisibleForTesting
+ static boolean sKeepSliceShow;
+ @VisibleForTesting
+ static long sActiveUiSession = -1000;
+
+ private final Context mContext;
+ private final UiModeManager mUiModeManager;
+
+ public DarkThemeSlice(Context context) {
+ mContext = context;
+ mUiModeManager = context.getSystemService(UiModeManager.class);
+ }
+
+ @Override
+ public Slice getSlice() {
+ final long currentUiSession = FeatureFactory.getFactory(mContext)
+ .getSlicesFeatureProvider().getUiSessionToken();
+ if (currentUiSession != sActiveUiSession) {
+ sActiveUiSession = currentUiSession;
+ sKeepSliceShow = false;
+ }
+ if (!sKeepSliceShow && !isAvailable(mContext)) {
+ return null;
+ }
+ sKeepSliceShow = true;
+ final PendingIntent toggleAction = getBroadcastIntent(mContext);
+ @ColorInt final int color = Utils.getColorAccentDefaultColor(mContext);
+ final IconCompat icon =
+ IconCompat.createWithResource(mContext, R.drawable.dark_theme);
+ final boolean isChecked = mUiModeManager.getNightMode() == UiModeManager.MODE_NIGHT_YES;
+ return new ListBuilder(mContext, CustomSliceRegistry.DARK_THEME_SLICE_URI,
+ ListBuilder.INFINITY)
+ .setAccentColor(color)
+ .addRow(new ListBuilder.RowBuilder()
+ .setTitle(mContext.getText(R.string.dark_theme_slice_title))
+ .setTitleItem(icon, ICON_IMAGE)
+ .setSubtitle(mContext.getText(R.string.dark_theme_slice_subtitle))
+ .setPrimaryAction(
+ SliceAction.createToggle(toggleAction, null /* actionTitle */,
+ isChecked)))
+ .build();
+ }
+
+ @Override
+ public Uri getUri() {
+ return CustomSliceRegistry.DARK_THEME_SLICE_URI;
+ }
+
+ @Override
+ public void onNotifyChange(Intent intent) {
+ final boolean isChecked = intent.getBooleanExtra(android.app.slice.Slice.EXTRA_TOGGLE_STATE,
+ false);
+ // make toggle transition more smooth before dark theme takes effect
+ new Handler(Looper.getMainLooper()).postDelayed(() -> {
+ mUiModeManager.setNightMode(
+ isChecked ? UiModeManager.MODE_NIGHT_YES : UiModeManager.MODE_NIGHT_NO);
+ }, DELAY_TIME_EXECUTING_DARK_THEME);
+ }
+
+ @Override
+ public Intent getIntent() {
+ return null;
+ }
+
+ @VisibleForTesting
+ boolean isAvailable(Context context) {
+ // checking dark theme mode.
+ if (mUiModeManager.getNightMode() == UiModeManager.MODE_NIGHT_YES) {
+ return false;
+ }
+
+ // checking the current battery level
+ final BatteryManager batteryManager = context.getSystemService(BatteryManager.class);
+ final int level = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY);
+ Log.d(TAG, "battery level=" + level);
+
+ return level <= BATTERY_LEVEL_THRESHOLD;
+ }
+}
diff --git a/src/com/android/settings/slices/CustomSliceRegistry.java b/src/com/android/settings/slices/CustomSliceRegistry.java
index 946a9d3dfda..42d81e95598 100644
--- a/src/com/android/settings/slices/CustomSliceRegistry.java
+++ b/src/com/android/settings/slices/CustomSliceRegistry.java
@@ -37,6 +37,7 @@ import com.android.settings.homepage.contextualcards.slices.BatteryFixSlice;
import com.android.settings.homepage.contextualcards.slices.BluetoothDevicesSlice;
import com.android.settings.homepage.contextualcards.slices.ContextualAdaptiveSleepSlice;
import com.android.settings.homepage.contextualcards.slices.ContextualNotificationChannelSlice;
+import com.android.settings.homepage.contextualcards.slices.DarkThemeSlice;
import com.android.settings.homepage.contextualcards.slices.FaceSetupSlice;
import com.android.settings.homepage.contextualcards.slices.LowStorageSlice;
import com.android.settings.homepage.contextualcards.slices.NotificationChannelSlice;
@@ -342,6 +343,16 @@ public class CustomSliceRegistry {
.appendPath("media_output_indicator")
.build();
+ /**
+ * Backing Uri for the Dark theme Slice.
+ */
+ public static final Uri DARK_THEME_SLICE_URI = new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(SettingsSliceProvider.SLICE_AUTHORITY)
+ .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION)
+ .appendPath("dark_theme")
+ .build();
+
@VisibleForTesting
static final Map> sUriToSlice;
@@ -367,6 +378,7 @@ public class CustomSliceRegistry {
sUriToSlice.put(NOTIFICATION_CHANNEL_SLICE_URI, NotificationChannelSlice.class);
sUriToSlice.put(STORAGE_SLICE_URI, StorageSlice.class);
sUriToSlice.put(WIFI_SLICE_URI, WifiSlice.class);
+ sUriToSlice.put(DARK_THEME_SLICE_URI, DarkThemeSlice.class);
}
public static Class extends CustomSliceable> getSliceClassByUri(Uri uri) {
diff --git a/tests/robotests/src/com/android/settings/homepage/contextualcards/slices/DarkThemeSliceTest.java b/tests/robotests/src/com/android/settings/homepage/contextualcards/slices/DarkThemeSliceTest.java
new file mode 100644
index 00000000000..bb213329b79
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/homepage/contextualcards/slices/DarkThemeSliceTest.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2019 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.homepage.contextualcards.slices;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.app.UiModeManager;
+import android.content.Context;
+import android.net.Uri;
+import android.os.BatteryManager;
+
+import androidx.slice.Slice;
+import androidx.slice.SliceMetadata;
+import androidx.slice.SliceProvider;
+import androidx.slice.widget.SliceLiveData;
+
+import com.android.settings.R;
+import com.android.settings.slices.CustomSliceRegistry;
+import com.android.settings.slices.SlicesFeatureProviderImpl;
+import com.android.settings.testutils.FakeFeatureFactory;
+
+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;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(RobolectricTestRunner.class)
+public class DarkThemeSliceTest {
+ @Mock
+ private UiModeManager mUiModeManager;
+ @Mock
+ private BatteryManager mBatteryManager;
+
+ private Context mContext;
+ private DarkThemeSlice mDarkThemeSlice;
+ private FakeFeatureFactory mFeatureFactory;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mContext = spy(RuntimeEnvironment.application);
+ mFeatureFactory = FakeFeatureFactory.setupForTest();
+ mFeatureFactory.slicesFeatureProvider = new SlicesFeatureProviderImpl();
+ mFeatureFactory.slicesFeatureProvider.newUiSession();
+ doReturn(mUiModeManager).when(mContext).getSystemService(UiModeManager.class);
+
+ // Set-up specs for SliceMetadata.
+ SliceProvider.setSpecs(SliceLiveData.SUPPORTED_SPECS);
+ mDarkThemeSlice = new DarkThemeSlice(mContext);
+ mDarkThemeSlice.sKeepSliceShow = false;
+ }
+
+ @Test
+ public void getUri_shouldBeDarkThemeSliceUri() {
+ final Uri uri = mDarkThemeSlice.getUri();
+
+ assertThat(uri).isEqualTo(CustomSliceRegistry.DARK_THEME_SLICE_URI);
+ }
+
+ @Test
+ public void isAvailable_inDarkThemeMode_returnFalse() {
+ when(mUiModeManager.getNightMode()).thenReturn(UiModeManager.MODE_NIGHT_YES);
+
+ assertThat(mDarkThemeSlice.isAvailable(mContext)).isFalse();
+ }
+
+ @Test
+ public void isAvailable_nonDarkThemeBatteryCapacityEq100_returnFalse() {
+ setBatteryCapacityLevel(100);
+
+ assertThat(mDarkThemeSlice.isAvailable(mContext)).isFalse();
+ }
+
+ @Test
+ public void isAvailable_nonDarkThemeBatteryCapacityLt50_returnTrue() {
+ setBatteryCapacityLevel(40);
+
+ assertThat(mDarkThemeSlice.isAvailable(mContext)).isTrue();
+ }
+
+ @Test
+ public void getSlice_notAvailable_returnNull() {
+ when(mUiModeManager.getNightMode()).thenReturn(UiModeManager.MODE_NIGHT_YES);
+
+ assertThat(mDarkThemeSlice.getSlice()).isNull();
+ }
+
+ @Test
+ public void getSlice_newSession_notAvailable_returnNull() {
+ // previous displayed: yes
+ mDarkThemeSlice.sKeepSliceShow = true;
+ // Session: use original value + 1 to become a new session
+ mDarkThemeSlice.sActiveUiSession =
+ mFeatureFactory.slicesFeatureProvider.getUiSessionToken() + 1;
+
+ when(mUiModeManager.getNightMode()).thenReturn(UiModeManager.MODE_NIGHT_YES);
+
+ assertThat(mDarkThemeSlice.getSlice()).isNull();
+ }
+
+ @Test
+ public void getSlice_previouslyDisplayed_isAvailable_returnSlice() {
+ mDarkThemeSlice.sActiveUiSession =
+ mFeatureFactory.slicesFeatureProvider.getUiSessionToken();
+ mDarkThemeSlice.sKeepSliceShow = true;
+ setBatteryCapacityLevel(40);
+
+ assertThat(mDarkThemeSlice.getSlice()).isNotNull();
+ }
+
+ @Test
+ public void getSlice_isAvailable_returnSlice() {
+ setBatteryCapacityLevel(40);
+
+ assertThat(mDarkThemeSlice.getSlice()).isNotNull();
+ }
+
+ @Test
+ public void getSlice_isAvailable_showTitleSubtitle() {
+ setBatteryCapacityLevel(40);
+
+ final Slice slice = mDarkThemeSlice.getSlice();
+ final SliceMetadata metadata = SliceMetadata.from(mContext, slice);
+ assertThat(metadata.getTitle()).isEqualTo(
+ mContext.getString(R.string.dark_theme_slice_title));
+ assertThat(metadata.getSubtitle()).isEqualTo(
+ mContext.getString(R.string.dark_theme_slice_subtitle));
+ }
+
+ private void setBatteryCapacityLevel(int power_level) {
+ when(mUiModeManager.getNightMode()).thenReturn(UiModeManager.MODE_NIGHT_NO);
+ doReturn(mBatteryManager).when(mContext).getSystemService(BatteryManager.class);
+ when(mBatteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY))
+ .thenReturn(power_level);
+ }
+}