Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 2b6876cc authored by Victor Chang's avatar Victor Chang
Browse files

Time zone, Region, UTC picker

- Extract most common view related codes into BaseTimeZoneAdapter
  and BaseTimeZonePicker. Subclass handles the text formatting and
  order.
- Search view is added compared to previous version of time
  zone picker
- SpannableUtil is added to preserve spannable when formatting
  String resource.
- Fix the bug using GMT+<arabic> as time zone id. b/73132985
- Fix Talkback treating flags on screens as a separate element

Bug: 72146259
Bug: 73132985
Bug: 73952488
Test: mm RunSettingsRoboTests
Change-Id: I42c6ac369199c09d11e7f5cc4707358fa4780fed
(cherry picked from commit fbd30ace)
parent e8acc0c4
Loading
Loading
Loading
Loading
+99 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!--
     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.
-->
<!-- similar to preference_material.xml but textview for emoji country flag
instead of an ImageView -->

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:minHeight="?android:attr/listPreferredItemHeightSmall"
    android:gravity="center_vertical"
    android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
    android:paddingRight="?android:attr/listPreferredItemPaddingRight"
    android:background="?android:attr/selectableItemBackground"
    android:clipToPadding="false">

    <LinearLayout
        android:id="@+id/icon_frame"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="-4dp"
        android:minWidth="60dp"
        android:gravity="start|center_vertical"
        android:orientation="horizontal"
        android:paddingRight="12dp"
        android:paddingTop="4dp"
        android:paddingBottom="4dp">
        <!-- It's not ImageView because the icon is Unicode emoji. -->
        <TextView
            android:id="@+id/icon_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:singleLine="true"
            android:contentDescription=""
            android:textAppearance="?android:attr/textAppearanceLarge"
            android:textColor="?android:attr/textColorPrimary"
            android:importantForAccessibility="no"/>
    </LinearLayout>

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:orientation="vertical"
        android:paddingTop="16dp"
        android:paddingBottom="16dp">

        <TextView android:id="@android:id/title"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:singleLine="true"
            android:textAppearance="@style/Preference_TextAppearanceMaterialSubhead"
            android:ellipsize="marquee" />

        <RelativeLayout
            android:id="@+id/summary_frame"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_below="@android:id/title"
            android:layout_alignLeft="@android:id/title">

            <TextView
                android:id="@+id/current_time"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textAppearance="?android:attr/textAppearanceSmall"
                android:textColor="?android:attr/textColorSecondary"
                android:layout_alignParentEnd="true"/>

            <!-- Use layout_alignParentStart and layout_toStartOf to make the TextView multi-lines
                if needed -->
            <TextView
                android:id="@android:id/summary"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textAppearance="?android:attr/textAppearanceSmall"
                android:textColor="?android:attr/textColorSecondary"
                android:maxLines="10"
                android:layout_alignParentStart="true"
                android:layout_toStartOf="@+id/current_time"/>

        </RelativeLayout>

    </LinearLayout>
</LinearLayout>
 No newline at end of file
+26 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!--
     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.
-->

<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/time_zone_search_menu"
        android:title="@string/search_settings"
        android:icon="@*android:drawable/ic_search_api_material"
        android:showAsAction="always|collapseActionView"
        android:actionViewClass="android.widget.SearchView" />

</menu>
 No newline at end of file
+16 −2
Original line number Diff line number Diff line
@@ -748,20 +748,34 @@
    <string name="date_time_set_date_title">Date</string>
    <!-- Date & time setting screen setting option title -->
    <string name="date_time_set_date">Set date</string>
    <!-- Setting option title to select region in time zone setting screen [CHAR LIMIT=30] -->
    <string name="date_time_select_region">Region</string>
    <!-- Setting option title to select time zone in time zone setting screen [CHAR LIMIT=30]-->
    <string name="date_time_select_zone">Time Zone</string>
    <!-- Setting option title in time zone setting screen [CHAR LIMIT=30] -->
    <string name="date_time_select_fixed_offset_time_zones">Select UTC offset</string>
    <!-- Menu item on Select time zone screen -->
    <string name="zone_list_menu_sort_alphabetically">Sort alphabetically</string>
    <!-- Menu item on Select time zone screen -->
    <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>
    <!-- Label describing a exemplar location and time zone offset[CHAR LIMIT=NONE] -->
    <string name="zone_info_exemplar_location_and_offset"><xliff:g id="exemplar_location" example="Los Angeles">%1$s</xliff:g> (<xliff:g id="offset" example="GMT-08:00">%2$s</xliff:g>)</string>
    <!-- Label describing a time zone offset and name[CHAR LIMIT=NONE] -->
    <string name="zone_info_offset_and_name"><xliff:g id="time_type" example="Pacific Time">%2$s</xliff:g> (<xliff:g id="offset" example="GMT-08:00">%1$s</xliff:g>)</string>
    <!-- Label describing a time zone and changes to DST or standard time [CHAR LIMIT=NONE] -->
    <string name="zone_info_footer">Uses <xliff:g id="offset_and_name" example="Pacific Time (GMT-08:00)">%1$s</xliff:g>. <xliff:g id="dst_time_type" example="Pacific Daylight Time">%2$s</xliff:g> starts on <xliff:g id="transition_date" example="Mar 11 2018">%3$s</xliff:g>.</string>
    <!-- Label describing a time zone without DST [CHAR LIMIT=NONE] -->
    <string name="zone_info_footer_no_dst">Uses <xliff:g id="offset_and_name" example="GMT-08:00 Pacific Time">%1$s</xliff:g>. No daylight savings time.</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>
    <string name="zone_menu_by_region">Show time zones 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>
    <string name="zone_menu_by_offset">Show time zones by UTC offset</string>
    <!-- Title string shown above DatePicker, letting a user select system date
         [CHAR LIMIT=20] -->
+206 −0
Original line number Diff line number Diff line
/*
 * 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.datetime.timezone;

import android.icu.text.BreakIterator;
import android.support.annotation.NonNull;
import android.support.annotation.WorkerThread;
import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Filter;
import android.widget.TextView;

import com.android.settings.R;
import com.android.settings.datetime.timezone.BaseTimeZonePicker.OnListItemClickListener;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

/**
 * Used with {@class BaseTimeZonePicker}. It renders text in each item into list view. A list of
 * {@class AdapterItem} must be provided when an instance is created.
 */
public class BaseTimeZoneAdapter<T extends BaseTimeZoneAdapter.AdapterItem>
        extends RecyclerView.Adapter<BaseTimeZoneAdapter.ItemViewHolder> {

    private final List<T> mOriginalItems;
    private final OnListItemClickListener mOnListItemClickListener;
    private final Locale mLocale;
    private final boolean mShowItemSummary;

    private List<T> mItems;
    private ArrayFilter mFilter;

    public BaseTimeZoneAdapter(List<T> items, OnListItemClickListener
            onListItemClickListener, Locale locale, boolean showItemSummary) {
        mOriginalItems = items;
        mItems = items;
        mOnListItemClickListener = onListItemClickListener;
        mLocale = locale;
        mShowItemSummary = showItemSummary;
        setHasStableIds(true);
    }

    @NonNull
    @Override
    public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        final View view = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.time_zone_search_item, parent, false);
        return new ItemViewHolder(view, mOnListItemClickListener);
    }

    @Override
    public void onBindViewHolder(@NonNull ItemViewHolder holder, int position) {
        final AdapterItem item = mItems.get(position);
        holder.mSummaryFrame.setVisibility(
                mShowItemSummary ? View.VISIBLE : View.GONE);
        holder.mTitleView.setText(item.getTitle());
        holder.mIconTextView.setText(item.getIconText());
        holder.mSummaryView.setText(item.getSummary());
        holder.mTimeView.setText(item.getCurrentTime());
        holder.setPosition(position);
    }

    @Override
    public long getItemId(int position) {
        return getItem(position).getItemId();
    }

    @Override
    public int getItemCount() {
        return mItems.size();
    }

    public  @NonNull
    Filter getFilter() {
        if (mFilter == null) {
            mFilter = new ArrayFilter();
        }
        return mFilter;
    }

    public T getItem(int position) {
        return mItems.get(position);
    }

    public interface AdapterItem {
        CharSequence getTitle();
        CharSequence getSummary();
        String getIconText();
        String getCurrentTime();
        long getItemId();
        String[] getSearchKeys();
    }

    public static class ItemViewHolder extends RecyclerView.ViewHolder
            implements View.OnClickListener{

        final OnListItemClickListener mOnListItemClickListener;
        final View mSummaryFrame;
        final TextView mTitleView;
        final TextView mIconTextView;
        final TextView mSummaryView;
        final TextView mTimeView;
        private int mPosition;

        public ItemViewHolder(View itemView, OnListItemClickListener onListItemClickListener) {
            super(itemView);
            itemView.setOnClickListener(this);
            mSummaryFrame = itemView.findViewById(R.id.summary_frame);
            mTitleView = itemView.findViewById(android.R.id.title);
            mIconTextView = itemView.findViewById(R.id.icon_text);
            mSummaryView = itemView.findViewById(android.R.id.summary);
            mTimeView = itemView.findViewById(R.id.current_time);
            mOnListItemClickListener = onListItemClickListener;
        }

        public void setPosition(int position) {
            mPosition = position;
        }

        @Override
        public void onClick(View v) {
            mOnListItemClickListener.onListItemClick(mPosition);
        }
    }

    /**
     * <p>An array filter constrains the content of the array adapter with
     * a prefix. Each item that does not start with the supplied prefix
     * is removed from the list.</p>
     *
     * The filtering operation is not optimized, due to small data size (~260 regions),
     * require additional pre-processing. Potentially, a trie structure can be used to match
     * prefixes of the search keys.
     */
    private class ArrayFilter extends Filter {

        private BreakIterator mBreakIterator = BreakIterator.getWordInstance(mLocale);

        @WorkerThread
        @Override
        protected FilterResults performFiltering(CharSequence prefix) {
            final List<T> newItems;
            if (TextUtils.isEmpty(prefix)) {
                newItems = mOriginalItems;
            } else {
                final String prefixString = prefix.toString().toLowerCase(mLocale);
                newItems = new ArrayList<>();

                for (T item : mOriginalItems) {
                    outer:
                    for (String searchKey : item.getSearchKeys()) {
                        searchKey = searchKey.toLowerCase(mLocale);
                        // First match against the whole, non-splitted value
                        if (searchKey.startsWith(prefixString)) {
                            newItems.add(item);
                            break outer;
                        } else {
                            mBreakIterator.setText(searchKey);
                            for (int wordStart = 0, wordLimit = mBreakIterator.next();
                                    wordLimit != BreakIterator.DONE;
                                    wordStart = wordLimit,
                                            wordLimit = mBreakIterator.next()) {
                                if (mBreakIterator.getRuleStatus() != BreakIterator.WORD_NONE
                                        && searchKey.startsWith(prefixString, wordStart)) {
                                    newItems.add(item);
                                    break outer;
                                }
                            }
                        }
                    }
                }
            }

            final FilterResults results = new FilterResults();
            results.values = newItems;
            results.count = newItems.size();

            return results;
        }

        @Override
        protected void publishResults(CharSequence constraint, FilterResults results) {
            mItems = (List<T>) results.values;
            notifyDataSetChanged();
        }
    }
}
+166 −0
Original line number Diff line number Diff line
/*
 * 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.datetime.timezone;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.icu.text.DateFormat;
import android.icu.text.SimpleDateFormat;
import android.icu.util.Calendar;

import com.android.settings.R;
import com.android.settings.datetime.timezone.model.TimeZoneData;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;

/**
 * Render a list of {@class TimeZoneInfo} into the list view in {@class BaseTimeZonePicker}
 */
public abstract class BaseTimeZoneInfoPicker extends BaseTimeZonePicker {
    protected static final String TAG = "RegionZoneSearchPicker";
    protected ZoneAdapter mAdapter;

    protected BaseTimeZoneInfoPicker(int titleResId, int searchHintResId,
            boolean searchEnabled, boolean defaultExpandSearch) {
        super(titleResId, searchHintResId, searchEnabled, defaultExpandSearch);
    }

    @Override
    protected BaseTimeZoneAdapter createAdapter(TimeZoneData timeZoneData) {
        mAdapter = new ZoneAdapter(getContext(), getAllTimeZoneInfos(timeZoneData),
                this::onListItemClick, getLocale());
        return mAdapter;
    }

    private void onListItemClick(int position) {
        final TimeZoneInfo timeZoneInfo = mAdapter.getItem(position).mTimeZoneInfo;
        getActivity().setResult(Activity.RESULT_OK, prepareResultData(timeZoneInfo));
        getActivity().finish();
    }

    protected Intent prepareResultData(TimeZoneInfo selectedTimeZoneInfo) {
        return new Intent().putExtra(EXTRA_RESULT_TIME_ZONE_ID, selectedTimeZoneInfo.getId());
    }

    public abstract List<TimeZoneInfo> getAllTimeZoneInfos(TimeZoneData timeZoneData);

    protected static class ZoneAdapter extends BaseTimeZoneAdapter<TimeZoneInfoItem> {

        public ZoneAdapter(Context context, List<TimeZoneInfo> timeZones,
                OnListItemClickListener onListItemClickListener, Locale locale) {
            super(createTimeZoneInfoItems(context, timeZones, locale),
                    onListItemClickListener, locale,  true /* showItemSummary */);
        }

        private static List<TimeZoneInfoItem> createTimeZoneInfoItems(Context context,
                List<TimeZoneInfo> timeZones, Locale locale) {
            final DateFormat currentTimeFormat = new SimpleDateFormat(
                    android.text.format.DateFormat.getTimeFormatString(context), locale);
            final ArrayList<TimeZoneInfoItem> results = new ArrayList<>(timeZones.size());
            final Resources resources = context.getResources();
            long i = 0;
            for (TimeZoneInfo timeZone : timeZones) {
                results.add(new TimeZoneInfoItem(i++, timeZone, resources, currentTimeFormat));
            }
            return results;
        }
    }

    private static class TimeZoneInfoItem implements BaseTimeZoneAdapter.AdapterItem {
        private final long mItemId;
        private final TimeZoneInfo mTimeZoneInfo;
        private final Resources mResources;
        private final DateFormat mTimeFormat;
        private final String mTitle;
        private final String[] mSearchKeys;

        private TimeZoneInfoItem(long itemId, TimeZoneInfo timeZoneInfo, Resources resources,
                DateFormat timeFormat) {
            mItemId = itemId;
            mTimeZoneInfo = timeZoneInfo;
            mResources = resources;
            mTimeFormat = timeFormat;
            mTitle = createTitle(timeZoneInfo);
            mSearchKeys = new String[] { mTitle };
        }

        private static String createTitle(TimeZoneInfo timeZoneInfo) {
            String name = timeZoneInfo.getExemplarLocation();
            if (name == null) {
                name = timeZoneInfo.getGenericName();
            }
            if (name == null && timeZoneInfo.getTimeZone().inDaylightTime(new Date())) {
                name = timeZoneInfo.getDaylightName();
            }
            if (name == null) {
                name = timeZoneInfo.getStandardName();
            }
            if (name == null) {
                name = String.valueOf(timeZoneInfo.getGmtOffset());
            }
            return name;
        }

        @Override
        public CharSequence getTitle() {
            return mTitle;
        }

        @Override
        public CharSequence getSummary() {
            String name = mTimeZoneInfo.getGenericName();
            if (name == null) {
                if (mTimeZoneInfo.getTimeZone().inDaylightTime(new Date())) {
                    name = mTimeZoneInfo.getDaylightName();
                } else {
                    name = mTimeZoneInfo.getStandardName();
                }
            }
            if (name == null) {
                return mTimeZoneInfo.getGmtOffset();
            } else {
                return SpannableUtil.getResourcesText(mResources,
                        R.string.zone_info_offset_and_name, mTimeZoneInfo.getGmtOffset(), name);
            }
        }

        @Override
        public String getIconText() {
            return null;
        }

        @Override
        public String getCurrentTime() {
            return mTimeFormat.format(Calendar.getInstance(mTimeZoneInfo.getTimeZone()));
        }

        @Override
        public long getItemId() {
            return mItemId;
        }

        @Override
        public String[] getSearchKeys() {
            return mSearchKeys;
        }
    }
}
Loading