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
This commit is contained in:
44
res/layout/time_zone_list.xml
Normal file
44
res/layout/time_zone_list.xml
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<!--
|
||||||
|
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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/tz_region_spinner_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?android:attr/actionBarSize"
|
||||||
|
android:background="?android:attr/colorAccent"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:paddingEnd="@dimen/switchbar_subsettings_margin_end"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/tz_region_spinner"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:paddingStart="64dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="@drawable/app_filter_spinner_background"/>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<android.support.v7.widget.RecyclerView
|
||||||
|
android:id="@+id/tz_list"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"/>
|
||||||
|
</LinearLayout>
|
62
res/layout/time_zone_list_item.xml
Normal file
62
res/layout/time_zone_list_item.xml
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<!--
|
||||||
|
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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:minHeight="?android:attr/listPreferredItemHeight"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
|
||||||
|
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tz_item_name"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="TimeZone name"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceListItem" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tz_item_details"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceListItemSecondary"
|
||||||
|
android:textColor="?android:attr/textColorSecondary" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tz_item_time"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAlignment="viewEnd"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceListItemSecondary"
|
||||||
|
android:textColor="?android:attr/textColorSecondary" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tz_item_dst"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceListItemSecondary"
|
||||||
|
android:textColor="?android:attr/textColorSecondary" />
|
||||||
|
</LinearLayout>
|
@@ -752,6 +752,17 @@
|
|||||||
<string name="zone_list_menu_sort_alphabetically">Sort alphabetically</string>
|
<string name="zone_list_menu_sort_alphabetically">Sort alphabetically</string>
|
||||||
<!-- Menu item on Select time zone screen -->
|
<!-- Menu item on Select time zone screen -->
|
||||||
<string name="zone_list_menu_sort_by_timezone">Sort by time zone</string>
|
<string name="zone_list_menu_sort_by_timezone">Sort by time zone</string>
|
||||||
|
<!-- Label describing when a given time zone changes to DST or standard time -->
|
||||||
|
<string name="zone_change_to_from_dst"><xliff:g id="time_type" example="Pacific Summer Time">%1$s</xliff:g> starts on <xliff:g id="transition_date" example="Mar 11 2018">%2$s</xliff:g>.</string>
|
||||||
|
<!-- Describes the time type "daylight savings time" (used in zone_change_to_from_dst, when no zone specific name is available) -->
|
||||||
|
<string name="zone_time_type_dst">Daylight savings time</string>
|
||||||
|
<!-- Describes the time type "standard time" (used in zone_change_to_from_dst, when no zone specific name is available) -->
|
||||||
|
<string name="zone_time_type_standard">Standard time</string>
|
||||||
|
<!-- The menu item to switch to selecting a time zone by region (default) -->
|
||||||
|
<string name="zone_menu_by_region">Time zone by region</string>
|
||||||
|
<!-- The menu item to switch to selecting a time zone with a fixed offset (such as UTC or GMT+0200) -->
|
||||||
|
<string name="zone_menu_by_offset">Fixed offset time zones</string>
|
||||||
|
|
||||||
<!-- Title string shown above DatePicker, letting a user select system date
|
<!-- Title string shown above DatePicker, letting a user select system date
|
||||||
[CHAR LIMIT=20] -->
|
[CHAR LIMIT=20] -->
|
||||||
<string name="date_picker_title">Date</string>
|
<string name="date_picker_title">Date</string>
|
||||||
|
@@ -26,4 +26,5 @@ public class FeatureFlags {
|
|||||||
public static final String BATTERY_SETTINGS_V2 = "settings_battery_v2";
|
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 BATTERY_DISPLAY_APP_LIST = "settings_battery_display_app_list";
|
||||||
public static final String SECURITY_SETTINGS_V2 = "settings_security_settings_v2";
|
public static final String SECURITY_SETTINGS_V2 = "settings_security_settings_v2";
|
||||||
|
public static final String ZONE_PICKER_V2 = "settings_zone_picker_v2";
|
||||||
}
|
}
|
||||||
|
@@ -20,7 +20,10 @@ import android.content.Context;
|
|||||||
import android.support.annotation.VisibleForTesting;
|
import android.support.annotation.VisibleForTesting;
|
||||||
import android.support.v7.preference.Preference;
|
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.core.PreferenceControllerMixin;
|
||||||
|
import com.android.settings.datetime.timezone.ZonePicker;
|
||||||
import com.android.settingslib.RestrictedPreference;
|
import com.android.settingslib.RestrictedPreference;
|
||||||
import com.android.settingslib.core.AbstractPreferenceController;
|
import com.android.settingslib.core.AbstractPreferenceController;
|
||||||
import com.android.settingslib.datetime.ZoneGetter;
|
import com.android.settingslib.datetime.ZoneGetter;
|
||||||
@@ -33,11 +36,13 @@ public class TimeZonePreferenceController extends AbstractPreferenceController
|
|||||||
private static final String KEY_TIMEZONE = "timezone";
|
private static final String KEY_TIMEZONE = "timezone";
|
||||||
|
|
||||||
private final AutoTimeZonePreferenceController mAutoTimeZonePreferenceController;
|
private final AutoTimeZonePreferenceController mAutoTimeZonePreferenceController;
|
||||||
|
private final boolean mZonePickerV2;
|
||||||
|
|
||||||
public TimeZonePreferenceController(Context context,
|
public TimeZonePreferenceController(Context context,
|
||||||
AutoTimeZonePreferenceController autoTimeZonePreferenceController) {
|
AutoTimeZonePreferenceController autoTimeZonePreferenceController) {
|
||||||
super(context);
|
super(context);
|
||||||
mAutoTimeZonePreferenceController = autoTimeZonePreferenceController;
|
mAutoTimeZonePreferenceController = autoTimeZonePreferenceController;
|
||||||
|
mZonePickerV2 = FeatureFlagUtils.isEnabled(mContext, FeatureFlags.ZONE_PICKER_V2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -45,6 +50,9 @@ public class TimeZonePreferenceController extends AbstractPreferenceController
|
|||||||
if (!(preference instanceof RestrictedPreference)) {
|
if (!(preference instanceof RestrictedPreference)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (mZonePickerV2) {
|
||||||
|
preference.setFragment(ZonePicker.class.getName());
|
||||||
|
}
|
||||||
preference.setSummary(getTimeZoneOffsetAndName());
|
preference.setSummary(getTimeZoneOffsetAndName());
|
||||||
if( !((RestrictedPreference) preference).isDisabledByAdmin()) {
|
if( !((RestrictedPreference) preference).isDisabledByAdmin()) {
|
||||||
preference.setEnabled(!mAutoTimeZonePreferenceController.isEnabled());
|
preference.setEnabled(!mAutoTimeZonePreferenceController.isEnabled());
|
||||||
|
208
src/com/android/settings/datetime/timezone/TimeZoneAdapter.java
Normal file
208
src/com/android/settings/datetime/timezone/TimeZoneAdapter.java
Normal file
@@ -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<TimeZoneInfo> 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<TimeZoneInfo> getTimeZones() {
|
||||||
|
if (mTimeZoneInfos == null) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
return mTimeZoneInfos;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setTimeZoneInfos(List<TimeZoneInfo> timeZoneInfos) {
|
||||||
|
mTimeZoneInfos = timeZoneInfos;
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
}
|
40
src/com/android/settings/datetime/timezone/ViewHolder.java
Normal file
40
src/com/android/settings/datetime/timezone/ViewHolder.java
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
224
src/com/android/settings/datetime/timezone/ZonePicker.java
Normal file
224
src/com/android/settings/datetime/timezone/ZonePicker.java
Normal file
@@ -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<RegionInfo> mRegions;
|
||||||
|
private Map<String, List<TimeZoneInfo>> mZoneInfos;
|
||||||
|
private List<TimeZoneInfo> 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<RegionInfo> 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<TimeZoneInfo> 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<TimeZoneInfo> getTimeZoneInfos(RegionInfo regionInfo) {
|
||||||
|
List<TimeZoneInfo> tzInfos = mZoneInfos.get(regionInfo.getId());
|
||||||
|
if (tzInfos == null) {
|
||||||
|
// TODO: move this off the UI thread.
|
||||||
|
Collection<String> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user