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:
Joachim Sauer
2017-11-07 13:56:16 +00:00
parent 0bed37a086
commit 0cdbe1897c
9 changed files with 703 additions and 0 deletions

View File

@@ -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";
}

View File

@@ -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());

View 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();
}
}

View 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);
}
}

View 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);
}
}