From 921e3d14b8745bf618be21dc23124a0f996c7012 Mon Sep 17 00:00:00 2001 From: Ben Lin Date: Fri, 5 Jan 2018 12:30:09 -0800 Subject: [PATCH 01/22] Add ability to show/hide individual settings in Sounds page. This adds the following 7 new boolean flags: config_show_alarm_volume config_show_charging_sounds config_show_media_volume config_show_notification_ringtone config_show_notification_volume config_show_screen_locking_sounds config_show_touch_sounds Which when set to false, will hide the respective preferences in Sounds page. Bug: 69813881 Test: make RunSettingsRoboTests ROBOTEST_FILTER=com.android.settings.notification all pass. Change-Id: I84a2ce66b07c00d658422ce1a0eacaf9a01fae8a --- res/values/bools.xml | 21 ++++++++ .../AlarmVolumePreferenceController.java | 4 +- .../ChargingSoundPreferenceController.java | 6 ++- .../MediaVolumePreferenceController.java | 3 +- ...ificationRingtonePreferenceController.java | 7 +++ ...otificationVolumePreferenceController.java | 4 +- .../ScreenLockSoundPreferenceController.java | 5 ++ .../settings/notification/SoundSettings.java | 49 +++++++++++++++---- .../TouchSoundPreferenceController.java | 6 +++ tests/robotests/res/values-mcc999/config.xml | 7 +++ .../AlarmVolumePreferenceControllerTest.java | 13 ++++- ...ChargingSoundPreferenceControllerTest.java | 14 ++++-- .../MediaVolumePreferenceControllerTest.java | 14 ++++-- ...ationRingtonePreferenceControllerTest.java | 16 +++++- ...icationVolumePreferenceControllerTest.java | 13 ++++- ...reenLockSoundPreferenceControllerTest.java | 14 ++++-- .../TouchSoundPreferenceControllerTest.java | 14 ++++-- 17 files changed, 180 insertions(+), 30 deletions(-) diff --git a/res/values/bools.xml b/res/values/bools.xml index 687d5bd1ddc..c1f5ad0c8b6 100644 --- a/res/values/bools.xml +++ b/res/values/bools.xml @@ -60,6 +60,27 @@ true + + true + + + true + + + true + + + true + + + true + + + true + + + true + true diff --git a/src/com/android/settings/notification/AlarmVolumePreferenceController.java b/src/com/android/settings/notification/AlarmVolumePreferenceController.java index cd1f6e39cd6..c9b283b5741 100644 --- a/src/com/android/settings/notification/AlarmVolumePreferenceController.java +++ b/src/com/android/settings/notification/AlarmVolumePreferenceController.java @@ -21,6 +21,7 @@ import android.media.AudioManager; import com.android.internal.annotations.VisibleForTesting; import com.android.settings.notification.VolumeSeekBarPreference.Callback; +import com.android.settings.R; import com.android.settingslib.core.lifecycle.Lifecycle; public class AlarmVolumePreferenceController extends @@ -43,7 +44,8 @@ public class AlarmVolumePreferenceController extends @Override public boolean isAvailable() { - return !mHelper.isSingleVolume(); + return mContext.getResources().getBoolean(R.bool.config_show_alarm_volume) + && !mHelper.isSingleVolume(); } @Override diff --git a/src/com/android/settings/notification/ChargingSoundPreferenceController.java b/src/com/android/settings/notification/ChargingSoundPreferenceController.java index 55cba9677f0..e280177ce6d 100644 --- a/src/com/android/settings/notification/ChargingSoundPreferenceController.java +++ b/src/com/android/settings/notification/ChargingSoundPreferenceController.java @@ -21,6 +21,7 @@ import static com.android.settings.notification.SettingPref.TYPE_GLOBAL; import android.content.Context; import android.provider.Settings.Global; +import com.android.settings.R; import com.android.settings.SettingsPreferenceFragment; import com.android.settingslib.core.lifecycle.Lifecycle; @@ -33,7 +34,10 @@ public class ChargingSoundPreferenceController extends SettingPrefController { super(context, parent, lifecycle); mPreference = new SettingPref( TYPE_GLOBAL, KEY_CHARGING_SOUNDS, Global.CHARGING_SOUNDS_ENABLED, DEFAULT_ON); - } + @Override + public boolean isAvailable() { + return mContext.getResources().getBoolean(R.bool.config_show_charging_sounds); + } } diff --git a/src/com/android/settings/notification/MediaVolumePreferenceController.java b/src/com/android/settings/notification/MediaVolumePreferenceController.java index 4f167f8750d..381135e0956 100644 --- a/src/com/android/settings/notification/MediaVolumePreferenceController.java +++ b/src/com/android/settings/notification/MediaVolumePreferenceController.java @@ -19,6 +19,7 @@ package com.android.settings.notification; import android.content.Context; import android.media.AudioManager; import com.android.settings.notification.VolumeSeekBarPreference.Callback; +import com.android.settings.R; import com.android.settingslib.core.lifecycle.Lifecycle; public class MediaVolumePreferenceController extends @@ -32,7 +33,7 @@ public class MediaVolumePreferenceController extends @Override public boolean isAvailable() { - return true; + return mContext.getResources().getBoolean(R.bool.config_show_media_volume); } @Override diff --git a/src/com/android/settings/notification/NotificationRingtonePreferenceController.java b/src/com/android/settings/notification/NotificationRingtonePreferenceController.java index 72e8d8f54d6..00f478f2d28 100644 --- a/src/com/android/settings/notification/NotificationRingtonePreferenceController.java +++ b/src/com/android/settings/notification/NotificationRingtonePreferenceController.java @@ -19,6 +19,8 @@ package com.android.settings.notification; import android.content.Context; import android.media.RingtoneManager; +import com.android.settings.R; + public class NotificationRingtonePreferenceController extends RingtonePreferenceControllerBase { private static final String KEY_NOTIFICATION_RINGTONE = "notification_ringtone"; @@ -27,6 +29,11 @@ public class NotificationRingtonePreferenceController extends RingtonePreference super(context); } + @Override + public boolean isAvailable() { + return mContext.getResources().getBoolean(R.bool.config_show_notification_ringtone); + } + @Override public String getPreferenceKey() { return KEY_NOTIFICATION_RINGTONE; diff --git a/src/com/android/settings/notification/NotificationVolumePreferenceController.java b/src/com/android/settings/notification/NotificationVolumePreferenceController.java index 5f888f8af65..4024f9fcf21 100644 --- a/src/com/android/settings/notification/NotificationVolumePreferenceController.java +++ b/src/com/android/settings/notification/NotificationVolumePreferenceController.java @@ -20,6 +20,7 @@ import android.content.Context; import android.media.AudioManager; import com.android.internal.annotations.VisibleForTesting; +import com.android.settings.R; import com.android.settings.Utils; import com.android.settings.notification.VolumeSeekBarPreference.Callback; import com.android.settingslib.core.lifecycle.Lifecycle; @@ -45,7 +46,8 @@ public class NotificationVolumePreferenceController extends @Override public boolean isAvailable() { - return !Utils.isVoiceCapable(mContext) && !mHelper.isSingleVolume(); + return mContext.getResources().getBoolean(R.bool.config_show_notification_volume) + && !Utils.isVoiceCapable(mContext) && !mHelper.isSingleVolume(); } @Override diff --git a/src/com/android/settings/notification/ScreenLockSoundPreferenceController.java b/src/com/android/settings/notification/ScreenLockSoundPreferenceController.java index b08b8f84090..11aaa92dd01 100644 --- a/src/com/android/settings/notification/ScreenLockSoundPreferenceController.java +++ b/src/com/android/settings/notification/ScreenLockSoundPreferenceController.java @@ -21,6 +21,7 @@ import static com.android.settings.notification.SettingPref.TYPE_SYSTEM; import android.content.Context; import android.provider.Settings.System; +import com.android.settings.R; import com.android.settings.SettingsPreferenceFragment; import com.android.settingslib.core.lifecycle.Lifecycle; @@ -35,4 +36,8 @@ public class ScreenLockSoundPreferenceController extends SettingPrefController { TYPE_SYSTEM, KEY_SCREEN_LOCKING_SOUNDS, System.LOCKSCREEN_SOUNDS_ENABLED, DEFAULT_ON); } + @Override + public boolean isAvailable() { + return mContext.getResources().getBoolean(R.bool.config_show_screen_locking_sounds); + } } diff --git a/src/com/android/settings/notification/SoundSettings.java b/src/com/android/settings/notification/SoundSettings.java index 6e998f6fe32..4a27b11a49f 100644 --- a/src/com/android/settings/notification/SoundSettings.java +++ b/src/com/android/settings/notification/SoundSettings.java @@ -33,6 +33,7 @@ import com.android.settings.R; import com.android.settings.RingtonePreference; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.widget.PreferenceCategoryController; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.lifecycle.Lifecycle; @@ -201,15 +202,45 @@ public class SoundSettings extends DashboardFragment { controllers.add(new WorkSoundPreferenceController(context, fragment, lifecycle)); // === Other Sound Settings === - controllers.add(new DialPadTonePreferenceController(context, fragment, lifecycle)); - controllers.add(new ScreenLockSoundPreferenceController(context, fragment, lifecycle)); - controllers.add(new ChargingSoundPreferenceController(context, fragment, lifecycle)); - controllers.add(new DockingSoundPreferenceController(context, fragment, lifecycle)); - controllers.add(new TouchSoundPreferenceController(context, fragment, lifecycle)); - controllers.add(new VibrateOnTouchPreferenceController(context, fragment, lifecycle)); - controllers.add(new DockAudioMediaPreferenceController(context, fragment, lifecycle)); - controllers.add(new BootSoundPreferenceController(context)); - controllers.add(new EmergencyTonePreferenceController(context, fragment, lifecycle)); + final DialPadTonePreferenceController dialPadTonePreferenceController = + new DialPadTonePreferenceController(context, fragment, lifecycle); + final ScreenLockSoundPreferenceController screenLockSoundPreferenceController = + new ScreenLockSoundPreferenceController(context, fragment, lifecycle); + final ChargingSoundPreferenceController chargingSoundPreferenceController = + new ChargingSoundPreferenceController(context, fragment, lifecycle); + final DockingSoundPreferenceController dockingSoundPreferenceController = + new DockingSoundPreferenceController(context, fragment, lifecycle); + final TouchSoundPreferenceController touchSoundPreferenceController = + new TouchSoundPreferenceController(context, fragment, lifecycle); + final VibrateOnTouchPreferenceController vibrateOnTouchPreferenceController = + new VibrateOnTouchPreferenceController(context, fragment, lifecycle); + final DockAudioMediaPreferenceController dockAudioMediaPreferenceController = + new DockAudioMediaPreferenceController(context, fragment, lifecycle); + final BootSoundPreferenceController bootSoundPreferenceController = + new BootSoundPreferenceController(context); + final EmergencyTonePreferenceController emergencyTonePreferenceController = + new EmergencyTonePreferenceController(context, fragment, lifecycle); + + controllers.add(dialPadTonePreferenceController); + controllers.add(screenLockSoundPreferenceController); + controllers.add(chargingSoundPreferenceController); + controllers.add(dockingSoundPreferenceController); + controllers.add(touchSoundPreferenceController); + controllers.add(vibrateOnTouchPreferenceController); + controllers.add(dockAudioMediaPreferenceController); + controllers.add(bootSoundPreferenceController); + controllers.add(emergencyTonePreferenceController); + controllers.add(new PreferenceCategoryController(context, + "other_sounds_and_vibrations_category", + Arrays.asList(dialPadTonePreferenceController, + screenLockSoundPreferenceController, + chargingSoundPreferenceController, + dockingSoundPreferenceController, + touchSoundPreferenceController, + vibrateOnTouchPreferenceController, + dockAudioMediaPreferenceController, + bootSoundPreferenceController, + emergencyTonePreferenceController))); return controllers; } diff --git a/src/com/android/settings/notification/TouchSoundPreferenceController.java b/src/com/android/settings/notification/TouchSoundPreferenceController.java index 4e25507aef1..20c9cee0d05 100644 --- a/src/com/android/settings/notification/TouchSoundPreferenceController.java +++ b/src/com/android/settings/notification/TouchSoundPreferenceController.java @@ -23,6 +23,7 @@ import android.content.Context; import android.media.AudioManager; import android.os.AsyncTask; import android.provider.Settings.System; +import com.android.settings.R; import com.android.settings.SettingsPreferenceFragment; import com.android.settingslib.core.lifecycle.Lifecycle; @@ -53,4 +54,9 @@ public class TouchSoundPreferenceController extends SettingPrefController { } }; } + + @Override + public boolean isAvailable() { + return mContext.getResources().getBoolean(R.bool.config_show_touch_sounds); + } } diff --git a/tests/robotests/res/values-mcc999/config.xml b/tests/robotests/res/values-mcc999/config.xml index e10fee1e080..90a22372ed4 100644 --- a/tests/robotests/res/values-mcc999/config.xml +++ b/tests/robotests/res/values-mcc999/config.xml @@ -22,6 +22,13 @@ false false false + false + false + false + false + false + false + false false false false diff --git a/tests/robotests/src/com/android/settings/notification/AlarmVolumePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/AlarmVolumePreferenceControllerTest.java index 7b0b0333041..b2fbb004f7c 100644 --- a/tests/robotests/src/com/android/settings/notification/AlarmVolumePreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/AlarmVolumePreferenceControllerTest.java @@ -27,28 +27,37 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; @RunWith(SettingsRobolectricTestRunner.class) @Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) public class AlarmVolumePreferenceControllerTest { - @Mock - private Context mContext; @Mock private AudioHelper mHelper; + private Context mContext; private AlarmVolumePreferenceController mController; @Before public void setUp() { MockitoAnnotations.initMocks(this); + mContext = spy(RuntimeEnvironment.application); mController = new AlarmVolumePreferenceController(mContext, null, null, mHelper); } + @Test + @Config(qualifiers = "mcc999") + public void isAvailable_whenNotVisible_isFalse() { + assertThat(mController.isAvailable()).isFalse(); + } + @Test public void isAvailable_singleVolume_shouldReturnFalse() { when(mHelper.isSingleVolume()).thenReturn(true); diff --git a/tests/robotests/src/com/android/settings/notification/ChargingSoundPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/ChargingSoundPreferenceControllerTest.java index 2b2d024ee80..c24f02e2ed4 100644 --- a/tests/robotests/src/com/android/settings/notification/ChargingSoundPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/ChargingSoundPreferenceControllerTest.java @@ -31,11 +31,13 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowApplication; 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; @RunWith(SettingsRobolectricTestRunner.class) @@ -50,15 +52,15 @@ public class ChargingSoundPreferenceControllerTest { private ContentResolver mContentResolver; @Mock private SoundSettings mSetting; - @Mock - private Context mContext; + private Context mContext; private ChargingSoundPreferenceController mController; private SwitchPreference mPreference; @Before public void setUp() { MockitoAnnotations.initMocks(this); + mContext = spy(RuntimeEnvironment.application); when(mSetting.getActivity()).thenReturn(mActivity); when(mActivity.getContentResolver()).thenReturn(mContentResolver); mPreference = new SwitchPreference(ShadowApplication.getInstance().getApplicationContext()); @@ -68,10 +70,16 @@ public class ChargingSoundPreferenceControllerTest { } @Test - public void isAvailable_isAlwaysTrue() { + public void isAvailable_byDefault_isTrue() { assertThat(mController.isAvailable()).isTrue(); } + @Test + @Config(qualifiers = "mcc999") + public void isAvailable_whenNotVisible_isFalse() { + assertThat(mController.isAvailable()).isFalse(); + } + @Test public void displayPreference_chargingSoundEnabled_shouldCheckedPreference() { Global.putInt(mContentResolver, Global.CHARGING_SOUNDS_ENABLED, 1); diff --git a/tests/robotests/src/com/android/settings/notification/MediaVolumePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/MediaVolumePreferenceControllerTest.java index ca7fc444565..688575abdd3 100644 --- a/tests/robotests/src/com/android/settings/notification/MediaVolumePreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/MediaVolumePreferenceControllerTest.java @@ -27,30 +27,38 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.spy; + @RunWith(SettingsRobolectricTestRunner.class) @Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) public class MediaVolumePreferenceControllerTest { - @Mock private Context mContext; - private MediaVolumePreferenceController mController; @Before public void setUp() { MockitoAnnotations.initMocks(this); + mContext = spy(RuntimeEnvironment.application); mController = new MediaVolumePreferenceController(mContext, null, null); } @Test - public void isAlwaysAvailable() { + public void isAvailable_byDefault_isTrue() { assertThat(mController.isAvailable()).isTrue(); } + @Test + @Config(qualifiers = "mcc999") + public void isAvailable_whenNotVisible_isFalse() { + assertThat(mController.isAvailable()).isFalse(); + } + @Test public void getAudioStream_shouldReturnMusic() { assertThat(mController.getAudioStream()).isEqualTo(AudioManager.STREAM_MUSIC); diff --git a/tests/robotests/src/com/android/settings/notification/NotificationRingtonePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/NotificationRingtonePreferenceControllerTest.java index 940a948ea5c..f712ec8e49f 100644 --- a/tests/robotests/src/com/android/settings/notification/NotificationRingtonePreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/NotificationRingtonePreferenceControllerTest.java @@ -27,24 +27,38 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.spy; + @RunWith(SettingsRobolectricTestRunner.class) @Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) public class NotificationRingtonePreferenceControllerTest { - @Mock private Context mContext; private NotificationRingtonePreferenceController mController; @Before public void setUp() { MockitoAnnotations.initMocks(this); + mContext = spy(RuntimeEnvironment.application); mController = new NotificationRingtonePreferenceController(mContext); } + @Test + public void isAvailable_byDefault_isTrue() { + assertThat(mController.isAvailable()).isTrue(); + } + + @Test + @Config(qualifiers = "mcc999") + public void isAvailable_whenNotVisible_isFalse() { + assertThat(mController.isAvailable()).isFalse(); + } + @Test public void getRingtoneType_shouldReturnNotification() { assertThat(mController.getRingtoneType()).isEqualTo(RingtoneManager.TYPE_NOTIFICATION); diff --git a/tests/robotests/src/com/android/settings/notification/NotificationVolumePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/NotificationVolumePreferenceControllerTest.java index f919e7bc313..131fb188363 100644 --- a/tests/robotests/src/com/android/settings/notification/NotificationVolumePreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/NotificationVolumePreferenceControllerTest.java @@ -29,17 +29,18 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; @RunWith(SettingsRobolectricTestRunner.class) @Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) public class NotificationVolumePreferenceControllerTest { - @Mock - private Context mContext; @Mock private AudioHelper mHelper; @Mock @@ -49,17 +50,25 @@ public class NotificationVolumePreferenceControllerTest { @Mock private Vibrator mVibrator; + private Context mContext; private NotificationVolumePreferenceController mController; @Before public void setUp() { MockitoAnnotations.initMocks(this); + mContext = spy(RuntimeEnvironment.application); when(mContext.getSystemService(Context.TELEPHONY_SERVICE)).thenReturn(mTelephonyManager); when(mContext.getSystemService(Context.AUDIO_SERVICE)).thenReturn(mAudioManager); when(mContext.getSystemService(Context.VIBRATOR_SERVICE)).thenReturn(mVibrator); mController = new NotificationVolumePreferenceController(mContext, null, null, mHelper); } + @Test + @Config(qualifiers = "mcc999") + public void isAvailable_whenNotVisible_shouldReturnFalse() { + assertThat(mController.isAvailable()).isFalse(); + } + @Test public void isAvailable_singleVolume_shouldReturnFalse() { when(mHelper.isSingleVolume()).thenReturn(true); diff --git a/tests/robotests/src/com/android/settings/notification/ScreenLockSoundPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/ScreenLockSoundPreferenceControllerTest.java index f94f8bfe4b9..e6a871840dc 100644 --- a/tests/robotests/src/com/android/settings/notification/ScreenLockSoundPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/ScreenLockSoundPreferenceControllerTest.java @@ -31,11 +31,13 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowApplication; 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; @RunWith(SettingsRobolectricTestRunner.class) @@ -50,15 +52,15 @@ public class ScreenLockSoundPreferenceControllerTest { private ContentResolver mContentResolver; @Mock private SoundSettings mSetting; - @Mock - private Context mContext; + private Context mContext; private ScreenLockSoundPreferenceController mController; private SwitchPreference mPreference; @Before public void setUp() { MockitoAnnotations.initMocks(this); + mContext = spy(RuntimeEnvironment.application); when(mSetting.getActivity()).thenReturn(mActivity); when(mActivity.getContentResolver()).thenReturn(mContentResolver); mPreference = new SwitchPreference(ShadowApplication.getInstance().getApplicationContext()); @@ -68,10 +70,16 @@ public class ScreenLockSoundPreferenceControllerTest { } @Test - public void isAvailable_isAlwaysTrue() { + public void isAvailable_byDefault_isTrue() { assertThat(mController.isAvailable()).isTrue(); } + @Test + @Config(qualifiers = "mcc999") + public void isAvailable_whenNotVisible_isFalse() { + assertThat(mController.isAvailable()).isFalse(); + } + @Test public void displayPreference_lockScreenSoundEnabled_shouldCheckedPreference() { System.putInt(mContentResolver, System.LOCKSCREEN_SOUNDS_ENABLED, 1); diff --git a/tests/robotests/src/com/android/settings/notification/TouchSoundPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/TouchSoundPreferenceControllerTest.java index eaf9bb5e963..d9145fff6b1 100644 --- a/tests/robotests/src/com/android/settings/notification/TouchSoundPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/TouchSoundPreferenceControllerTest.java @@ -32,11 +32,13 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowApplication; 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.verify; import static org.mockito.Mockito.when; @@ -54,15 +56,15 @@ public class TouchSoundPreferenceControllerTest { private ContentResolver mContentResolver; @Mock private SoundSettings mSetting; - @Mock - private Context mContext; + private Context mContext; private TouchSoundPreferenceController mController; private SwitchPreference mPreference; @Before public void setUp() { MockitoAnnotations.initMocks(this); + mContext = spy(RuntimeEnvironment.application); when(mActivity.getSystemService(Context.AUDIO_SERVICE)).thenReturn(mAudioManager); when(mSetting.getActivity()).thenReturn(mActivity); when(mActivity.getContentResolver()).thenReturn(mContentResolver); @@ -73,10 +75,16 @@ public class TouchSoundPreferenceControllerTest { } @Test - public void isAvailable_isAlwaysTrue() { + public void isAvailable_byDefault_isTrue() { assertThat(mController.isAvailable()).isTrue(); } + @Test + @Config(qualifiers = "mcc999") + public void isAvailable_whenNotVisible_isFalse() { + assertThat(mController.isAvailable()).isFalse(); + } + @Test public void displayPreference_soundEffectEnabled_shouldCheckedPreference() { System.putInt(mContentResolver, System.SOUND_EFFECTS_ENABLED, 1); From 43e2e87889e09c8ffbc12abd9c8d5dd888cbebdf Mon Sep 17 00:00:00 2001 From: Ben Lin Date: Wed, 10 Jan 2018 14:06:28 -0800 Subject: [PATCH 02/22] Add string variants for "device" products. Bug: 62379606 Test: Strings are showing for device products. Change-Id: I2ecde371f0a7eaaaca118c3e03b194a6ecc7a5fd --- res/values/strings.xml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/res/values/strings.xml b/res/values/strings.xml index 8c7b6f4789f..b8206069ba5 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -3432,6 +3432,8 @@ About phone + About device + About emulated device View legal info, status, software version @@ -3706,6 +3708,15 @@ result from their use. + + + Your device and personal data are more vulnerable + to attack by unknown apps. By installing apps from + this source, you agree that you are responsible for + any damage to your device or loss of data that may + result from their use. + + Advanced settings @@ -5600,6 +5611,8 @@ Removing this account will delete all of its messages, contacts, and other data from the tablet! Removing this account will delete all of its messages, contacts, and other data from the phone! + + Removing this account will delete all of its messages, contacts, and other data from the device! This change isn\'t allowed by your admin From dc6bc4820e619e50b75b64929b1f6964aad90bbe Mon Sep 17 00:00:00 2001 From: Vishnu Nair Date: Wed, 10 Jan 2018 16:07:23 -0800 Subject: [PATCH 03/22] Add developer tiles for layer and window trace Bug: 64831661 Test: Toggle layer and window trace from new QS Tile Test: make RunSettingsRoboTests ROBOTEST_FILTER=LayerTraceTest && make RunSettingsRoboTests ROBOTEST_FILTER=WindowTraceTest Change-Id: I86b63361821e1bf5dd6a934e7fcb7e810740b74a --- AndroidManifest.xml | 20 +++ res/drawable/tile_icon_layer_trace.xml | 29 +++++ res/drawable/tile_icon_window_trace.xml | 29 +++++ res/values/strings.xml | 6 + .../development/qstile/DevelopmentTiles.java | 109 ++++++++++++++++ .../wrapper/IWindowManagerWrapper.java | 55 ++++++++ ...dwareOverlaysPreferenceControllerTest.java | 1 + .../settings/development/ShadowParcel.java | 20 --- ...urfaceUpdatesPreferenceControllerTest.java | 1 + .../development/qstile/LayerTraceTest.java | 123 ++++++++++++++++++ .../development/qstile/WindowTraceTest.java | 97 ++++++++++++++ .../testutils/shadow/ShadowParcel.java | 32 +++++ 12 files changed, 502 insertions(+), 20 deletions(-) create mode 100644 res/drawable/tile_icon_layer_trace.xml create mode 100644 res/drawable/tile_icon_window_trace.xml create mode 100644 src/com/android/settings/wrapper/IWindowManagerWrapper.java delete mode 100644 tests/robotests/src/com/android/settings/development/ShadowParcel.java create mode 100644 tests/robotests/src/com/android/settings/development/qstile/LayerTraceTest.java create mode 100644 tests/robotests/src/com/android/settings/development/qstile/WindowTraceTest.java create mode 100644 tests/robotests/src/com/android/settings/testutils/shadow/ShadowParcel.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 8c1ab5648b0..5fe84d5fd5e 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -3220,6 +3220,26 @@ + + + + + + + + + + + + + + + diff --git a/res/drawable/tile_icon_window_trace.xml b/res/drawable/tile_icon_window_trace.xml new file mode 100644 index 00000000000..25630494476 --- /dev/null +++ b/res/drawable/tile_icon_window_trace.xml @@ -0,0 +1,29 @@ + + + + + + diff --git a/res/values/strings.xml b/res/values/strings.xml index 8c7b6f4789f..242e36ef01e 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -8683,6 +8683,12 @@ Quick settings developer tiles + + Window Trace + + + Layer Trace + Work profile settings diff --git a/src/com/android/settings/development/qstile/DevelopmentTiles.java b/src/com/android/settings/development/qstile/DevelopmentTiles.java index bc3fcb5af77..fea658869c2 100644 --- a/src/com/android/settings/development/qstile/DevelopmentTiles.java +++ b/src/com/android/settings/development/qstile/DevelopmentTiles.java @@ -16,20 +16,27 @@ package com.android.settings.development.qstile; +import android.os.IBinder; +import android.os.Parcel; import android.os.RemoteException; +import android.os.ServiceManager; import android.os.SystemProperties; import android.provider.Settings; import android.service.quicksettings.Tile; import android.service.quicksettings.TileService; +import android.support.annotation.VisibleForTesting; +import android.util.Log; import android.view.IWindowManager; import android.view.ThreadedRenderer; import android.view.View; import android.view.WindowManagerGlobal; import com.android.internal.app.LocalePicker; +import com.android.settings.wrapper.IWindowManagerWrapper; import com.android.settingslib.development.SystemPropPoker; public abstract class DevelopmentTiles extends TileService { + private static final String TAG = "DevelopmentTiles"; protected abstract boolean isEnabled(); @@ -131,4 +138,106 @@ public abstract class DevelopmentTiles extends TileService { } catch (RemoteException e) { } } } + + /** + * Tile to toggle Window Trace. + */ + public static class WindowTrace extends DevelopmentTiles { + @VisibleForTesting + IWindowManagerWrapper mWindowManager; + + @Override + public void onCreate() { + super.onCreate(); + mWindowManager = new IWindowManagerWrapper(WindowManagerGlobal + .getWindowManagerService()); + } + + @Override + protected boolean isEnabled() { + try { + return mWindowManager.isWindowTraceEnabled(); + } catch (RemoteException e) { + Log.e(TAG, + "Could not get window trace status, defaulting to false." + e.toString()); + } + return false; + } + + @Override + protected void setIsEnabled(boolean isEnabled) { + try { + if (isEnabled) { + mWindowManager.startWindowTrace(); + } else { + mWindowManager.stopWindowTrace(); + } + } catch (RemoteException e) { + Log.e(TAG, "Could not set window trace status." + e.toString()); + } + } + } + + /** + * Tile to toggle Layer Trace. + */ + public static class LayerTrace extends DevelopmentTiles { + @VisibleForTesting + static final int SURFACE_FLINGER_LAYER_TRACE_CONTROL_CODE = 1025; + @VisibleForTesting + static final int SURFACE_FLINGER_LAYER_TRACE_STATUS_CODE = 1026; + @VisibleForTesting + IBinder mSurfaceFlinger; + + @Override + public void onCreate() { + super.onCreate(); + mSurfaceFlinger = ServiceManager.getService("SurfaceFlinger"); + } + + @Override + protected boolean isEnabled() { + boolean surfaceTraceEnabled = false; + Parcel reply = null; + Parcel data = null; + try { + if (mSurfaceFlinger != null) { + reply = Parcel.obtain(); + data = Parcel.obtain(); + data.writeInterfaceToken("android.ui.ISurfaceComposer"); + mSurfaceFlinger.transact(SURFACE_FLINGER_LAYER_TRACE_STATUS_CODE, + data, reply, 0 /* flags */ ); + surfaceTraceEnabled = reply.readBoolean(); + } + } catch (RemoteException e) { + Log.e(TAG, "Could not get layer trace status, defaulting to false." + e.toString()); + } finally { + if (data != null) { + data.recycle(); + reply.recycle(); + } + } + return surfaceTraceEnabled; + } + + @Override + protected void setIsEnabled(boolean isEnabled) { + Parcel data = null; + try { + if (mSurfaceFlinger != null) { + data = Parcel.obtain(); + data.writeInterfaceToken("android.ui.ISurfaceComposer"); + data.writeInt(isEnabled ? 1 : 0); + mSurfaceFlinger.transact(SURFACE_FLINGER_LAYER_TRACE_CONTROL_CODE, + data, null, 0 /* flags */); + } + } catch (RemoteException e) { + Log.e(TAG, "Could not set layer tracing." + e.toString()); + } finally { + if (data != null) { + data.recycle(); + } + } + } + } } \ No newline at end of file diff --git a/src/com/android/settings/wrapper/IWindowManagerWrapper.java b/src/com/android/settings/wrapper/IWindowManagerWrapper.java new file mode 100644 index 00000000000..8c2ed35935f --- /dev/null +++ b/src/com/android/settings/wrapper/IWindowManagerWrapper.java @@ -0,0 +1,55 @@ +/* + * 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.wrapper; + +import android.os.RemoteException; +import android.view.IWindowManager; + +/** + * This class replicates a subset of the android.view.IWindowManager. The class + * exists so that we can use a thin wrapper around the IWindowManager in production code + * and a mock in tests. + */ +public class IWindowManagerWrapper { + + private final IWindowManager mWindowManager; + + public IWindowManagerWrapper(IWindowManager wm) { + mWindowManager = wm; + } + + /** + * Returns true if window trace is enabled. + */ + public boolean isWindowTraceEnabled() throws RemoteException { + return mWindowManager.isWindowTraceEnabled(); + } + + /** + * Starts a window trace. + */ + public void startWindowTrace() throws RemoteException { + mWindowManager.startWindowTrace(); + } + + /** + * Stops a window trace. + */ + public void stopWindowTrace() throws RemoteException { + mWindowManager.stopWindowTrace(); + } +} diff --git a/tests/robotests/src/com/android/settings/development/HardwareOverlaysPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/development/HardwareOverlaysPreferenceControllerTest.java index 09e48d3971e..8522b995c53 100644 --- a/tests/robotests/src/com/android/settings/development/HardwareOverlaysPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/development/HardwareOverlaysPreferenceControllerTest.java @@ -37,6 +37,7 @@ import android.support.v7.preference.PreferenceScreen; import com.android.settings.TestConfig; import com.android.settings.testutils.SettingsRobolectricTestRunner; +import com.android.settings.testutils.shadow.ShadowParcel; import org.junit.Before; import org.junit.Test; diff --git a/tests/robotests/src/com/android/settings/development/ShadowParcel.java b/tests/robotests/src/com/android/settings/development/ShadowParcel.java deleted file mode 100644 index 965c959cb0f..00000000000 --- a/tests/robotests/src/com/android/settings/development/ShadowParcel.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.android.settings.development; - -import android.os.Parcel; - -import org.robolectric.annotation.Implementation; -import org.robolectric.annotation.Implements; - -/** - * This class provides helpers to test logic that reads from parcels. - */ -@Implements(Parcel.class) -public class ShadowParcel { - - static int sReadIntResult; - - @Implementation - public int readInt() { - return sReadIntResult; - } -} \ No newline at end of file diff --git a/tests/robotests/src/com/android/settings/development/ShowSurfaceUpdatesPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/development/ShowSurfaceUpdatesPreferenceControllerTest.java index a5cfa2274da..32768b652f9 100644 --- a/tests/robotests/src/com/android/settings/development/ShowSurfaceUpdatesPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/development/ShowSurfaceUpdatesPreferenceControllerTest.java @@ -37,6 +37,7 @@ import android.support.v7.preference.PreferenceScreen; import com.android.settings.TestConfig; import com.android.settings.testutils.SettingsRobolectricTestRunner; +import com.android.settings.testutils.shadow.ShadowParcel; import org.junit.Before; import org.junit.Test; diff --git a/tests/robotests/src/com/android/settings/development/qstile/LayerTraceTest.java b/tests/robotests/src/com/android/settings/development/qstile/LayerTraceTest.java new file mode 100644 index 00000000000..594b96c8e71 --- /dev/null +++ b/tests/robotests/src/com/android/settings/development/qstile/LayerTraceTest.java @@ -0,0 +1,123 @@ +/* + * 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.development.qstile; + +import static com.android.settings.development.qstile.DevelopmentTiles.LayerTrace + .SURFACE_FLINGER_LAYER_TRACE_CONTROL_CODE; +import static com.android.settings.development.qstile.DevelopmentTiles.LayerTrace + .SURFACE_FLINGER_LAYER_TRACE_STATUS_CODE; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; + +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import android.os.IBinder; +import android.os.RemoteException; + +import com.android.settings.TestConfig; +import com.android.settings.testutils.shadow.ShadowParcel; +import com.android.settings.testutils.SettingsRobolectricTestRunner; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.annotation.Config; +import org.robolectric.util.ReflectionHelpers; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class LayerTraceTest { + @Mock + private IBinder mSurfaceFlinger; + + private DevelopmentTiles.LayerTrace mLayerTraceTile; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mLayerTraceTile = spy(new DevelopmentTiles.LayerTrace()); + mLayerTraceTile.onCreate(); + ReflectionHelpers.setField(mLayerTraceTile, "mSurfaceFlinger", mSurfaceFlinger); + } + + @After + public void after() { + verifyNoMoreInteractions(mSurfaceFlinger); + } + + @Test + @Config(shadows = {ShadowParcel.class}) + public void sfReturnsTraceEnabled_shouldReturnEnabled() throws RemoteException { + ShadowParcel.sReadBoolResult = true; + assertThat(mLayerTraceTile.isEnabled()).isTrue(); + verify(mSurfaceFlinger) + .transact(eq(SURFACE_FLINGER_LAYER_TRACE_STATUS_CODE), any(), any(), + eq(0 /* flags */)); + } + + @Test + @Config(shadows = {ShadowParcel.class}) + public void sfReturnsTraceDisabled_shouldReturnDisabled() throws RemoteException { + ShadowParcel.sReadBoolResult = false; + assertThat(mLayerTraceTile.isEnabled()).isFalse(); + verify(mSurfaceFlinger) + .transact(eq(SURFACE_FLINGER_LAYER_TRACE_STATUS_CODE), any(), any(), + eq(0 /* flags */)); + } + + @Test + public void sfUnavailable_shouldReturnDisabled() throws RemoteException { + ReflectionHelpers.setField(mLayerTraceTile, "mSurfaceFlinger", null); + assertThat(mLayerTraceTile.isEnabled()).isFalse(); + } + + @Test + @Config(shadows = {ShadowParcel.class}) + public void setIsEnableTrue_shouldEnableLayerTrace() throws RemoteException { + mLayerTraceTile.setIsEnabled(true); + assertThat(ShadowParcel.sWriteIntResult).isEqualTo(1); + verify(mSurfaceFlinger) + .transact(eq(SURFACE_FLINGER_LAYER_TRACE_CONTROL_CODE), any(), isNull(), + eq(0 /* flags */)); + } + + @Test + @Config(shadows = {ShadowParcel.class}) + public void setIsEnableFalse_shouldDisableLayerTrace() throws RemoteException { + mLayerTraceTile.setIsEnabled(false); + assertThat(ShadowParcel.sWriteIntResult).isEqualTo(0); + verify(mSurfaceFlinger) + .transact(eq(SURFACE_FLINGER_LAYER_TRACE_CONTROL_CODE), any(), isNull(), + eq(0 /* flags */)); + } + + @Test + public void setIsEnableAndSfUnavailable_shouldDoNothing() throws RemoteException { + ReflectionHelpers.setField(mLayerTraceTile, "mSurfaceFlinger", null); + mLayerTraceTile.setIsEnabled(true); + mLayerTraceTile.setIsEnabled(false); + } +} diff --git a/tests/robotests/src/com/android/settings/development/qstile/WindowTraceTest.java b/tests/robotests/src/com/android/settings/development/qstile/WindowTraceTest.java new file mode 100644 index 00000000000..3c4d9baad3b --- /dev/null +++ b/tests/robotests/src/com/android/settings/development/qstile/WindowTraceTest.java @@ -0,0 +1,97 @@ +/* + * 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.development.qstile; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import android.os.RemoteException; + +import com.android.settings.TestConfig; +import com.android.settings.testutils.shadow.ShadowParcel; +import com.android.settings.testutils.SettingsRobolectricTestRunner; +import com.android.settings.wrapper.IWindowManagerWrapper; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.annotation.Config; +import org.robolectric.util.ReflectionHelpers; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class WindowTraceTest { + @Mock + private IWindowManagerWrapper mWindowManager; + + private DevelopmentTiles.WindowTrace mWindowTrace; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mWindowTrace = spy(new DevelopmentTiles.WindowTrace()); + mWindowTrace.onCreate(); + ReflectionHelpers.setField(mWindowTrace, "mWindowManager", mWindowManager); + } + + @Test + public void wmReturnsTraceEnabled_shouldReturnEnabled() throws RemoteException { + doReturn(true).when(mWindowManager).isWindowTraceEnabled(); + assertThat(mWindowTrace.isEnabled()).isTrue(); + } + + @Test + public void wmReturnsTraceDisabled_shouldReturnDisabled() throws RemoteException { + doReturn(false).when(mWindowManager).isWindowTraceEnabled(); + assertThat(mWindowTrace.isEnabled()).isFalse(); + } + + @Test + public void wmThrowsRemoteException_shouldReturnDisabled() throws RemoteException { + doThrow(new RemoteException("Unknown")) + .when(mWindowManager).isWindowTraceEnabled(); + assertThat(mWindowTrace.isEnabled()).isFalse(); + } + + @Test + public void setIsEnableTrue_shouldEnableWindowTrace() throws RemoteException { + mWindowTrace.setIsEnabled(true); + verify(mWindowManager).startWindowTrace(); + verifyNoMoreInteractions(mWindowManager); + } + + @Test + @Config(shadows = {ShadowParcel.class}) + public void setIsEnableFalse_shouldDisableWindowTrace() throws RemoteException { + mWindowTrace.setIsEnabled(false); + verify(mWindowManager).stopWindowTrace(); + verifyNoMoreInteractions(mWindowManager); + } + + @Test + public void setIsEnableAndWmThrowsRemoteException_shouldDoNothing() throws RemoteException { + doThrow(new RemoteException("Unknown")).when(mWindowManager).isWindowTraceEnabled(); + mWindowTrace.setIsEnabled(true); + } +} diff --git a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowParcel.java b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowParcel.java new file mode 100644 index 00000000000..6e42fea56d5 --- /dev/null +++ b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowParcel.java @@ -0,0 +1,32 @@ +package com.android.settings.testutils.shadow; + +import android.os.Parcel; + +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; + +/** + * This class provides helpers to test logic that reads from parcels. + */ +@Implements(Parcel.class) +public class ShadowParcel { + + public static int sReadIntResult; + public static int sWriteIntResult; + public static boolean sReadBoolResult; + + @Implementation + public int readInt() { + return sReadIntResult; + } + + @Implementation + public void writeInt(int val) { + sWriteIntResult = val; + } + + @Implementation + public boolean readBoolean() { + return sReadBoolResult; + } +} From f1346930bc18530940e464b4eb772d7717763a40 Mon Sep 17 00:00:00 2001 From: Ben Lin Date: Mon, 18 Dec 2017 15:46:44 -0800 Subject: [PATCH 04/22] Add ability to show/hide Color Correction and Color Inversion. This adds two new boolean flags: config_show_color_inversion_preference config_show_color_correction_preference Which whent set to false, will hide the color inversion and color correction preference items, respectively. Bug: 62378109 Test: make RunSettingsRoboTests ROBOTEST_FILTER=AccessibilitySettingsTest Change-Id: I06eac8e141bd6564495298c6c6544a7b059a4e73 --- res/values/bools.xml | 6 +++ .../accessibility/AccessibilitySettings.java | 24 ++++++++-- tests/robotests/res/values-mcc999/config.xml | 2 + .../AccessibilitySettingsTest.java | 45 +++++++++++++++++++ 4 files changed, 73 insertions(+), 4 deletions(-) diff --git a/res/values/bools.xml b/res/values/bools.xml index c490365fe4a..23dc88ac747 100644 --- a/res/values/bools.xml +++ b/res/values/bools.xml @@ -99,6 +99,12 @@ true + + true + + + true + true diff --git a/src/com/android/settings/accessibility/AccessibilitySettings.java b/src/com/android/settings/accessibility/AccessibilitySettings.java index cc72280c4e3..df8d4c8b4b3 100644 --- a/src/com/android/settings/accessibility/AccessibilitySettings.java +++ b/src/com/android/settings/accessibility/AccessibilitySettings.java @@ -90,8 +90,6 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements // Preferences private static final String TOGGLE_HIGH_TEXT_CONTRAST_PREFERENCE = "toggle_high_text_contrast_preference"; - private static final String TOGGLE_INVERSION_PREFERENCE = - "toggle_inversion_preference"; private static final String TOGGLE_POWER_BUTTON_ENDS_CALL_PREFERENCE = "toggle_power_button_ends_call_preference"; private static final String TOGGLE_LOCK_SCREEN_ROTATION_PREFERENCE = @@ -113,9 +111,11 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements "tts_settings_preference"; private static final String AUTOCLICK_PREFERENCE_SCREEN = "autoclick_preference_screen"; - private static final String DISPLAY_DALTONIZER_PREFERENCE_SCREEN = - "daltonizer_preference_screen"; + @VisibleForTesting static final String TOGGLE_INVERSION_PREFERENCE = + "toggle_inversion_preference"; + @VisibleForTesting static final String DISPLAY_DALTONIZER_PREFERENCE_SCREEN = + "daltonizer_preference_screen"; @VisibleForTesting static final String ACCESSIBILITY_SHORTCUT_PREFERENCE = "accessibility_shortcut_preference"; @@ -619,6 +619,8 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements displayCategory.addPreference(mToggleInversionPreference); displayCategory.addPreference(mDisplayDaltonizerPreferenceScreen); } + checkColorCorrectionVisibility(mDisplayDaltonizerPreferenceScreen); + checkColorInversionVisibility(mToggleInversionPreference); // Text contrast. mToggleHighTextContrastPreference.setChecked( @@ -769,6 +771,20 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements } } + @VisibleForTesting void checkColorCorrectionVisibility(Preference preference) { + if (!getContext().getResources().getBoolean( + R.bool.config_show_color_correction_preference)) { + removePreference(DISPLAY_DALTONIZER_PREFERENCE_SCREEN); + } + } + + @VisibleForTesting void checkColorInversionVisibility(Preference preference) { + if (!getContext().getResources().getBoolean( + R.bool.config_show_color_inversion_preference)) { + removePreference(TOGGLE_INVERSION_PREFERENCE); + } + } + @VisibleForTesting void checkAccessibilityShortcutVisibility(Preference preference) { if (!getContext().getResources().getBoolean( R.bool.config_show_accessibility_shortcut_preference)) { diff --git a/tests/robotests/res/values-mcc999/config.xml b/tests/robotests/res/values-mcc999/config.xml index 7bb80e888c3..6ac135dfa76 100644 --- a/tests/robotests/res/values-mcc999/config.xml +++ b/tests/robotests/res/values-mcc999/config.xml @@ -37,4 +37,6 @@ false false false + false + false diff --git a/tests/robotests/src/com/android/settings/accessibility/AccessibilitySettingsTest.java b/tests/robotests/src/com/android/settings/accessibility/AccessibilitySettingsTest.java index c721fc9e663..96ce18381a7 100644 --- a/tests/robotests/src/com/android/settings/accessibility/AccessibilitySettingsTest.java +++ b/tests/robotests/src/com/android/settings/accessibility/AccessibilitySettingsTest.java @@ -45,6 +45,8 @@ public class AccessibilitySettingsTest { private Context mContext; private AccessibilitySettings mFragment; private boolean mAccessibilityShortcutPreferenceRemoved; + private boolean mColorInversionPreferenceRemoved; + private boolean mColorCorrectionPreferenceRemoved; @Before public void setUp() { @@ -60,7 +62,16 @@ public class AccessibilitySettingsTest { protected boolean removePreference(String key) { if (AccessibilitySettings.ACCESSIBILITY_SHORTCUT_PREFERENCE.equals(key)) { mAccessibilityShortcutPreferenceRemoved = true; + return true; + } + if (AccessibilitySettings.TOGGLE_INVERSION_PREFERENCE.equals(key)) { + mColorInversionPreferenceRemoved = true; + return true; + } + + if (AccessibilitySettings.DISPLAY_DALTONIZER_PREFERENCE_SCREEN.equals(key)) { + mColorCorrectionPreferenceRemoved = true; return true; } return false; @@ -104,4 +115,38 @@ public class AccessibilitySettingsTest { assertThat(niks).contains(AccessibilitySettings.ACCESSIBILITY_SHORTCUT_PREFERENCE); } + + @Test + public void testColorInversionPreference_byDefault_shouldBeShown() { + final Preference preference = new Preference(mContext); + mFragment.checkColorInversionVisibility(preference); + + assertThat(mColorInversionPreferenceRemoved).isEqualTo(false); + } + + @Test + @Config(qualifiers = "mcc999") + public void testColorInversionPreference_ifDisabled_shouldNotBeShown() { + final Preference preference = new Preference(mContext); + mFragment.checkColorInversionVisibility(preference); + + assertThat(mColorInversionPreferenceRemoved).isEqualTo(true); + } + + @Test + public void testColorCorrectionPreference_byDefault_shouldBeShown() { + final Preference preference = new Preference(mContext); + mFragment.checkColorCorrectionVisibility(preference); + + assertThat(mColorCorrectionPreferenceRemoved).isEqualTo(false); + } + + @Test + @Config(qualifiers = "mcc999") + public void testColorCorrectionPreference_ifDisabled_shouldNotBeShown() { + final Preference preference = new Preference(mContext); + mFragment.checkColorCorrectionVisibility(preference); + + assertThat(mColorCorrectionPreferenceRemoved).isEqualTo(true); + } } From 0bed37a0863fa897df377f3bbfad4d626e7feb0e Mon Sep 17 00:00:00 2001 From: Joachim Sauer Date: Tue, 7 Nov 2017 13:52:12 +0000 Subject: [PATCH 05/22] Data loading component for new time zone picker. Add new data loading classes for improved manual time zone picker. These classes use existing sources mostly from ICU4J to construct the list of regions and timezones to present to the user. Test: SettingsRoboTests Bug: 62255208 Change-Id: I244c391a41b0b53cd3f7857f9c0d1ef766a39b17 --- .../datetime/timezone/DataLoader.java | 205 ++++++++++++++++++ .../datetime/timezone/RegionInfo.java | 60 +++++ .../datetime/timezone/TimeZoneInfo.java | 136 ++++++++++++ .../datetime/timezone/DataLoaderTest.java | 94 ++++++++ 4 files changed, 495 insertions(+) create mode 100644 src/com/android/settings/datetime/timezone/DataLoader.java create mode 100644 src/com/android/settings/datetime/timezone/RegionInfo.java create mode 100644 src/com/android/settings/datetime/timezone/TimeZoneInfo.java create mode 100644 tests/robotests/src/com/android/settings/datetime/timezone/DataLoaderTest.java diff --git a/src/com/android/settings/datetime/timezone/DataLoader.java b/src/com/android/settings/datetime/timezone/DataLoader.java new file mode 100644 index 00000000000..038558a426a --- /dev/null +++ b/src/com/android/settings/datetime/timezone/DataLoader.java @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2017 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.datetime.timezone; + +import android.graphics.Paint; +import android.icu.text.Collator; +import android.icu.text.LocaleDisplayNames; +import android.icu.text.TimeZoneFormat; +import android.icu.text.TimeZoneNames; +import android.icu.text.TimeZoneNames.NameType; +import android.icu.util.Region; +import android.icu.util.Region.RegionType; +import android.icu.util.TimeZone; +import android.icu.util.TimeZone.SystemTimeZoneType; +import com.android.settingslib.datetime.ZoneGetter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Provides data for manual selection of time zones based associated to regions. This class makes no + * attempt to avoid IO and processing intensive actions. This means it should not be called from the + * UI thread. + */ +public class DataLoader { + + private static final int MIN_HOURS_OFFSET = -14; + private static final int MAX_HOURS_OFFSET = +12; + + private final Locale mLocale; + + private final Collator mCollator; + private final LocaleDisplayNames mLocaleDisplayNames; + private final TimeZoneFormat mTimeZoneFormat; + private final Paint mPaint; + private final AtomicLong nextItemId = new AtomicLong(1); + private final long mNow = System.currentTimeMillis(); + + public DataLoader(Locale locale) { + mLocale = locale; + mCollator = Collator.getInstance(locale); + mLocaleDisplayNames = LocaleDisplayNames.getInstance(locale); + mTimeZoneFormat = TimeZoneFormat.getInstance(locale); + mPaint = new Paint(); + } + + /** + * Returns a {@link RegionInfo} object for each region that has selectable time zones. The + * returned list will be sorted properly for display in the locale. + */ + public List loadRegionInfos() { + final Set regions = Region.getAvailable(RegionType.TERRITORY); + final TreeSet regionInfos = new TreeSet<>(new RegionInfoComparator()); + for (final Region region : regions) { + final String regionId = region.toString(); + final Set timeZoneIds = getTimeZoneIds(regionId); + if (timeZoneIds.isEmpty()) { + continue; + } + + final String name = mLocaleDisplayNames.regionDisplayName(regionId); + final String regionalIndicator = createRegionalIndicator(regionId); + + regionInfos.add(new RegionInfo(regionId, name, regionalIndicator, timeZoneIds)); + } + + return Collections.unmodifiableList(new ArrayList<>(regionInfos)); + } + + /** + * Returns a list of {@link TimeZoneInfo} objects. The returned list will be sorted properly for + * display in the locale.It may be smaller than the input collection, if equivalent IDs are + * passed in. + * + * @param timeZoneIds a list of Olson IDs. + */ + public List loadTimeZoneInfos(Collection timeZoneIds) { + final TreeSet timeZoneInfos = new TreeSet<>(new TimeZoneInfoComparator()); + outer: + for (final String timeZoneId : timeZoneIds) { + final TimeZone timeZone = TimeZone.getFrozenTimeZone(timeZoneId); + for (final TimeZoneInfo other : timeZoneInfos) { + if (other.getTimeZone().hasSameRules(timeZone)) { + continue outer; + } + } + timeZoneInfos.add(createTimeZoneInfo(timeZone)); + } + return Collections.unmodifiableList(new ArrayList<>(timeZoneInfos)); + } + + /** + * Returns a {@link TimeZoneInfo} for each fixed offset time zone, such as UTC or GMT+4. The + * returned list will be sorted in a reasonable way for display. + */ + public List loadFixedOffsets() { + final List timeZoneInfos = new ArrayList<>(); + timeZoneInfos.add(createTimeZoneInfo(TimeZone.getFrozenTimeZone("Etc/UTC"))); + for (int hoursOffset = MAX_HOURS_OFFSET; hoursOffset >= MIN_HOURS_OFFSET; --hoursOffset) { + if (hoursOffset == 0) { + // UTC is handled above, so don't add GMT +/-0 again. + continue; + } + final String id = String.format("Etc/GMT%+d", hoursOffset); + timeZoneInfos.add(createTimeZoneInfo(TimeZone.getFrozenTimeZone(id))); + } + return Collections.unmodifiableList(timeZoneInfos); + } + + /** + * Gets the set of ids for relevant TimeZones in the given region. + */ + private Set getTimeZoneIds(String regionId) { + return TimeZone.getAvailableIDs( + SystemTimeZoneType.CANONICAL_LOCATION, regionId, /* rawOffset */ null); + } + + private TimeZoneInfo createTimeZoneInfo(TimeZone timeZone) { + // Every timezone we handle must be an OlsonTimeZone. + final String id = timeZone.getID(); + final TimeZoneNames timeZoneNames = mTimeZoneFormat.getTimeZoneNames(); + final java.util.TimeZone javaTimeZone = android.icu.impl.TimeZoneAdapter.wrap(timeZone); + final CharSequence gmtOffset = ZoneGetter.getGmtOffsetText(mTimeZoneFormat, mLocale, + javaTimeZone, new Date(mNow)); + return new TimeZoneInfo.Builder(timeZone) + .setGenericName(timeZoneNames.getDisplayName(id, NameType.LONG_GENERIC, mNow)) + .setStandardName(timeZoneNames.getDisplayName(id, NameType.LONG_STANDARD, mNow)) + .setDaylightName(timeZoneNames.getDisplayName(id, NameType.LONG_DAYLIGHT, mNow)) + .setExemplarLocation(timeZoneNames.getExemplarLocationName(id)) + .setGmtOffset(gmtOffset) + .setItemId(nextItemId.getAndIncrement()) + .build(); + } + + /** + * Create a Unicode Region Indicator Symbol for a given region id (a.k.a flag emoji). If the + * system can't render a flag for this region or the input is not a region id, this returns + * {@code null}. + * + * @param id the two-character region id. + * @return a String representing the flag of the region or {@code null}. + */ + private String createRegionalIndicator(String id) { + if (id.length() != 2) { + return null; + } + final char c1 = id.charAt(0); + final char c2 = id.charAt(1); + if ('A' > c1 || c1 > 'Z' || 'A' > c2 || c2 > 'Z') { + return null; + } + // Regional Indicator A is U+1F1E6 which is 0xD83C 0xDDE6 in UTF-16. + final String regionalIndicator = new String( + new char[]{0xd83c, (char) (0xdde6 - 'A' + c1), 0xd83c, (char) (0xdde6 - 'A' + c2)}); + if (!mPaint.hasGlyph(regionalIndicator)) { + return null; + } + return regionalIndicator; + } + + private class TimeZoneInfoComparator implements Comparator { + + @Override + public int compare(TimeZoneInfo tzi1, TimeZoneInfo tzi2) { + int result = + Integer + .compare(tzi1.getTimeZone().getRawOffset(), tzi2.getTimeZone().getRawOffset()); + if (result == 0) { + result = mCollator.compare(tzi1.getExemplarLocation(), tzi2.getExemplarLocation()); + } + if (result == 0 && tzi1.getGenericName() != null && tzi2.getGenericName() != null) { + result = mCollator.compare(tzi1.getGenericName(), tzi2.getGenericName()); + } + return result; + } + } + + private class RegionInfoComparator implements Comparator { + + @Override + public int compare(RegionInfo r1, RegionInfo r2) { + return mCollator.compare(r1.getName(), r2.getName()); + } + } +} diff --git a/src/com/android/settings/datetime/timezone/RegionInfo.java b/src/com/android/settings/datetime/timezone/RegionInfo.java new file mode 100644 index 00000000000..99fbaf09a05 --- /dev/null +++ b/src/com/android/settings/datetime/timezone/RegionInfo.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2017 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.datetime.timezone; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * Data object describing a geographical region. + * + * Regions are roughly equivalent to countries, but not every region is a country (for example "U.S. + * overseas territories" is treated as a country). + */ +public class RegionInfo { + + private final String mId; + private final String mName; + private final String mRegionalIndicator; + private final Collection mTimeZoneIds; + + public RegionInfo(String id, String name, String regionalIndicator, + Collection timeZoneIds) { + mId = id; + mName = name; + mRegionalIndicator = regionalIndicator; + mTimeZoneIds = Collections.unmodifiableList(new ArrayList<>(timeZoneIds)); + } + + public String getId() { + return mId; + } + + public String getName() { + return mName; + } + + public Collection getTimeZoneIds() { + return mTimeZoneIds; + } + + @Override + public String toString() { + return mRegionalIndicator != null ? mRegionalIndicator + " " + mName : mName; + } +} diff --git a/src/com/android/settings/datetime/timezone/TimeZoneInfo.java b/src/com/android/settings/datetime/timezone/TimeZoneInfo.java new file mode 100644 index 00000000000..96a20674de5 --- /dev/null +++ b/src/com/android/settings/datetime/timezone/TimeZoneInfo.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2017 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.datetime.timezone; + +import android.icu.util.TimeZone; +import android.text.TextUtils; + +/** + * Data object containing information for displaying a time zone for the user to select. + */ +public class TimeZoneInfo { + + private final String mId; + private final TimeZone mTimeZone; + private final String mGenericName; + private final String mStandardName; + private final String mDaylightName; + private final String mExemplarLocation; + private final CharSequence mGmtOffset; + // Arbitrary id that's unique within all TimeZoneInfo objects created by a given DataLoader instance. + private final long mItemId; + + public TimeZoneInfo(Builder builder) { + mTimeZone = builder.mTimeZone; + mId = mTimeZone.getID(); + mGenericName = builder.mGenericName; + mStandardName = builder.mStandardName; + mDaylightName = builder.mDaylightName; + mExemplarLocation = builder.mExemplarLocation; + mGmtOffset = builder.mGmtOffset; + mItemId = builder.mItemId; + } + + public String getId() { + return mId; + } + + public TimeZone getTimeZone() { + return mTimeZone; + } + + public String getExemplarLocation() { + return mExemplarLocation; + } + + public String getGenericName() { + return mGenericName; + } + + public String getStandardName() { + return mStandardName; + } + + public String getDaylightName() { + return mDaylightName; + } + + public CharSequence getGmtOffset() { + return mGmtOffset; + } + + public long getItemId() { + return mItemId; + } + + public static class Builder { + private final TimeZone mTimeZone; + private String mGenericName; + private String mStandardName; + private String mDaylightName; + private String mExemplarLocation; + private CharSequence mGmtOffset; + private long mItemId = -1; + + public Builder(TimeZone timeZone) { + if (timeZone == null) { + throw new IllegalArgumentException("TimeZone must not be null!"); + } + mTimeZone = timeZone; + } + + public Builder setGenericName(String genericName) { + this.mGenericName = genericName; + return this; + } + + public Builder setStandardName(String standardName) { + this.mStandardName = standardName; + return this; + } + + public Builder setDaylightName(String daylightName) { + mDaylightName = daylightName; + return this; + } + + public Builder setExemplarLocation(String exemplarLocation) { + mExemplarLocation = exemplarLocation; + return this; + } + + public Builder setGmtOffset(CharSequence gmtOffset) { + mGmtOffset = gmtOffset; + return this; + } + + public Builder setItemId(long itemId) { + mItemId = itemId; + return this; + } + + public TimeZoneInfo build() { + if (TextUtils.isEmpty(mGmtOffset)) { + throw new IllegalStateException("gmtOffset must not be empty!"); + } + if (mItemId == -1) { + throw new IllegalStateException("ItemId not set!"); + } + return new TimeZoneInfo(this); + } + + } +} diff --git a/tests/robotests/src/com/android/settings/datetime/timezone/DataLoaderTest.java b/tests/robotests/src/com/android/settings/datetime/timezone/DataLoaderTest.java new file mode 100644 index 00000000000..23bfabb89d0 --- /dev/null +++ b/tests/robotests/src/com/android/settings/datetime/timezone/DataLoaderTest.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2017 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.datetime.timezone; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.android.settings.TestConfig; +import com.android.settings.testutils.SettingsRobolectricTestRunner; + +import java.util.List; +import java.util.Locale; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class DataLoaderTest { + + @Test + public void testHasData() { + List regions = new DataLoader(Locale.US).loadRegionInfos(); + // Sanity check. Real size is closer to 200. + assertNotNull(regions); + assertTrue(regions.size() > 100); + assertEquals("Afghanistan", regions.get(0).getName()); + assertEquals("Zimbabwe", regions.get(regions.size() - 1).getName()); + } + + @Test + public void testRegionsWithTimeZone() { + List regions = new DataLoader(Locale.US).loadRegionInfos(); + checkRegionHasTimeZone(regions, "AT", "Europe/Vienna"); + checkRegionHasTimeZone(regions, "US", "America/Los_Angeles"); + checkRegionHasTimeZone(regions, "CN", "Asia/Shanghai"); + checkRegionHasTimeZone(regions, "AU", "Australia/Sydney"); + } + + @Test + public void testFixedOffsetTimeZones() { + List timeZones = new DataLoader(Locale.US).loadFixedOffsets(); + // Etc/GMT would be equivalent to Etc/UTC, except for how it is labelled. Users have + // explicitly asked for UTC to be supported, so make sure we label it as such. + checkHasTimeZone(timeZones, "Etc/UTC"); + checkHasTimeZone(timeZones, "Etc/GMT-1"); + checkHasTimeZone(timeZones, "Etc/GMT-14"); + checkHasTimeZone(timeZones, "Etc/GMT+1"); + checkHasTimeZone(timeZones, "Etc/GMT+12"); + } + + private void checkRegionHasTimeZone(List regions, String regionId, String tzId) { + RegionInfo ri = findRegion(regions, regionId); + assertTrue("Region " + regionId + " does not have time zone " + tzId, + ri.getTimeZoneIds().contains(tzId)); + } + + private void checkHasTimeZone(List timeZoneInfos, String tzId) { + for (TimeZoneInfo tz : timeZoneInfos) { + if (tz.getId().equals(tzId)) { + return; + } + } + fail("Fixed offset time zones do not contain " + tzId); + } + + private RegionInfo findRegion(List regions, String regionId) { + for (RegionInfo region : regions) { + if (region.getId().equals(regionId)) { + assertNotNull(region.getName()); + return region; + } + + } + fail("No region with id " + regionId + " found."); + return null; // can't reach. + } +} From 0cdbe1897cd56a613a04377b6c3e2f19add806de Mon Sep 17 00:00:00 2001 From: Joachim Sauer Date: Tue, 7 Nov 2017 13:56:16 +0000 Subject: [PATCH 06/22] New manual time zone picker. This implements a new manual time zone picker that allows selection of a time zone for a selected country. It also allows selecting a fixed offset time zone (most importantly Etc/UTC, which is a frequently requested feature). The new time zone picker is currently behind a feature flag (settings_zone_picker_v2), which is disabled by default. Test: manual Test: SettingsFunctionalTests Test: SettingsRobotTests Bug: 62255208 Change-Id: I89c5a04bcb562b6facf5f31a8aa4ad1cdd51ab10 --- res/layout/time_zone_list.xml | 44 ++++ res/layout/time_zone_list_item.xml | 62 +++++ res/values/strings.xml | 11 + .../android/settings/core/FeatureFlags.java | 1 + .../TimeZonePreferenceController.java | 8 + .../datetime/timezone/TimeZoneAdapter.java | 208 ++++++++++++++++ .../datetime/timezone/ViewHolder.java | 40 ++++ .../datetime/timezone/ZonePicker.java | 224 ++++++++++++++++++ .../timezone/TimeZoneAdapterTest.java | 105 ++++++++ 9 files changed, 703 insertions(+) create mode 100644 res/layout/time_zone_list.xml create mode 100644 res/layout/time_zone_list_item.xml create mode 100644 src/com/android/settings/datetime/timezone/TimeZoneAdapter.java create mode 100644 src/com/android/settings/datetime/timezone/ViewHolder.java create mode 100644 src/com/android/settings/datetime/timezone/ZonePicker.java create mode 100644 tests/robotests/src/com/android/settings/datetime/timezone/TimeZoneAdapterTest.java diff --git a/res/layout/time_zone_list.xml b/res/layout/time_zone_list.xml new file mode 100644 index 00000000000..a3c47cd904a --- /dev/null +++ b/res/layout/time_zone_list.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + diff --git a/res/layout/time_zone_list_item.xml b/res/layout/time_zone_list_item.xml new file mode 100644 index 00000000000..471c9d85a50 --- /dev/null +++ b/res/layout/time_zone_list_item.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index 8c7b6f4789f..3708a3e5f2c 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -752,6 +752,17 @@ Sort alphabetically Sort by time zone + + %1$s starts on %2$s. + + Daylight savings time + + Standard time + + Time zone by region + + Fixed offset time zones + Date diff --git a/src/com/android/settings/core/FeatureFlags.java b/src/com/android/settings/core/FeatureFlags.java index e88fb11179a..7d9b331f5bf 100644 --- a/src/com/android/settings/core/FeatureFlags.java +++ b/src/com/android/settings/core/FeatureFlags.java @@ -26,4 +26,5 @@ public class FeatureFlags { public static final String BATTERY_SETTINGS_V2 = "settings_battery_v2"; public static final String BATTERY_DISPLAY_APP_LIST = "settings_battery_display_app_list"; public static final String SECURITY_SETTINGS_V2 = "settings_security_settings_v2"; + public static final String ZONE_PICKER_V2 = "settings_zone_picker_v2"; } diff --git a/src/com/android/settings/datetime/TimeZonePreferenceController.java b/src/com/android/settings/datetime/TimeZonePreferenceController.java index 435b1fe77a3..e29e24550b3 100644 --- a/src/com/android/settings/datetime/TimeZonePreferenceController.java +++ b/src/com/android/settings/datetime/TimeZonePreferenceController.java @@ -20,7 +20,10 @@ import android.content.Context; import android.support.annotation.VisibleForTesting; import android.support.v7.preference.Preference; +import android.util.FeatureFlagUtils; +import com.android.settings.core.FeatureFlags; import com.android.settings.core.PreferenceControllerMixin; +import com.android.settings.datetime.timezone.ZonePicker; import com.android.settingslib.RestrictedPreference; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.datetime.ZoneGetter; @@ -33,11 +36,13 @@ public class TimeZonePreferenceController extends AbstractPreferenceController private static final String KEY_TIMEZONE = "timezone"; private final AutoTimeZonePreferenceController mAutoTimeZonePreferenceController; + private final boolean mZonePickerV2; public TimeZonePreferenceController(Context context, AutoTimeZonePreferenceController autoTimeZonePreferenceController) { super(context); mAutoTimeZonePreferenceController = autoTimeZonePreferenceController; + mZonePickerV2 = FeatureFlagUtils.isEnabled(mContext, FeatureFlags.ZONE_PICKER_V2); } @Override @@ -45,6 +50,9 @@ public class TimeZonePreferenceController extends AbstractPreferenceController if (!(preference instanceof RestrictedPreference)) { return; } + if (mZonePickerV2) { + preference.setFragment(ZonePicker.class.getName()); + } preference.setSummary(getTimeZoneOffsetAndName()); if( !((RestrictedPreference) preference).isDisabledByAdmin()) { preference.setEnabled(!mAutoTimeZonePreferenceController.isEnabled()); diff --git a/src/com/android/settings/datetime/timezone/TimeZoneAdapter.java b/src/com/android/settings/datetime/timezone/TimeZoneAdapter.java new file mode 100644 index 00000000000..79075ca78f5 --- /dev/null +++ b/src/com/android/settings/datetime/timezone/TimeZoneAdapter.java @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2017 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.datetime.timezone; + +import android.content.Context; +import android.graphics.Typeface; +import android.icu.impl.OlsonTimeZone; +import android.icu.text.DateFormat; +import android.icu.text.DisplayContext; +import android.icu.text.SimpleDateFormat; +import android.icu.util.Calendar; +import android.icu.util.TimeZone; +import android.icu.util.TimeZoneTransition; +import android.support.annotation.NonNull; +import android.support.v7.widget.RecyclerView; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import com.android.settings.R; + +import java.util.Collections; +import java.util.Date; +import java.util.List; + +/** + * Adapter for showing {@link TimeZoneInfo} objects in a recycler view. + */ +class TimeZoneAdapter extends RecyclerView.Adapter { + + static final int VIEW_TYPE_NORMAL = 1; + static final int VIEW_TYPE_SELECTED = 2; + + private final DateFormat mTimeFormat; + private final DateFormat mDateFormat; + private final View.OnClickListener mOnClickListener; + private final Context mContext; + private final String mCurrentTimeZone; + + private List mTimeZoneInfos; + + TimeZoneAdapter(View.OnClickListener onClickListener, Context context) { + mOnClickListener = onClickListener; + mContext = context; + mTimeFormat = DateFormat.getTimeInstance(SimpleDateFormat.SHORT); + mDateFormat = DateFormat.getDateInstance(SimpleDateFormat.MEDIUM); + mDateFormat.setContext(DisplayContext.CAPITALIZATION_NONE); + mCurrentTimeZone = TimeZone.getDefault().getID(); + setHasStableIds(true); + } + + @Override + public long getItemId(int position) { + return getItem(position).getItemId(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + final View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.time_zone_list_item, parent, false); + view.setOnClickListener(mOnClickListener); + final ViewHolder viewHolder = new ViewHolder(view); + if (viewType == VIEW_TYPE_SELECTED) { + viewHolder.mNameView.setTypeface( + viewHolder.mNameView.getTypeface(), Typeface.BOLD); + } + return viewHolder; + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { + final TimeZoneInfo item = getItem(position); + final ViewHolder tzHolder = (ViewHolder) holder; + tzHolder.mNameView.setText(formatName(item)); + tzHolder.mDetailsView.setText(formatDetails(item)); + tzHolder.mTimeView.setText(formatTime(item)); + String dstText = formatDstText(item); + tzHolder.mDstView.setText(dstText); + // Hide DST TextView when it has no content. + tzHolder.mDstView.setVisibility(dstText != null ? View.VISIBLE : View.GONE); + + } + + @Override + public int getItemCount() { + return getTimeZones().size(); + } + + @Override + public int getItemViewType(int position) { + final TimeZoneInfo tz = getItem(position); + if (tz.getId().equals(mCurrentTimeZone)) { + return VIEW_TYPE_SELECTED; + } else { + return VIEW_TYPE_NORMAL; + } + } + + public TimeZoneInfo getItem(int position) { + return getTimeZones().get(position); + } + + private CharSequence formatName(TimeZoneInfo item) { + CharSequence name = item.getExemplarLocation(); + if (name == null) { + name = item.getGenericName(); + } + if (name == null && item.getTimeZone().inDaylightTime(new Date())) { + name = item.getDaylightName(); + } + if (name == null) { + name = item.getStandardName(); + } + if (name == null) { + name = item.getGmtOffset(); + } + return name; + } + + private CharSequence formatDetails(TimeZoneInfo item) { + String name = item.getGenericName(); + if (name == null) { + if (item.getTimeZone().inDaylightTime(new Date())) { + name = item.getDaylightName(); + } else { + name = item.getStandardName(); + } + } + if (name == null) { + return item.getGmtOffset(); + } else { + return TextUtils.concat(item.getGmtOffset(), " ", name); + } + } + + private String formatDstText(TimeZoneInfo item) { + final TimeZone timeZone = item.getTimeZone(); + if (!timeZone.observesDaylightTime()) { + return null; + } + + final TimeZoneTransition nextDstTransition = findNextDstTransition(timeZone); + if (nextDstTransition == null) { + return null; + } + final boolean toDst = nextDstTransition.getTo().getDSTSavings() != 0; + String timeType = toDst ? item.getDaylightName() : item.getStandardName(); + if (timeType == null) { + // Fall back to generic "summer time" and "standard time" if the time zone has no + // specific names. + timeType = toDst ? + mContext.getString(R.string.zone_time_type_dst) : + mContext.getString(R.string.zone_time_type_standard); + + } + final Calendar transitionTime = Calendar.getInstance(timeZone); + transitionTime.setTimeInMillis(nextDstTransition.getTime()); + final String date = mDateFormat.format(transitionTime); + return mContext.getString(R.string.zone_change_to_from_dst, timeType, date); + } + + private TimeZoneTransition findNextDstTransition(TimeZone timeZone) { + if (!(timeZone instanceof OlsonTimeZone)) { + return null; + } + final OlsonTimeZone olsonTimeZone = (OlsonTimeZone) timeZone; + TimeZoneTransition transition = olsonTimeZone.getNextTransition( + System.currentTimeMillis(), /* inclusive */ false); + do { + if (transition.getTo().getDSTSavings() != transition.getFrom().getDSTSavings()) { + break; + } + transition = olsonTimeZone.getNextTransition( + transition.getTime(), /*inclusive */ false); + } while (transition != null); + return transition; + } + + private String formatTime(TimeZoneInfo item) { + return mTimeFormat.format(Calendar.getInstance(item.getTimeZone())); + } + + private List getTimeZones() { + if (mTimeZoneInfos == null) { + return Collections.emptyList(); + } + return mTimeZoneInfos; + } + + void setTimeZoneInfos(List timeZoneInfos) { + mTimeZoneInfos = timeZoneInfos; + notifyDataSetChanged(); + } +} diff --git a/src/com/android/settings/datetime/timezone/ViewHolder.java b/src/com/android/settings/datetime/timezone/ViewHolder.java new file mode 100644 index 00000000000..3cb2c4e29d8 --- /dev/null +++ b/src/com/android/settings/datetime/timezone/ViewHolder.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2017 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.datetime.timezone; + +import android.support.v7.widget.RecyclerView; +import android.view.View; +import android.widget.TextView; +import com.android.settings.R; + +/** + * View holder for a time zone list item. + */ +class ViewHolder extends RecyclerView.ViewHolder { + + final TextView mNameView; + final TextView mDstView; + final TextView mDetailsView; + final TextView mTimeView; + + public ViewHolder(View itemView) { + super(itemView); + mNameView = itemView.findViewById(R.id.tz_item_name); + mDstView = itemView.findViewById(R.id.tz_item_dst); + mDetailsView = itemView.findViewById(R.id.tz_item_details); + mTimeView = itemView.findViewById(R.id.tz_item_time); + } +} diff --git a/src/com/android/settings/datetime/timezone/ZonePicker.java b/src/com/android/settings/datetime/timezone/ZonePicker.java new file mode 100644 index 00000000000..eafbaa29bf2 --- /dev/null +++ b/src/com/android/settings/datetime/timezone/ZonePicker.java @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2017 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.datetime.timezone; + +import android.app.Activity; +import android.app.AlarmManager; +import android.content.Context; +import android.icu.util.TimeZone; +import android.os.Bundle; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Spinner; +import com.android.internal.logging.nano.MetricsProto; +import com.android.settings.R; +import com.android.settings.core.InstrumentedFragment; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** + * The class displaying a region list and a list of time zones for the selected region. + * Choosing an item from the list will set the time zone. Pressing Back without choosing from the + * list will not result in a change in the time zone setting. + */ +public class ZonePicker extends InstrumentedFragment + implements AdapterView.OnItemSelectedListener, View.OnClickListener { + + private static final int MENU_BY_REGION = Menu.FIRST; + private static final int MENU_BY_OFFSET = Menu.FIRST + 1; + + private Locale mLocale; + private List mRegions; + private Map> mZoneInfos; + private List mFixedOffsetTimeZones; + private TimeZoneAdapter mTimeZoneAdapter; + private String mSelectedTimeZone; + private boolean mSelectByRegion; + private DataLoader mDataLoader; + private RecyclerView mRecyclerView; + + @Override + public int getMetricsCategory() { + return MetricsProto.MetricsEvent.ZONE_PICKER; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + final View view = inflater.inflate(R.layout.time_zone_list, container, false); + + mLocale = getContext().getResources().getConfiguration().locale; + mDataLoader = new DataLoader(mLocale); + // TOOD: move this off the UI thread. + mRegions = mDataLoader.loadRegionInfos(); + mZoneInfos = new HashMap<>(); + mSelectByRegion = true; + mSelectedTimeZone = TimeZone.getDefault().getID(); + + mTimeZoneAdapter = new TimeZoneAdapter(this, getContext()); + mRecyclerView = view.findViewById(R.id.tz_list); + mRecyclerView.setAdapter(mTimeZoneAdapter); + mRecyclerView.setLayoutManager( + new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, /* reverseLayout */ false)); + + final ArrayAdapter regionAdapter = new ArrayAdapter<>(getContext(), + R.layout.filter_spinner_item, mRegions); + regionAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + final Spinner spinner = view.findViewById(R.id.tz_region_spinner); + spinner.setAdapter(regionAdapter); + spinner.setOnItemSelectedListener(this); + setupForCurrentTimeZone(spinner); + setHasOptionsMenu(true); + return view; + } + + private void setupForCurrentTimeZone(Spinner spinner) { + final String localeRegionId = mLocale.getCountry().toUpperCase(Locale.ROOT); + final String currentTimeZone = TimeZone.getDefault().getID(); + boolean fixedOffset = currentTimeZone.startsWith("Etc/GMT") || + currentTimeZone.equals("Etc/UTC"); + + for (int regionIndex = 0; regionIndex < mRegions.size(); regionIndex++) { + final RegionInfo region = mRegions.get(regionIndex); + if (localeRegionId.equals(region.getId())) { + spinner.setSelection(regionIndex); + } + if (!fixedOffset) { + for (String timeZoneId: region.getTimeZoneIds()) { + if (TextUtils.equals(timeZoneId, mSelectedTimeZone)) { + spinner.setSelection(regionIndex); + return; + } + } + } + } + + if (fixedOffset) { + setSelectByRegion(false); + } + } + + @Override + public void onClick(View view) { + // Ignore extra clicks + if (!isResumed()) { + return; + } + final int position = mRecyclerView.getChildAdapterPosition(view); + if (position == RecyclerView.NO_POSITION) { + return; + } + final TimeZoneInfo timeZoneInfo = mTimeZoneAdapter.getItem(position); + + // Update the system timezone value + final Activity activity = getActivity(); + final AlarmManager alarm = (AlarmManager) activity.getSystemService(Context.ALARM_SERVICE); + alarm.setTimeZone(timeZoneInfo.getId()); + + activity.onBackPressed(); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + menu.add(0, MENU_BY_REGION, 0, R.string.zone_menu_by_region); + menu.add(0, MENU_BY_OFFSET, 0, R.string.zone_menu_by_offset); + super.onCreateOptionsMenu(menu, inflater); + } + + @Override + public void onPrepareOptionsMenu(Menu menu) { + if (mSelectByRegion) { + menu.findItem(MENU_BY_REGION).setVisible(false); + menu.findItem(MENU_BY_OFFSET).setVisible(true); + } else { + menu.findItem(MENU_BY_REGION).setVisible(true); + menu.findItem(MENU_BY_OFFSET).setVisible(false); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + + case MENU_BY_REGION: + setSelectByRegion(true); + return true; + + case MENU_BY_OFFSET: + setSelectByRegion(false); + return true; + + default: + return false; + } + } + + private void setSelectByRegion(boolean selectByRegion) { + mSelectByRegion = selectByRegion; + getView().findViewById(R.id.tz_region_spinner_layout).setVisibility( + mSelectByRegion ? View.VISIBLE : View.GONE); + List tzInfos; + if (selectByRegion) { + Spinner regionSpinner = getView().findViewById(R.id.tz_region_spinner); + int selectedRegion = regionSpinner.getSelectedItemPosition(); + if (selectedRegion == -1) { + // Arbitrarily pick the first item if no region was selected above. + selectedRegion = 0; + regionSpinner.setSelection(selectedRegion); + } + tzInfos = getTimeZoneInfos(mRegions.get(selectedRegion)); + } else { + if (mFixedOffsetTimeZones == null) { + mFixedOffsetTimeZones = mDataLoader.loadFixedOffsets(); + } + tzInfos = mFixedOffsetTimeZones; + } + mTimeZoneAdapter.setTimeZoneInfos(tzInfos); + } + + private List getTimeZoneInfos(RegionInfo regionInfo) { + List tzInfos = mZoneInfos.get(regionInfo.getId()); + if (tzInfos == null) { + // TODO: move this off the UI thread. + Collection tzIds = regionInfo.getTimeZoneIds(); + tzInfos = mDataLoader.loadTimeZoneInfos(tzIds); + mZoneInfos.put(regionInfo.getId(), tzInfos); + } + return tzInfos; + } + + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + mTimeZoneAdapter.setTimeZoneInfos(getTimeZoneInfos(mRegions.get(position))); + } + + @Override + public void onNothingSelected(AdapterView parent) { + mTimeZoneAdapter.setTimeZoneInfos(null); + } + +} diff --git a/tests/robotests/src/com/android/settings/datetime/timezone/TimeZoneAdapterTest.java b/tests/robotests/src/com/android/settings/datetime/timezone/TimeZoneAdapterTest.java new file mode 100644 index 00000000000..5f29a0b65f1 --- /dev/null +++ b/tests/robotests/src/com/android/settings/datetime/timezone/TimeZoneAdapterTest.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2017 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.datetime.timezone; + +import android.icu.util.TimeZone; +import android.text.TextUtils; +import android.view.View; +import android.widget.FrameLayout; +import com.android.settings.TestConfig; +import com.android.settings.testutils.SettingsRobolectricTestRunner; +import com.android.settings.testutils.shadow.SettingsShadowResources; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +import java.util.Collections; + +import static com.google.common.truth.Truth.assertThat; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION, + shadows = { + SettingsShadowResources.class, + SettingsShadowResources.SettingsShadowTheme.class}) +public class TimeZoneAdapterTest { + @Mock + private View.OnClickListener mOnClickListener; + + private TimeZoneAdapter mTimeZoneAdapter; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mTimeZoneAdapter = new TimeZoneAdapter(mOnClickListener, RuntimeEnvironment.application); + } + + @Test + public void getItemViewType_onDefaultTimeZone_returnsTypeSelected() { + final TimeZoneInfo tzi = dummyTimeZoneInfo(TimeZone.getDefault()); + mTimeZoneAdapter.setTimeZoneInfos(Collections.singletonList(tzi)); + assertThat(mTimeZoneAdapter.getItemViewType(0)).isEqualTo(TimeZoneAdapter.VIEW_TYPE_SELECTED); + } + + @Test + public void getItemViewType_onNonDefaultTimeZone_returnsTypeNormal() { + final TimeZoneInfo tzi = dummyTimeZoneInfo(getNonDefaultTimeZone()); + mTimeZoneAdapter.setTimeZoneInfos(Collections.singletonList(tzi)); + assertThat(mTimeZoneAdapter.getItemViewType(0)).isEqualTo(TimeZoneAdapter.VIEW_TYPE_NORMAL); + } + + @Test + public void bindViewHolder_onDstTimeZone_showsDstLabel() { + final TimeZoneInfo tzi = dummyTimeZoneInfo(TimeZone.getTimeZone("America/Los_Angeles")); + mTimeZoneAdapter.setTimeZoneInfos(Collections.singletonList(tzi)); + + final FrameLayout parent = new FrameLayout(RuntimeEnvironment.application); + + final ViewHolder viewHolder = (ViewHolder) mTimeZoneAdapter.createViewHolder(parent, TimeZoneAdapter.VIEW_TYPE_NORMAL); + mTimeZoneAdapter.bindViewHolder(viewHolder, 0); + assertThat(viewHolder.mDstView.getVisibility()).isEqualTo(View.VISIBLE); + } + + @Test + public void bindViewHolder_onNonDstTimeZone_hidesDstLabel() { + final TimeZoneInfo tzi = dummyTimeZoneInfo(TimeZone.getTimeZone("Etc/UTC")); + mTimeZoneAdapter.setTimeZoneInfos(Collections.singletonList(tzi)); + + final FrameLayout parent = new FrameLayout(RuntimeEnvironment.application); + + final ViewHolder viewHolder = (ViewHolder) mTimeZoneAdapter.createViewHolder(parent, TimeZoneAdapter.VIEW_TYPE_NORMAL); + mTimeZoneAdapter.bindViewHolder(viewHolder, 0); + assertThat(viewHolder.mDstView.getVisibility()).isEqualTo(View.GONE); + } + + // Pick an arbitrary time zone that's not the current default. + private static TimeZone getNonDefaultTimeZone() { + final String[] availableIDs = TimeZone.getAvailableIDs(); + int index = 0; + if (TextUtils.equals(availableIDs[index], TimeZone.getDefault().getID())) { + index++; + } + return TimeZone.getTimeZone(availableIDs[index]); + } + + private TimeZoneInfo dummyTimeZoneInfo(TimeZone timeZone) { + return new TimeZoneInfo.Builder(timeZone).setGmtOffset("GMT+0").setItemId(1).build(); + } +} From 86a9be347c9662b9fde320993917e63b7de6846b Mon Sep 17 00:00:00 2001 From: Salvador Martinez Date: Thu, 11 Jan 2018 10:16:49 -0800 Subject: [PATCH 07/22] Add support for early warning query This CL adds the necessary code to enable the early warning query from the provider. Test: robotests in topic Bug: 71593245 Bug: 71812931 Change-Id: I32e10d1b9f752af672b5d1c6ccb8ba9f2f9cc387 --- .../settings/fuelgauge/PowerUsageFeatureProvider.java | 9 +++++++++ .../fuelgauge/PowerUsageFeatureProviderImpl.java | 6 +++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java b/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java index 67bdada24a0..79675cb3bd9 100644 --- a/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java +++ b/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java @@ -109,4 +109,13 @@ public interface PowerUsageFeatureProvider { * Checks whether to display the battery v2. */ boolean isBatteryV2Enabled(); + + /** + * Returns a signal to indicate if the device will need to warn the user they may not make it + * to their next charging time. + * + * @param id Optional string used to identify the caller for metrics. Usually the class name of + * the caller + */ + boolean getEarlyWarningSignal(Context context, String id); } diff --git a/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java b/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java index 3d9938651a8..93c4e032522 100644 --- a/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java +++ b/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java @@ -21,7 +21,6 @@ import static com.android.settings.core.FeatureFlags.BATTERY_SETTINGS_V2; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; -import android.net.Uri; import android.os.Process; import android.util.FeatureFlagUtils; import android.util.SparseIntArray; @@ -132,4 +131,9 @@ public class PowerUsageFeatureProviderImpl implements PowerUsageFeatureProvider public boolean isBatteryV2Enabled() { return FeatureFlagUtils.isEnabled(mContext, BATTERY_SETTINGS_V2); } + + @Override + public boolean getEarlyWarningSignal(Context context, String id) { + return false; + } } From eb66aac00fff898099e30af581bf05a3789f7542 Mon Sep 17 00:00:00 2001 From: Salvador Martinez Date: Thu, 11 Jan 2018 10:55:55 -0800 Subject: [PATCH 08/22] Re-add support string that was being used by wifi A wifi button was piggybacking on a support string. This CL adds a new string for the button to use instead. Test: robotests pass Bug: 71858657 Change-Id: Ic0da845fb70f33fc4365a4ee4586de685e3ef338 --- res/layout/wifi_network_details_two_buttons_panel.xml | 2 +- res/values/strings.xml | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/res/layout/wifi_network_details_two_buttons_panel.xml b/res/layout/wifi_network_details_two_buttons_panel.xml index f076eeaf032..57f76b5d62d 100644 --- a/res/layout/wifi_network_details_two_buttons_panel.xml +++ b/res/layout/wifi_network_details_two_buttons_panel.xml @@ -37,7 +37,7 @@