diff --git a/res/xml/dark_ui_settings.xml b/res/xml/dark_ui_settings.xml new file mode 100644 index 00000000000..1f11ebae94d --- /dev/null +++ b/res/xml/dark_ui_settings.xml @@ -0,0 +1,24 @@ + + + + diff --git a/res/xml/display_settings.xml b/res/xml/display_settings.xml index c4c39e29fce..940e6de81c6 100644 --- a/res/xml/display_settings.xml +++ b/res/xml/display_settings.xml @@ -58,14 +58,13 @@ android:targetClass="@string/config_wallpaper_picker_class" /> - + settings:searchable="false" + settings:controller="com.android.settings.display.DarkUIPreferenceController"/> 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 new file mode 100644 index 00000000000..0fca306338c --- /dev/null +++ b/src/com/android/settings/display/DarkUISettingsRadioButtonsController.java @@ -0,0 +1,86 @@ +/* + * 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/src/com/android/settings/widget/RadioButtonPickerFragment.java b/src/com/android/settings/widget/RadioButtonPickerFragment.java index 591cd218f84..8861c94a715 100644 --- a/src/com/android/settings/widget/RadioButtonPickerFragment.java +++ b/src/com/android/settings/widget/RadioButtonPickerFragment.java @@ -58,6 +58,9 @@ public abstract class RadioButtonPickerFragment extends InstrumentedPreferenceFr protected UserManager mUserManager; protected int mUserId; + private int mIllustrationId; + private int mIllustrationPreviewId; + private VideoPreference mVideoPreference; @Override public void onAttach(Context context) { @@ -164,6 +167,9 @@ public abstract class RadioButtonPickerFragment extends InstrumentedPreferenceFr final String systemDefaultKey = getSystemDefaultKey(); final PreferenceScreen screen = getPreferenceScreen(); screen.removeAll(); + if (mIllustrationId != 0) { + addIllustration(screen); + } if (!mAppendStaticPreferences) { addStaticPreferences(screen); } @@ -241,6 +247,23 @@ public abstract class RadioButtonPickerFragment extends InstrumentedPreferenceFr } } + /** + * Allows you to set an illustration at the top of this screen. Set the illustration id to 0 + * if you want to remove the illustration. + * @param illustrationId The res id for the raw of the illustration. + * @param previewId The res id for the drawable of the illustration + */ + protected void setIllustration(int illustrationId, int previewId) { + mIllustrationId = illustrationId; + mIllustrationPreviewId = previewId; + } + + private void addIllustration(PreferenceScreen screen) { + mVideoPreference = new VideoPreference(getContext()); + mVideoPreference.setVideo(mIllustrationId, mIllustrationPreviewId); + screen.addPreference(mVideoPreference); + } + protected abstract List getCandidates(); protected abstract String getDefaultKey(); diff --git a/src/com/android/settings/widget/VideoPreference.java b/src/com/android/settings/widget/VideoPreference.java index fd215d80e47..2d886732fa0 100644 --- a/src/com/android/settings/widget/VideoPreference.java +++ b/src/com/android/settings/widget/VideoPreference.java @@ -55,22 +55,41 @@ public class VideoPreference extends Preference { private int mPreviewResource; private boolean mViewVisible; private Surface mSurface; + private int mAnimationId; + + public VideoPreference(Context context) { + super(context); + mContext = context; + initialize(context, null); + } public VideoPreference(Context context, AttributeSet attrs) { super(context, attrs); mContext = context; + initialize(context, attrs); + } + + private void initialize(Context context, AttributeSet attrs) { TypedArray attributes = context.getTheme().obtainStyledAttributes( attrs, - com.android.settings.R.styleable.VideoPreference, + R.styleable.VideoPreference, 0, 0); try { - int animation = attributes.getResourceId(R.styleable.VideoPreference_animation, 0); + // if these are already set that means they were set dynamically and don't need + // to be loaded from xml + mAnimationId = mAnimationId == 0 + ? attributes.getResourceId(R.styleable.VideoPreference_animation, 0) + : mAnimationId; mVideoPath = new Uri.Builder().scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) .authority(context.getPackageName()) - .appendPath(String.valueOf(animation)) + .appendPath(String.valueOf(mAnimationId)) .build(); - mPreviewResource = attributes.getResourceId( - R.styleable.VideoPreference_preview, 0); + mPreviewResource = mPreviewResource == 0 + ? attributes.getResourceId(R.styleable.VideoPreference_preview, 0) + : mPreviewResource; + if (mPreviewResource == 0 && mAnimationId == 0) { + return; + } initMediaPlayer(); if (mMediaPlayer != null && mMediaPlayer.getDuration() > 0) { setVisible(true); @@ -103,20 +122,9 @@ public class VideoPreference extends Preference { imageView.setImageResource(mPreviewResource); layout.setAspectRatio(mAspectRadio); + updateViewStates(imageView, playButton); - video.setOnClickListener(v -> { - if (mMediaPlayer != null) { - if (mMediaPlayer.isPlaying()) { - mMediaPlayer.pause(); - playButton.setVisibility(View.VISIBLE); - mVideoPaused = true; - } else { - mMediaPlayer.start(); - playButton.setVisibility(View.GONE); - mVideoPaused = false; - } - } - }); + video.setOnClickListener(v -> updateViewStates(imageView, playButton)); video.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() { @Override @@ -161,6 +169,23 @@ public class VideoPreference extends Preference { }); } + @VisibleForTesting + void updateViewStates(ImageView imageView, ImageView playButton) { + if (mMediaPlayer != null) { + if (mMediaPlayer.isPlaying()) { + mMediaPlayer.pause(); + playButton.setVisibility(View.VISIBLE); + imageView.setVisibility(View.VISIBLE); + mVideoPaused = true; + } else { + imageView.setVisibility(View.GONE); + playButton.setVisibility(View.GONE); + mMediaPlayer.start(); + mVideoPaused = false; + } + } + } + @Override public void onDetached() { releaseMediaPlayer(); @@ -178,6 +203,20 @@ public class VideoPreference extends Preference { releaseMediaPlayer(); } + /** + * Sets the video for this preference. If a previous video was set this one will override it + * and properly release any resources and re-initialize the preference to play the new video. + * + * @param videoId The raw res id of the video + * @param previewId The drawable res id of the preview image to use if the video fails to load. + */ + public void setVideo(int videoId, int previewId) { + mAnimationId = videoId; + mPreviewResource = previewId; + releaseMediaPlayer(); + initialize(mContext, null); + } + private void initMediaPlayer() { if (mMediaPlayer == null) { mMediaPlayer = MediaPlayer.create(mContext, mVideoPath); diff --git a/tests/robotests/src/com/android/settings/display/DarkUIPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/display/DarkUIPreferenceControllerTest.java deleted file mode 100644 index c8f847b8872..00000000000 --- a/tests/robotests/src/com/android/settings/display/DarkUIPreferenceControllerTest.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * 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.display; - -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.app.UiModeManager; -import android.content.Context; - -import androidx.preference.ListPreference; -import androidx.preference.PreferenceScreen; - -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 DarkUIPreferenceControllerTest { - - private Context mContext; - @Mock - private ListPreference mPreference; - @Mock - private PreferenceScreen mPreferenceScreen; - @Mock - private UiModeManager mUiModeManager; - private DarkUIPreferenceController mController; - - @Before - public void setup() { - MockitoAnnotations.initMocks(this); - mContext = RuntimeEnvironment.application; - mController = new DarkUIPreferenceController(mContext, "dark_ui_mode"); - mController.setUiModeManager(mUiModeManager); - when(mPreferenceScreen.findPreference(mController.getPreferenceKey())) - .thenReturn(mPreference); - mController.displayPreference(mPreferenceScreen); - } - - @Test - public void onPreferenceChanged_setAuto() { - // Auto was deprecated, it should default to NO. - mController.onPreferenceChange(mPreference, "auto"); - verify(mUiModeManager).setNightMode(eq(UiModeManager.MODE_NIGHT_NO)); - } - - @Test - public void onPreferenceChanged_setNightMode() { - mController.onPreferenceChange(mPreference, "yes"); - verify(mUiModeManager).setNightMode(eq(UiModeManager.MODE_NIGHT_YES)); - } - - @Test - public void onPreferenceChanged_setDayMode() { - mController.onPreferenceChange(mPreference, "no"); - verify(mUiModeManager).setNightMode(eq(UiModeManager.MODE_NIGHT_NO)); - } - - public int getCurrentMode() { - final UiModeManager uiModeManager = mContext.getSystemService(UiModeManager.class); - return uiModeManager.getNightMode(); - } -} diff --git a/tests/robotests/src/com/android/settings/display/DarkUISettingsRadioButtonsControllerTest.java b/tests/robotests/src/com/android/settings/display/DarkUISettingsRadioButtonsControllerTest.java new file mode 100644 index 00000000000..76142a42e60 --- /dev/null +++ b/tests/robotests/src/com/android/settings/display/DarkUISettingsRadioButtonsControllerTest.java @@ -0,0 +1,52 @@ +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(); + } +} diff --git a/tests/robotests/src/com/android/settings/widget/VideoPreferenceTest.java b/tests/robotests/src/com/android/settings/widget/VideoPreferenceTest.java index 4cd6be4c15f..b53f3644575 100644 --- a/tests/robotests/src/com/android/settings/widget/VideoPreferenceTest.java +++ b/tests/robotests/src/com/android/settings/widget/VideoPreferenceTest.java @@ -18,6 +18,10 @@ package com.android.settings.widget; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -29,6 +33,8 @@ import android.media.MediaPlayer; import android.view.LayoutInflater; import android.view.TextureView; +import android.view.View; +import android.widget.ImageView; import androidx.preference.PreferenceViewHolder; import com.android.settings.R; @@ -45,8 +51,13 @@ import org.robolectric.RuntimeEnvironment; public class VideoPreferenceTest { private static final int VIDEO_WIDTH = 100; private static final int VIDEO_HEIGHT = 150; + @Mock private MediaPlayer mMediaPlayer; + @Mock + private ImageView fakePreview; + @Mock + private ImageView fakePlayButton; private Context mContext; private VideoPreference mVideoPreference; private PreferenceViewHolder mPreferenceViewHolder; @@ -83,8 +94,8 @@ public class VideoPreferenceTest { (TextureView) mPreferenceViewHolder.findViewById(R.id.video_texture_view); mVideoPreference.mAnimationAvailable = true; mVideoPreference.mVideoReady = true; - mVideoPreference.onBindViewHolder(mPreferenceViewHolder); mVideoPreference.onViewInvisible(); + mVideoPreference.onBindViewHolder(mPreferenceViewHolder); when(mMediaPlayer.isPlaying()).thenReturn(false); final TextureView.SurfaceTextureListener listener = video.getSurfaceTextureListener(); @@ -101,4 +112,30 @@ public class VideoPreferenceTest { verify(mMediaPlayer).release(); } + + @Test + public void updateViewStates_paused_updatesViews() { + when(mMediaPlayer.isPlaying()).thenReturn(true); + mVideoPreference.updateViewStates(fakePreview, fakePlayButton); + verify(fakePlayButton).setVisibility(eq(View.VISIBLE)); + verify(fakePreview).setVisibility(eq(View.VISIBLE)); + verify(mMediaPlayer).pause(); + } + + @Test + public void updateViewStates_playing_updatesViews() { + when(mMediaPlayer.isPlaying()).thenReturn(false); + mVideoPreference.updateViewStates(fakePreview, fakePlayButton); + verify(fakePlayButton).setVisibility(eq(View.GONE)); + verify(fakePreview).setVisibility(eq(View.GONE)); + verify(mMediaPlayer).start(); + } + + @Test + public void updateViewStates_noMediaPlayer_skips() { + mVideoPreference.mMediaPlayer = null; + mVideoPreference.updateViewStates(fakePreview, fakePlayButton); + verify(fakePlayButton, never()).setVisibility(anyInt()); + verify(fakePreview, never()).setVisibility(anyInt()); + } }