diff --git a/res/drawable/dark_theme.xml b/res/drawable/dark_theme.xml new file mode 100644 index 00000000000..3425002417a --- /dev/null +++ b/res/drawable/dark_theme.xml @@ -0,0 +1,26 @@ + + + + + \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index da1c158b36f..414cb6efde0 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -9900,17 +9900,14 @@ System UI demo mode - Theme - - - Choose Theme - - - This setting also applies to apps + Dark Theme Supported apps will also switch to dark theme + + Got it + Quick settings developer tiles diff --git a/res/xml/accessibility_settings.xml b/res/xml/accessibility_settings.xml index 26849d1fe41..b4968f8200f 100644 --- a/res/xml/accessibility_settings.xml +++ b/res/xml/accessibility_settings.xml @@ -58,11 +58,10 @@ android:title="@string/screen_zoom_title" settings:searchable="false"/> - + settings:searchable="false"/> - diff --git a/src/com/android/settings/DisplaySettings.java b/src/com/android/settings/DisplaySettings.java index b67e6e84f3a..eb77d4a3492 100644 --- a/src/com/android/settings/DisplaySettings.java +++ b/src/com/android/settings/DisplaySettings.java @@ -18,11 +18,13 @@ package com.android.settings; import android.app.settings.SettingsEnums; import android.content.Context; +import android.os.Bundle; import android.provider.SearchIndexableResource; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.display.BrightnessLevelPreferenceController; import com.android.settings.display.CameraGesturePreferenceController; +import com.android.settings.display.DarkUIPreferenceController; import com.android.settings.display.LiftToWakePreferenceController; import com.android.settings.display.NightDisplayPreferenceController; import com.android.settings.display.NightModePreferenceController; @@ -62,6 +64,12 @@ public class DisplaySettings extends DashboardFragment { return R.xml.display_settings; } + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + use(DarkUIPreferenceController.class).setParentFragment(this); + } + @Override protected List createPreferenceControllers(Context context) { return buildPreferenceControllers(context, getSettingsLifecycle()); diff --git a/src/com/android/settings/accessibility/AccessibilitySettings.java b/src/com/android/settings/accessibility/AccessibilitySettings.java index efa14f64c7c..478db9f298f 100644 --- a/src/com/android/settings/accessibility/AccessibilitySettings.java +++ b/src/com/android/settings/accessibility/AccessibilitySettings.java @@ -244,7 +244,7 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements private SwitchPreference mToggleInversionPreference; private ColorInversionPreferenceController mInversionPreferenceController; private AccessibilityHearingAidPreferenceController mHearingAidPreferenceController; - private Preference mDarkUIModePreference; + private SwitchPreference mDarkUIModePreference; private DarkUIPreferenceController mDarkUIPreferenceController; private LiveCaptionPreferenceController mLiveCaptionPreferenceController; @@ -524,8 +524,8 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements mDarkUIModePreference = findPreference(DARK_UI_MODE_PREFERENCE); mDarkUIPreferenceController = new DarkUIPreferenceController(getContext(), DARK_UI_MODE_PREFERENCE); + mDarkUIPreferenceController.setParentFragment(this); mDarkUIPreferenceController.displayPreference(getPreferenceScreen()); - mDarkUIModePreference.setSummary(mDarkUIPreferenceController.getSummary()); } private void updateAllPreferences() { diff --git a/src/com/android/settings/display/DarkUIInfoDialogFragment.java b/src/com/android/settings/display/DarkUIInfoDialogFragment.java new file mode 100644 index 00000000000..8fca679081c --- /dev/null +++ b/src/com/android/settings/display/DarkUIInfoDialogFragment.java @@ -0,0 +1,93 @@ +/* + * 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.display; + +import static com.android.settings.display.DarkUIPreferenceController.DARK_MODE_PREFS; +import static com.android.settings.display.DarkUIPreferenceController.PREF_DARK_MODE_DIALOG_SEEN; + +import android.app.Dialog; +import android.app.UiModeManager; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.provider.Settings; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; + +import com.android.settings.R; +import com.android.settings.core.instrumentation.InstrumentedDialogFragment; + +public class DarkUIInfoDialogFragment extends InstrumentedDialogFragment + implements DialogInterface.OnClickListener{ + + @Override + public int getMetricsCategory() { + // TODO(b/130251804): Add metrics constant in followup change to avoid merge conflict in + // beta cherrypick + return 0; + } + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + Context context = getContext(); + AlertDialog.Builder dialog = new AlertDialog.Builder(context); + LayoutInflater inflater = LayoutInflater.from(dialog.getContext()); + View titleView = inflater.inflate(R.layout.settings_dialog_title, null); + ((ImageView) titleView.findViewById(R.id.settings_icon)) + .setImageDrawable(context.getDrawable(R.drawable.dark_theme)); + ((TextView) titleView.findViewById(R.id.settings_title)).setText(R.string.dark_ui_mode); + + dialog.setCustomTitle(titleView) + .setMessage(R.string.dark_ui_settings_dark_summary) + .setPositiveButton( + R.string.dark_ui_settings_dialog_acknowledge, + this); + return dialog.create(); + } + + @Override + public void onDismiss(@NonNull DialogInterface dialog) { + enableDarkTheme(); + super.onDismiss(dialog); + } + + @Override + public void onClick(DialogInterface dialogInterface, int i) { + // We have to manually dismiss the dialog because changing night mode causes it to + // recreate itself. + dialogInterface.dismiss(); + enableDarkTheme(); + } + + private void enableDarkTheme() { + final Context context = getContext(); + if (context != null) { + Settings.Secure.putInt(context.getContentResolver(), + Settings.Secure.DARK_MODE_DIALOG_SEEN, + DarkUIPreferenceController.DIALOG_SEEN); + context.getSystemService(UiModeManager.class) + .setNightMode(UiModeManager.MODE_NIGHT_YES); + } + } +} diff --git a/src/com/android/settings/display/DarkUIPreferenceController.java b/src/com/android/settings/display/DarkUIPreferenceController.java index 7d8fd56ae13..9df2402850e 100644 --- a/src/com/android/settings/display/DarkUIPreferenceController.java +++ b/src/com/android/settings/display/DarkUIPreferenceController.java @@ -18,37 +18,66 @@ package com.android.settings.display; import android.app.UiModeManager; import android.content.Context; +import android.provider.Settings; import androidx.annotation.VisibleForTesting; -import androidx.preference.ListPreference; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; +import androidx.fragment.app.Fragment; -import com.android.settings.R; -import com.android.settings.core.BasePreferenceController; +import com.android.settings.core.TogglePreferenceController; -public class DarkUIPreferenceController extends BasePreferenceController { +public class DarkUIPreferenceController extends TogglePreferenceController { + public static final String DARK_MODE_PREFS = "dark_mode_prefs"; + public static final String PREF_DARK_MODE_DIALOG_SEEN = "dark_mode_dialog_seen"; + public static final int DIALOG_SEEN = 1; private UiModeManager mUiModeManager; + private Context mContext; + private Fragment mFragment; public DarkUIPreferenceController(Context context, String key) { super(context, key); + mContext = context; mUiModeManager = context.getSystemService(UiModeManager.class); } + @Override + public boolean isChecked() { + return mUiModeManager.getNightMode() == UiModeManager.MODE_NIGHT_YES; + } + + @Override + public boolean setChecked(boolean isChecked) { + final boolean dialogSeen = + Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.DARK_MODE_DIALOG_SEEN, 0) == DIALOG_SEEN; + if (!dialogSeen && isChecked) { + showDarkModeDialog(); + return false; + } + mUiModeManager.setNightMode(isChecked + ? UiModeManager.MODE_NIGHT_YES + : UiModeManager.MODE_NIGHT_NO); + return true; + } + + private void showDarkModeDialog() { + final DarkUIInfoDialogFragment frag = new DarkUIInfoDialogFragment(); + if (mFragment.getFragmentManager() != null) { + frag.show(mFragment.getFragmentManager(), getClass().getName()); + } + } + @VisibleForTesting void setUiModeManager(UiModeManager uiModeManager) { mUiModeManager = uiModeManager; } + public void setParentFragment(Fragment fragment) { + mFragment = fragment; + } + @Override public int getAvailabilityStatus() { return AVAILABLE; } - - @Override - public CharSequence getSummary() { - return DarkUISettingsRadioButtonsController.modeToDescription( - mContext, mUiModeManager.getNightMode()); - } } diff --git a/src/com/android/settings/display/DarkUISettings.java b/src/com/android/settings/display/DarkUISettings.java deleted file mode 100644 index 50fd3868c93..00000000000 --- a/src/com/android/settings/display/DarkUISettings.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * 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.display; - -import android.app.UiModeManager; -import android.app.settings.SettingsEnums; -import android.content.Context; -import android.graphics.drawable.Drawable; -import android.provider.SearchIndexableResource; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; -import com.android.settings.R; -import com.android.settings.search.BaseSearchIndexProvider; -import com.android.settings.search.Indexable; -import com.android.settings.widget.RadioButtonPickerFragment; -import com.android.settingslib.search.SearchIndexable; -import com.android.settingslib.widget.CandidateInfo; -import com.android.settingslib.widget.FooterPreference; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -/** - * The screen for selecting the dark theme preference for this device. Automatically updates - * the associated footer view with any needed information. - */ -@SearchIndexable(forTarget = SearchIndexable.ALL & ~SearchIndexable.ARC) -public class DarkUISettings extends RadioButtonPickerFragment implements Indexable { - - private DarkUISettingsRadioButtonsController mController; - private Preference mFooter; - - @Override - protected int getPreferenceScreenResId() { - return R.xml.dark_ui_settings; - } - - @Override - public void onAttach(Context context) { - super.onAttach(context); - // TODO(b/128686189): add illustration once it is ready - setIllustration(0, 0); - mFooter = new FooterPreference(context); - mFooter.setIcon(android.R.color.transparent); - mController = new DarkUISettingsRadioButtonsController(context, mFooter); - } - - @Override - protected List getCandidates() { - final Context context = getContext(); - final List candidates = new ArrayList<>(); - candidates.add(new DarkUISettingsCandidateInfo( - DarkUISettingsRadioButtonsController.modeToDescription( - context, UiModeManager.MODE_NIGHT_YES), - /* summary */ null, - DarkUISettingsRadioButtonsController.KEY_DARK, - /* enabled */ true)); - candidates.add(new DarkUISettingsCandidateInfo( - DarkUISettingsRadioButtonsController.modeToDescription( - context, UiModeManager.MODE_NIGHT_NO), - /* summary */ null, - DarkUISettingsRadioButtonsController.KEY_LIGHT, - /* enabled */ true)); - return candidates; - } - - @Override - protected void addStaticPreferences(PreferenceScreen screen) { - screen.addPreference(mFooter); - } - - @Override - protected String getDefaultKey() { - return mController.getDefaultKey(); - } - - @Override - protected boolean setDefaultKey(String key) { - return mController.setDefaultKey(key); - } - - @Override - public int getMetricsCategory() { - return SettingsEnums.DARK_UI_SETTINGS; - } - - static class DarkUISettingsCandidateInfo extends CandidateInfo { - - private final CharSequence mLabel; - private final CharSequence mSummary; - private final String mKey; - - DarkUISettingsCandidateInfo(CharSequence label, CharSequence summary, String key, - boolean enabled) { - super(enabled); - mLabel = label; - mKey = key; - mSummary = summary; - } - - @Override - public CharSequence loadLabel() { - return mLabel; - } - - @Override - public Drawable loadIcon() { - return null; - } - - @Override - public String getKey() { - return mKey; - } - - public CharSequence getSummary() { - return mSummary; - } - } - - public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = - new BaseSearchIndexProvider() { - @Override - public List getXmlResourcesToIndex( - Context context, boolean enabled) { - final SearchIndexableResource sir = new SearchIndexableResource(context); - sir.xmlResId = R.xml.dark_ui_settings; - return Arrays.asList(sir); - } - }; -} diff --git a/src/com/android/settings/display/DarkUISettingsRadioButtonsController.java b/src/com/android/settings/display/DarkUISettingsRadioButtonsController.java deleted file mode 100644 index 0fca306338c..00000000000 --- a/src/com/android/settings/display/DarkUISettingsRadioButtonsController.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * 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.display; - -import android.app.UiModeManager; -import android.content.Context; -import androidx.preference.Preference; -import com.android.settings.R; -import androidx.annotation.VisibleForTesting; - -public class DarkUISettingsRadioButtonsController { - - public static final String KEY_DARK = "key_dark_ui_settings_dark"; - public static final String KEY_LIGHT = "key_dark_ui_settings_light"; - - @VisibleForTesting - UiModeManager mManager; - - private Preference mFooter; - - public DarkUISettingsRadioButtonsController(Context context, Preference footer) { - mManager = context.getSystemService(UiModeManager.class); - mFooter = footer; - } - - public String getDefaultKey() { - final int mode = mManager.getNightMode(); - updateFooter(); - return mode == UiModeManager.MODE_NIGHT_YES ? KEY_DARK : KEY_LIGHT; - } - - public boolean setDefaultKey(String key) { - switch(key) { - case KEY_DARK: - mManager.setNightMode(UiModeManager.MODE_NIGHT_YES); - break; - case KEY_LIGHT: - mManager.setNightMode(UiModeManager.MODE_NIGHT_NO); - break; - default: - throw new IllegalStateException( - "Not a valid key for " + this.getClass().getSimpleName() + ": " + key); - } - updateFooter(); - return true; - } - - public void updateFooter() { - final int mode = mManager.getNightMode(); - switch (mode) { - case UiModeManager.MODE_NIGHT_YES: - mFooter.setSummary(R.string.dark_ui_settings_dark_summary); - break; - case UiModeManager.MODE_NIGHT_NO: - case UiModeManager.MODE_NIGHT_AUTO: - default: - mFooter.setSummary(R.string.dark_ui_settings_light_summary); - } - } - - public static String modeToDescription(Context context, int mode) { - final String[] values = context.getResources().getStringArray(R.array.dark_ui_mode_entries); - switch (mode) { - case UiModeManager.MODE_NIGHT_YES: - return values[0]; - case UiModeManager.MODE_NIGHT_NO: - case UiModeManager.MODE_NIGHT_AUTO: - default: - return values[1]; - } - } -} diff --git a/tests/robotests/src/com/android/settings/accessibility/AccessibilitySettingsTest.java b/tests/robotests/src/com/android/settings/accessibility/AccessibilitySettingsTest.java index 0cc9dc43375..a07ffb9f505 100644 --- a/tests/robotests/src/com/android/settings/accessibility/AccessibilitySettingsTest.java +++ b/tests/robotests/src/com/android/settings/accessibility/AccessibilitySettingsTest.java @@ -154,19 +154,6 @@ public class AccessibilitySettingsTest { assertThat(preference.getSummary()).isEqualTo(mContext.getResources().getString(resId)); } - @Test - public void testDarkUIModePreferenceSummary_shouldUpdateSummary() { - final Preference darkUIModePreference = new Preference(mContext); - final DarkUIPreferenceController mController; - doReturn(darkUIModePreference).when(mSettings).findPreference( - DARK_UI_MODE_PREFERENCE); - mController = new DarkUIPreferenceController(mContext, DARK_UI_MODE_PREFERENCE); - final String darkUIModeDescription = modeToDescription(mUiModeManager.getNightMode()); - darkUIModePreference.setSummary(mController.getSummary()); - - assertThat(darkUIModePreference.getSummary()).isEqualTo(darkUIModeDescription); - } - private String modeToDescription(int mode) { String[] values = mContext.getResources().getStringArray(R.array.dark_ui_mode_entries); switch (mode) { diff --git a/tests/robotests/src/com/android/settings/display/DarkUIInfoDialogFragmentTest.java b/tests/robotests/src/com/android/settings/display/DarkUIInfoDialogFragmentTest.java new file mode 100644 index 00000000000..7a8bdedd63e --- /dev/null +++ b/tests/robotests/src/com/android/settings/display/DarkUIInfoDialogFragmentTest.java @@ -0,0 +1,65 @@ +/* + * 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.display; + +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.times; +import static org.mockito.Mockito.verify; + +import android.content.Context; +import android.content.DialogInterface; +import android.content.SharedPreferences; + +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 DarkUIInfoDialogFragmentTest { + private DarkUIInfoDialogFragment mFragment; + @Mock + private DialogInterface dialog; + + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mFragment = spy(new DarkUIInfoDialogFragment()); + } + + @Test + public void dialogDismissedOnConfirmation() { + doReturn(RuntimeEnvironment.application).when(mFragment).getContext(); + SharedPreferences prefs = RuntimeEnvironment.application.getSharedPreferences( + DarkUIPreferenceController.DARK_MODE_PREFS, + Context.MODE_PRIVATE); + assertThat(prefs.getBoolean(DarkUIPreferenceController.PREF_DARK_MODE_DIALOG_SEEN, false)) + .isFalse(); + mFragment.onClick(dialog, DialogInterface.BUTTON_POSITIVE); + verify(dialog, times(1)).dismiss(); + assertThat(prefs.getBoolean(DarkUIPreferenceController.PREF_DARK_MODE_DIALOG_SEEN, false)) + .isTrue(); + + } +} diff --git a/tests/robotests/src/com/android/settings/display/DarkUISettingsRadioButtonsControllerTest.java b/tests/robotests/src/com/android/settings/display/DarkUISettingsRadioButtonsControllerTest.java deleted file mode 100644 index 76142a42e60..00000000000 --- a/tests/robotests/src/com/android/settings/display/DarkUISettingsRadioButtonsControllerTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.android.settings.display; - -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.verify; - -import android.app.UiModeManager; -import android.content.Context; -import androidx.preference.Preference; -import com.android.settings.R; -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 DarkUISettingsRadioButtonsControllerTest { - - @Mock - private UiModeManager mUiModeManager; - @Mock - private Preference mFooter; - private Context mContext; - private DarkUISettingsRadioButtonsController mController; - - @Before - public void setup() { - MockitoAnnotations.initMocks(this); - mContext = RuntimeEnvironment.application; - mController = new DarkUISettingsRadioButtonsController(mContext, mFooter); - mController.mManager = mUiModeManager; - } - - @Test - public void footerUpdatesCorrectly() { - doReturn(UiModeManager.MODE_NIGHT_YES).when(mUiModeManager).getNightMode(); - mController.updateFooter(); - verify(mFooter).setSummary(eq(R.string.dark_ui_settings_dark_summary)); - - doReturn(UiModeManager.MODE_NIGHT_NO).when(mUiModeManager).getNightMode(); - mController.updateFooter(); - verify(mFooter).setSummary(eq(R.string.dark_ui_settings_light_summary)); - } - - public int getCurrentMode() { - final UiModeManager uiModeManager = mContext.getSystemService(UiModeManager.class); - return uiModeManager.getNightMode(); - } -}