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 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); + } +}