diff --git a/res/values/arrays.xml b/res/values/arrays.xml index 44403583ef1..396a485ae77 100644 --- a/res/values/arrays.xml +++ b/res/values/arrays.xml @@ -106,6 +106,8 @@ @string/dark_ui_auto_mode_never @string/dark_ui_auto_mode_auto + + @string/dark_ui_auto_mode_custom Will never turn on automatically - + Will turn on automatically at %1$s Will turn on automatically at sunset @@ -2804,9 +2804,11 @@ Schedule None - + Turns on from sunset to sunrise - + + Turns on at custom time + Status Off / %1$s @@ -2814,12 +2816,20 @@ Will never turn on automatically Will turn on automatically at sunset + + Will turn on automatically at %1$s On / %1$s Will never turn off automatically Will turn off automatically at sunrise + + Will turn off automatically at %1$s + + Turn on until %1$s + + Turn off until %1$s Dark theme uses a black background to help keep battery alive longer on some screens. Dark theme schedules wait to turn on until your screen is off. diff --git a/res/xml/dark_mode_settings.xml b/res/xml/dark_mode_settings.xml index 9247a0c3788..cf911861229 100644 --- a/res/xml/dark_mode_settings.xml +++ b/res/xml/dark_mode_settings.xml @@ -30,6 +30,16 @@ settings:controller="com.android.settings.display.darkmode.DarkModeScheduleSelectorController" settings:keywords="@string/keywords_dark_ui_mode"/> + + + + { + final LocalTime time = LocalTime.of(hourOfDay, minute); + if (TextUtils.equals(getPreferenceKey(), START_TIME_KEY)) { + mUiModeManager.setCustomNightModeStart(time); + } else { + mUiModeManager.setCustomNightModeEnd(time); + } + if (mFragmet != null) { + mFragmet.refresh(); + } + }, initialTime.getHour(), initialTime.getMinute(), mFormat.is24HourFormat()); + } + + @Override + protected void refreshSummary(Preference preference) { + if (mUiModeManager.getNightMode() != MODE_NIGHT_CUSTOM) { + preference.setVisible(false); + return; + } + preference.setVisible(true); + final LocalTime time; + if (TextUtils.equals(getPreferenceKey(), START_TIME_KEY)) { + time = mUiModeManager.getCustomNightModeStart(); + } else { + time = mUiModeManager.getCustomNightModeEnd(); + } + preference.setSummary(mFormat.of(time)); + } +} diff --git a/src/com/android/settings/display/darkmode/DarkModePreference.java b/src/com/android/settings/display/darkmode/DarkModePreference.java index c5fbdedf579..7fb80235730 100644 --- a/src/com/android/settings/display/darkmode/DarkModePreference.java +++ b/src/com/android/settings/display/darkmode/DarkModePreference.java @@ -22,6 +22,8 @@ import android.util.AttributeSet; import com.android.settings.R; import com.android.settings.widget.MasterSwitchPreference; +import java.time.LocalTime; + /** * component for the display settings dark ui summary*/ public class DarkModePreference extends MasterSwitchPreference { @@ -31,11 +33,14 @@ public class DarkModePreference extends MasterSwitchPreference { private PowerManager mPowerManager; private Runnable mCallback; + private TimeFormatter mFormat; + public DarkModePreference(Context context, AttributeSet attrs) { super(context, attrs); mDarkModeObserver = new DarkModeObserver(context); mUiModeManager = context.getSystemService(UiModeManager.class); mPowerManager = context.getSystemService(PowerManager.class); + mFormat = new TimeFormatter(context); mCallback = () -> { final boolean batterySaver = mPowerManager.isPowerSaveMode(); final boolean active = (getContext().getResources().getConfiguration().uiMode @@ -60,21 +65,30 @@ public class DarkModePreference extends MasterSwitchPreference { private void updateSummary(boolean batterySaver, boolean active) { if (batterySaver) { - final int stringId = active ? R.string.dark_ui_mode_disabled_summary_dark_theme_on + final int stringId = active + ? R.string.dark_ui_mode_disabled_summary_dark_theme_on : R.string.dark_ui_mode_disabled_summary_dark_theme_off; setSummary(getContext().getString(stringId)); return; } - final boolean auto = mUiModeManager.getNightMode() == UiModeManager.MODE_NIGHT_AUTO; - + final int mode = mUiModeManager.getNightMode(); String detail; - if (active) { - detail = getContext().getString(auto + + if (mode == UiModeManager.MODE_NIGHT_AUTO) { + detail = getContext().getString(active ? R.string.dark_ui_summary_on_auto_mode_auto - : R.string.dark_ui_summary_on_auto_mode_never); + : R.string.dark_ui_summary_off_auto_mode_auto); + } else if (mode == UiModeManager.MODE_NIGHT_CUSTOM) { + final LocalTime time = active + ? mUiModeManager.getCustomNightModeEnd() + : mUiModeManager.getCustomNightModeStart(); + final String timeStr = mFormat.of(time); + detail = getContext().getString(active + ? R.string.dark_ui_summary_on_auto_mode_custom + : R.string.dark_ui_summary_off_auto_mode_custom, timeStr); } else { - detail = getContext().getString(auto - ? R.string.dark_ui_summary_off_auto_mode_auto + detail = getContext().getString(active + ? R.string.dark_ui_summary_on_auto_mode_never : R.string.dark_ui_summary_off_auto_mode_never); } String summary = getContext().getString(active diff --git a/src/com/android/settings/display/darkmode/DarkModeScheduleSelectorController.java b/src/com/android/settings/display/darkmode/DarkModeScheduleSelectorController.java index 9a867570885..ea7fc036ee4 100644 --- a/src/com/android/settings/display/darkmode/DarkModeScheduleSelectorController.java +++ b/src/com/android/settings/display/darkmode/DarkModeScheduleSelectorController.java @@ -63,8 +63,17 @@ public class DarkModeScheduleSelectorController extends BasePreferenceController } private int getCurrentMode() { - final int resId = mUiModeManager.getNightMode() == UiModeManager.MODE_NIGHT_AUTO - ? R.string.dark_ui_auto_mode_auto : R.string.dark_ui_auto_mode_never; + int resId; + switch (mUiModeManager.getNightMode()) { + case UiModeManager.MODE_NIGHT_AUTO: + resId = R.string.dark_ui_auto_mode_auto; + break; + case UiModeManager.MODE_NIGHT_CUSTOM: + resId = R.string.dark_ui_auto_mode_custom; + break; + default: + resId = R.string.dark_ui_auto_mode_never; + } return mPreference.findIndexOfValue(mContext.getString(resId)); } @@ -85,6 +94,9 @@ public class DarkModeScheduleSelectorController extends BasePreferenceController } else if (mCurrentMode == mPreference.findIndexOfValue( mContext.getString(R.string.dark_ui_auto_mode_auto))) { mUiModeManager.setNightMode(UiModeManager.MODE_NIGHT_AUTO); + } else if (mCurrentMode == mPreference.findIndexOfValue( + mContext.getString(R.string.dark_ui_auto_mode_custom))) { + mUiModeManager.setNightMode(UiModeManager.MODE_NIGHT_CUSTOM); } return true; } diff --git a/src/com/android/settings/display/darkmode/DarkModeSettingsFragment.java b/src/com/android/settings/display/darkmode/DarkModeSettingsFragment.java index 62933166e26..97ee221e140 100644 --- a/src/com/android/settings/display/darkmode/DarkModeSettingsFragment.java +++ b/src/com/android/settings/display/darkmode/DarkModeSettingsFragment.java @@ -14,14 +14,23 @@ package com.android.settings.display.darkmode; +import android.app.Dialog; +import android.app.TimePickerDialog; +import android.app.UiModeManager; import android.content.Context; import android.os.Bundle; import android.app.settings.SettingsEnums; +import androidx.preference.Preference; import com.android.settings.R; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.search.SearchIndexable; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.List; + /** * Settings screen for Dark UI Mode */ @@ -29,15 +38,20 @@ import com.android.settingslib.search.SearchIndexable; public class DarkModeSettingsFragment extends DashboardFragment { private static final String TAG = "DarkModeSettingsFragment"; + private static final String DARK_THEME_END_TIME = "dark_theme_end_time"; + private static final String DARK_THEME_START_TIME = "dark_theme_start_time"; private DarkModeObserver mContentObserver; + private DarkModeCustomPreferenceController mCustomStartController; + private DarkModeCustomPreferenceController mCustomEndController; private Runnable mCallback = () -> { updatePreferenceStates(); }; + private static final int DIALOG_START_TIME = 0; + private static final int DIALOG_END_TIME = 1; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - final Context context = getContext(); mContentObserver = new DarkModeObserver(context); } @@ -49,6 +63,18 @@ public class DarkModeSettingsFragment extends DashboardFragment { mContentObserver.subscribe(mCallback); } + @Override + protected List createPreferenceControllers(Context context) { + List controllers = new ArrayList(2); + mCustomStartController = new DarkModeCustomPreferenceController(getContext(), + DARK_THEME_START_TIME, this); + mCustomEndController = new DarkModeCustomPreferenceController(getContext(), + DARK_THEME_END_TIME, this); + controllers.add(mCustomStartController); + controllers.add(mCustomEndController); + return controllers; + } + @Override public void onStop() { super.onStop(); @@ -56,6 +82,34 @@ public class DarkModeSettingsFragment extends DashboardFragment { mContentObserver.unsubscribe(); } + @Override + public boolean onPreferenceTreeClick(Preference preference) { + if (DARK_THEME_END_TIME.equals(preference.getKey())) { + showDialog(DIALOG_END_TIME); + return true; + } else if (DARK_THEME_START_TIME.equals(preference.getKey())) { + showDialog(DIALOG_START_TIME); + return true; + } + return super.onPreferenceTreeClick(preference); + } + + public void refresh() { + this.updatePreferenceStates(); + } + + @Override + public Dialog onCreateDialog(final int dialogId) { + if (dialogId == DIALOG_START_TIME || dialogId == DIALOG_END_TIME) { + if (dialogId == DIALOG_START_TIME) { + return mCustomStartController.getDialog(); + } else { + return mCustomEndController.getDialog(); + } + } + return super.onCreateDialog(dialogId); + } + @Override protected int getPreferenceScreenResId() { return R.xml.dark_mode_settings; @@ -76,6 +130,18 @@ public class DarkModeSettingsFragment extends DashboardFragment { return SettingsEnums.DARK_UI_SETTINGS; } + @Override + public int getDialogMetricsCategory(int dialogId) { + switch (dialogId) { + case DIALOG_START_TIME: + return SettingsEnums.DIALOG_DARK_THEME_SET_START_TIME; + case DIALOG_END_TIME: + return SettingsEnums.DIALOG_DARK_THEME_SET_END_TIME; + default: + return 0; + } + } + public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = new BaseSearchIndexProvider(R.xml.dark_mode_settings); } diff --git a/src/com/android/settings/display/darkmode/TimeFormatter.java b/src/com/android/settings/display/darkmode/TimeFormatter.java new file mode 100644 index 00000000000..87d22dabd27 --- /dev/null +++ b/src/com/android/settings/display/darkmode/TimeFormatter.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2020 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.display.darkmode; + +import android.content.Context; + +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; + +/** + * Formats LocalTime to the locale time string format +*/ +public class TimeFormatter { + private final Context mContext; + public static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("hh:mm a"); + public TimeFormatter(Context context) { + mContext = context; + } + + public String of(LocalTime time) { + return is24HourFormat() ? time.toString() : formatter.format(time); + } + + public boolean is24HourFormat() { + return android.text.format.DateFormat.is24HourFormat(mContext); + } +} diff --git a/tests/robotests/src/com/android/settings/display/darkmode/DarkModeActivationPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/display/darkmode/DarkModeActivationPreferenceControllerTest.java index cd20ea230ab..25117be1555 100644 --- a/tests/robotests/src/com/android/settings/display/darkmode/DarkModeActivationPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/display/darkmode/DarkModeActivationPreferenceControllerTest.java @@ -32,7 +32,10 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; +import java.util.Locale; + import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; @@ -60,9 +63,12 @@ public class DarkModeActivationPreferenceControllerTest { private Button mTurnOnButton; @Mock private PowerManager mPM; + @Mock + private TimeFormatter mFormat; - private Configuration configNightYes = new Configuration(); - private Configuration configNightNo = new Configuration();; + private Configuration mConfigNightYes = new Configuration(); + private Configuration mConfigNightNo = new Configuration(); + private Locale mLocal = new Locale("ENG"); @Before public void setUp() { @@ -77,6 +83,7 @@ public class DarkModeActivationPreferenceControllerTest { when(mPreference.findViewById( eq(R.id.dark_ui_turn_off_button))).thenReturn(mTurnOffButton); when(mService.setNightModeActivated(anyBoolean())).thenReturn(true); + when(mFormat.of(any())).thenReturn("10:00 AM"); when(mContext.getString( R.string.dark_ui_activation_off_auto)).thenReturn("off_auto"); when(mContext.getString( @@ -93,16 +100,22 @@ public class DarkModeActivationPreferenceControllerTest { R.string.dark_ui_summary_off_auto_mode_never)).thenReturn("summary_off_manual"); when(mContext.getString( R.string.dark_ui_summary_on_auto_mode_never)).thenReturn("summary_on_manual"); - mController = new DarkModeActivationPreferenceController(mContext, mPreferenceKey); + when(mContext.getString( + R.string.dark_ui_summary_on_auto_mode_custom)).thenReturn("summary_on_custom"); + when(mContext.getString( + R.string.dark_ui_summary_off_auto_mode_custom)).thenReturn("summary_off_custom"); + mController = new DarkModeActivationPreferenceController(mContext, mPreferenceKey, mFormat); mController.displayPreference(mScreen); - configNightNo.uiMode = Configuration.UI_MODE_NIGHT_NO; - configNightYes.uiMode = Configuration.UI_MODE_NIGHT_YES; + mConfigNightNo.uiMode = Configuration.UI_MODE_NIGHT_NO; + mConfigNightYes.uiMode = Configuration.UI_MODE_NIGHT_YES; + mConfigNightNo.locale = mLocal; + mConfigNightYes.locale = mLocal; } @Test public void nightMode_toggleButton_offManual() { when(mService.getNightMode()).thenReturn(UiModeManager.MODE_NIGHT_YES); - when(res.getConfiguration()).thenReturn(configNightYes); + when(res.getConfiguration()).thenReturn(mConfigNightYes); mController.updateState(mPreference); @@ -112,11 +125,37 @@ public class DarkModeActivationPreferenceControllerTest { R.string.dark_ui_activation_off_manual))); } + @Test + public void nightMode_toggleButton_offCustom() { + when(mService.getNightMode()).thenReturn(UiModeManager.MODE_NIGHT_CUSTOM); + when(res.getConfiguration()).thenReturn(mConfigNightYes); + + mController.updateState(mPreference); + + verify(mTurnOnButton).setVisibility(eq(View.GONE)); + verify(mTurnOffButton).setVisibility(eq(View.VISIBLE)); + verify(mTurnOffButton).setText(eq(mContext.getString( + R.string.dark_ui_activation_off_custom))); + } + + @Test + public void nightMode_toggleButton_onCustom() { + when(mService.getNightMode()).thenReturn(UiModeManager.MODE_NIGHT_CUSTOM); + when(res.getConfiguration()).thenReturn(mConfigNightYes); + + mController.updateState(mPreference); + + verify(mTurnOnButton).setVisibility(eq(View.GONE)); + verify(mTurnOffButton).setVisibility(eq(View.VISIBLE)); + verify(mTurnOffButton).setText(eq(mContext.getString( + R.string.dark_ui_activation_on_custom))); + } + @Test public void nightMode_toggleButton_onAutoWhenModeIsYes() { when(mService.getNightMode()).thenReturn(UiModeManager.MODE_NIGHT_YES); - when(res.getConfiguration()).thenReturn(configNightNo); + when(res.getConfiguration()).thenReturn(mConfigNightNo); mController.updateState(mPreference); @@ -129,7 +168,7 @@ public class DarkModeActivationPreferenceControllerTest { @Test public void nightMode_toggleButton_onAutoWhenModeIsAuto() { when(mService.getNightMode()).thenReturn(UiModeManager.MODE_NIGHT_AUTO); - when(res.getConfiguration()).thenReturn(configNightNo); + when(res.getConfiguration()).thenReturn(mConfigNightNo); mController.updateState(mPreference); @@ -142,7 +181,7 @@ public class DarkModeActivationPreferenceControllerTest { @Test public void nightModeSummary_buttonText_onManual() { when(mService.getNightMode()).thenReturn(UiModeManager.MODE_NIGHT_NO); - when(res.getConfiguration()).thenReturn(configNightYes); + when(res.getConfiguration()).thenReturn(mConfigNightYes); assertEquals(mController.getSummary(), mContext.getString( R.string.dark_ui_summary_on_auto_mode_never)); @@ -151,7 +190,7 @@ public class DarkModeActivationPreferenceControllerTest { @Test public void nightModeSummary_buttonText_offAuto() { when(mService.getNightMode()).thenReturn(UiModeManager.MODE_NIGHT_AUTO); - when(res.getConfiguration()).thenReturn(configNightNo); + when(res.getConfiguration()).thenReturn(mConfigNightNo); assertEquals(mController.getSummary(), mContext.getString( R.string.dark_ui_summary_off_auto_mode_auto)); diff --git a/tests/robotests/src/com/android/settings/display/darkmode/DarkModeCustomPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/display/darkmode/DarkModeCustomPreferenceControllerTest.java new file mode 100644 index 00000000000..107b79f7b77 --- /dev/null +++ b/tests/robotests/src/com/android/settings/display/darkmode/DarkModeCustomPreferenceControllerTest.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2020 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.display.darkmode; + +import android.app.UiModeManager; +import android.content.ContentResolver; +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import androidx.preference.Preference; +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 static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(RobolectricTestRunner.class) +public class DarkModeCustomPreferenceControllerTest { + private DarkModeCustomPreferenceController mController; + @Mock + private UiModeManager mService; + @Mock + private Context mContext; + @Mock + private Preference mPreference; + @Mock + private Resources mResources; + @Mock + private ContentResolver mCR; + @Mock + private TimeFormatter mFormat; + @Mock + private DarkModeSettingsFragment mFragment; + private Configuration mConfig = new Configuration(); + + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mContext.getResources()).thenReturn(mResources); + when(mResources.getConfiguration()).thenReturn(mConfig); + when(mContext.getContentResolver()).thenReturn(mCR); + mService = mock(UiModeManager.class); + when(mContext.getResources()).thenReturn(mResources); + when(mContext.getSystemService(UiModeManager.class)).thenReturn(mService); + mController = new DarkModeCustomPreferenceController(mContext, "key", mFragment, mFormat); + when(mFormat.is24HourFormat()).thenReturn(false); + when(mFormat.of(any())).thenReturn("10:00 AM"); + } + + @Test + public void nightMode_customOff_hidePreference() { + when(mService.getNightMode()).thenReturn(UiModeManager.MODE_NIGHT_YES); + mController.refreshSummary(mPreference); + verify(mPreference).setVisible(eq(false)); + } + + @Test + public void nightMode_customOff_showPreference() { + when(mService.getNightMode()).thenReturn(UiModeManager.MODE_NIGHT_CUSTOM); + mController.refreshSummary(mPreference); + verify(mPreference).setVisible(eq(true)); + } + + @Test + public void nightMode_customOff_setSummaryNotNull() { + when(mService.getNightMode()).thenReturn(UiModeManager.MODE_NIGHT_CUSTOM); + mController.refreshSummary(mPreference); + verify(mPreference).setSummary(eq(mFormat.of(null))); + } +} diff --git a/tests/robotests/src/com/android/settings/display/darkmode/DarkModeScheduleSelectorControllerTest.java b/tests/robotests/src/com/android/settings/display/darkmode/DarkModeScheduleSelectorControllerTest.java index b1c6738647d..8afda73e09e 100644 --- a/tests/robotests/src/com/android/settings/display/darkmode/DarkModeScheduleSelectorControllerTest.java +++ b/tests/robotests/src/com/android/settings/display/darkmode/DarkModeScheduleSelectorControllerTest.java @@ -65,6 +65,7 @@ public class DarkModeScheduleSelectorControllerTest { when(mContext.getSystemService(PowerManager.class)).thenReturn(mPM); when(mContext.getString(R.string.dark_ui_auto_mode_never)).thenReturn("never"); when(mContext.getString(R.string.dark_ui_auto_mode_auto)).thenReturn("auto"); + when(mContext.getString(R.string.dark_ui_auto_mode_custom)).thenReturn("custom"); mPreference = spy(new DropDownPreference(mContext)); mPreference.setEntryValues(new CharSequence[]{ mContext.getString(R.string.dark_ui_auto_mode_never),