From 84b1ceda7c59787aaef986d819df56cfc812f78c Mon Sep 17 00:00:00 2001 From: Maurice Lam Date: Mon, 31 Oct 2016 15:38:02 -0700 Subject: [PATCH] Fix TTS for GMT offset Add TtsSpans to the GMT offset string so that TalkBack knows to read it out in a more natural way. Test: cd tests/robotests && mma Bug: 30042703 Change-Id: Ifa3c540f086472bc3a315b35ba40c9497f17d2d8 --- src/com/android/settings/ZonePicker.java | 27 ++++++- .../com/android/settings/ZonePickerTest.java | 70 +++++++++++++++++++ .../shadow/ShadowLibcoreTimeZoneNames.java | 60 ++++++++++++++++ .../testutils/shadow/ShadowTimeZoneNames.java | 35 ++++++++++ 4 files changed, 189 insertions(+), 3 deletions(-) create mode 100644 tests/robotests/src/com/android/settings/ZonePickerTest.java create mode 100644 tests/robotests/src/com/android/settings/testutils/shadow/ShadowLibcoreTimeZoneNames.java create mode 100644 tests/robotests/src/com/android/settings/testutils/shadow/ShadowTimeZoneNames.java diff --git a/src/com/android/settings/ZonePicker.java b/src/com/android/settings/ZonePicker.java index 5d8f15666aa..f6d6a6c142d 100644 --- a/src/com/android/settings/ZonePicker.java +++ b/src/com/android/settings/ZonePicker.java @@ -30,6 +30,7 @@ import android.view.View; import android.view.ViewGroup; import android.widget.ListView; import android.widget.SimpleAdapter; +import android.widget.TextView; import com.android.internal.logging.nano.MetricsProto; import com.android.settings.core.instrumentation.VisibilityLoggerMixin; @@ -86,10 +87,15 @@ public class ZonePicker extends ListFragment implements Instrumentable { */ public static SimpleAdapter constructTimezoneAdapter(Context context, boolean sortedByName, int layoutId) { - final String[] from = new String[] {ZoneGetter.KEY_DISPLAYNAME, ZoneGetter.KEY_GMT}; + final String[] from = new String[] { + ZoneGetter.KEY_DISPLAY_LABEL, + ZoneGetter.KEY_OFFSET_LABEL + }; final int[] to = new int[] {android.R.id.text1, android.R.id.text2}; - final String sortKey = (sortedByName ? ZoneGetter.KEY_DISPLAYNAME : ZoneGetter.KEY_OFFSET); + final String sortKey = (sortedByName + ? ZoneGetter.KEY_DISPLAY_LABEL + : ZoneGetter.KEY_OFFSET); final MyComparator comparator = new MyComparator(sortKey); final List> sortedList = ZoneGetter.getZonesList(context); Collections.sort(sortedList, comparator); @@ -98,10 +104,25 @@ public class ZonePicker extends ListFragment implements Instrumentable { layoutId, from, to); - + adapter.setViewBinder(new TimeZoneViewBinder()); return adapter; } + private static class TimeZoneViewBinder implements SimpleAdapter.ViewBinder { + + /** + * Set the text to the given {@link CharSequence} as is, instead of calling toString, so + * that additional information stored in the CharSequence is, like spans added to a + * {@link android.text.SpannableString} are preserved. + */ + @Override + public boolean setViewValue(View view, Object data, String textRepresentation) { + TextView textView = (TextView) view; + textView.setText((CharSequence) data); + return true; + } + } + /** * Searches {@link TimeZone} from the given {@link SimpleAdapter} object, and returns * the index for the TimeZone. diff --git a/tests/robotests/src/com/android/settings/ZonePickerTest.java b/tests/robotests/src/com/android/settings/ZonePickerTest.java new file mode 100644 index 00000000000..344eea3dff5 --- /dev/null +++ b/tests/robotests/src/com/android/settings/ZonePickerTest.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2016 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; + +import static com.google.common.truth.Truth.assertThat; + +import android.text.Spanned; +import android.text.style.TtsSpan; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.SimpleAdapter; +import android.widget.TextView; + +import com.android.settings.testutils.shadow.ShadowLibcoreTimeZoneNames; +import com.android.settings.testutils.shadow.ShadowTimeZoneNames; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config( + manifest = TestConfig.MANIFEST_PATH, + sdk = TestConfig.SDK_VERSION, + shadows = { + ShadowLibcoreTimeZoneNames.class, + ShadowLibcoreTimeZoneNames.ShadowZoneStringsCache.class, + ShadowTimeZoneNames.class + } +) +public class ZonePickerTest { + + @Test + public void testConstructTimeZoneAdapter() { + final SimpleAdapter adapter = + ZonePicker.constructTimezoneAdapter(RuntimeEnvironment.application, true); + assertThat(adapter).isNotNull(); + + ViewGroup parent = new FrameLayout(RuntimeEnvironment.application); + ViewGroup convertView = new FrameLayout(RuntimeEnvironment.application); + TextView text1 = new TextView(RuntimeEnvironment.application); + text1.setId(android.R.id.text1); + convertView.addView(text1); + TextView text2 = new TextView(RuntimeEnvironment.application); + text2.setId(android.R.id.text2); + convertView.addView(text2); + + adapter.getView(0, convertView, parent); + final CharSequence text = text2.getText(); + assertThat(text).isInstanceOf(Spanned.class); + final TtsSpan[] spans = ((Spanned) text).getSpans(0, text.length(), TtsSpan.class); + // GMT offset label should have TTS spans + assertThat(spans.length).isGreaterThan(0); + } +} diff --git a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowLibcoreTimeZoneNames.java b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowLibcoreTimeZoneNames.java new file mode 100644 index 00000000000..7292234a8bc --- /dev/null +++ b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowLibcoreTimeZoneNames.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2016 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.testutils.shadow; + +import libcore.icu.TimeZoneNames; + +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; +import org.robolectric.annotation.RealObject; +import org.robolectric.util.ReflectionHelpers; + +import java.util.Locale; +import java.util.TimeZone; + +/** + * System.logI used by ZoneStringsCache.create is a method new in API 24 and not available in + * Robolectric's 6.0 jar. Create a shadow which removes that log call. + */ +@Implements(value = TimeZoneNames.class, isInAndroidSdk = false) +public class ShadowLibcoreTimeZoneNames { + + private static final String[] availableTimeZoneIds = TimeZone.getAvailableIDs(); + + @Implements(value = TimeZoneNames.ZoneStringsCache.class, isInAndroidSdk = false) + public static class ShadowZoneStringsCache { + + @RealObject + private TimeZoneNames.ZoneStringsCache mRealObject; + + @Implementation + public String[][] create(Locale locale) { + // Set up the 2D array used to hold the names. The first column contains the Olson ids. + String[][] result = new String[availableTimeZoneIds.length][5]; + for (int i = 0; i < availableTimeZoneIds.length; ++i) { + result[i][0] = availableTimeZoneIds[i]; + } + + ReflectionHelpers.callInstanceMethod(TimeZoneNames.class, + mRealObject, "fillZoneStrings", + ReflectionHelpers.ClassParameter.from(String.class, locale.toLanguageTag()), + ReflectionHelpers.ClassParameter.from(String[][].class, result)); + + return result; + } + } +} diff --git a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowTimeZoneNames.java b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowTimeZoneNames.java new file mode 100644 index 00000000000..2bcc92a28ad --- /dev/null +++ b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowTimeZoneNames.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2016 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.testutils.shadow; + +import android.icu.text.TimeZoneNames; + +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; + +/** + * TimeZoneNames.getDisplayName tries to access files which doesn't exist for Robolectric. Stub it + * out for a naive implementation. + */ +@Implements(TimeZoneNames.class) +public class ShadowTimeZoneNames { + + @Implementation + public String getDisplayName(String tzID, TimeZoneNames.NameType type, long date) { + return "[DisplayName]" + tzID; + } +}