/* * Copyright (C) 2012 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.embeddedlog.LightUpDroid.worldclock; import android.app.ActionBar; import android.app.Activity; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; import android.preference.PreferenceManager; import android.text.TextUtils; import android.text.format.DateFormat; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.view.inputmethod.EditorInfo; import android.widget.BaseAdapter; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.Filter; import android.widget.Filterable; import android.widget.ImageView; import android.widget.ListView; import android.widget.SearchView; import android.widget.SearchView.OnQueryTextListener; import android.widget.SectionIndexer; import android.widget.TextView; import com.embeddedlog.LightUpDroid.DeskClock; import com.embeddedlog.LightUpDroid.R; import com.embeddedlog.LightUpDroid.SettingsActivity; import com.embeddedlog.LightUpDroid.Utils; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.TimeZone; /** * Cities chooser for the world clock */ public class CitiesActivity extends Activity implements OnCheckedChangeListener, View.OnClickListener, OnQueryTextListener { private static final String KEY_SEARCH_QUERY = "search_query"; private static final String KEY_SEARCH_MODE = "search_mode"; private static final String KEY_LIST_POSITION = "list_position"; private static final String PREF_SORT = "sort_preference"; private static final int SORT_BY_NAME = 0; private static final int SORT_BY_GMT_OFFSET = 1; /** * This must be false for production. If true, turns on logging, test code, * etc. */ static final boolean DEBUG = false; static final String TAG = "CitiesActivity"; private LayoutInflater mFactory; private ListView mCitiesList; private CityAdapter mAdapter; private HashMap<String, CityObj> mUserSelectedCities; private Calendar mCalendar; private SearchView mSearchView; private StringBuffer mQueryTextBuffer = new StringBuffer(); private boolean mSearchMode; private int mPosition = -1; private SharedPreferences mPrefs; private int mSortType; private String mSelectedCitiesHeaderString; /** * Adapter for a list of cities with the respected time zone. The Adapter * sorts the list alphabetically and create an indexer. */ private class CityAdapter extends BaseAdapter implements Filterable, SectionIndexer { private static final int VIEW_TYPE_CITY = 0; private static final int VIEW_TYPE_HEADER = 1; private static final String DELETED_ENTRY = "C0"; private List<CityObj> mDisplayedCitiesList; private CityObj[] mCities; private CityObj[] mSelectedCities; // A map that caches names of cities in local memory. The names in this map are // preferred over the names of the selected cities stored in SharedPreferences, which could // be in a different language. This map gets reloaded on a locale change, when the new // language's city strings are read from the xml file. private HashMap<String, String> mCityNameMap = new HashMap<String, String>(); private String[] mSectionHeaders; private Integer[] mSectionPositions; private CityNameComparator mSortByNameComparator = new CityNameComparator(); private CityGmtOffsetComparator mSortByTimeComparator = new CityGmtOffsetComparator(); private final LayoutInflater mInflater; private boolean mIs24HoursMode; // AM/PM or 24 hours mode private final String mPattern12; private final String mPattern24; private int mSelectedEndPosition = 0; private Filter mFilter = new Filter() { @Override protected synchronized FilterResults performFiltering(CharSequence constraint) { FilterResults results = new FilterResults(); String modifiedQuery = constraint.toString().trim().toUpperCase(); ArrayList<CityObj> filteredList = new ArrayList<CityObj>(); ArrayList<String> sectionHeaders = new ArrayList<String>(); ArrayList<Integer> sectionPositions = new ArrayList<Integer>(); // If the search query is empty, add in the selected cities if (TextUtils.isEmpty(modifiedQuery) && mSelectedCities != null) { if (mSelectedCities.length > 0) { sectionHeaders.add("+"); sectionPositions.add(0); filteredList.add(new CityObj(mSelectedCitiesHeaderString, mSelectedCitiesHeaderString, null)); } for (CityObj city : mSelectedCities) { city.isHeader = false; filteredList.add(city); } } mSelectedEndPosition = filteredList.size(); long currentTime = System.currentTimeMillis(); String val = null; int offset = -100000; //some value that cannot be a real offset for (CityObj city : mCities) { // If the city is a deleted entry, ignore it. if (city.mCityId.equals(DELETED_ENTRY)) { continue; } // If the search query is empty, add section headers. if (TextUtils.isEmpty(modifiedQuery)) { // If the list is sorted by name, and the city begins with a letter // different than the previous city's letter, insert a section header. if (mSortType == SORT_BY_NAME && !city.mCityName.substring(0, 1).equals(val)) { val = city.mCityName.substring(0, 1).toUpperCase(); sectionHeaders.add(val); sectionPositions.add(filteredList.size()); filteredList.add(new CityObj(val, null, null)); city.isHeader = true; } else { city.isHeader = false; } // If the list is sorted by time, and the gmt offset is different than // the previous city's gmt offset, insert a section header. if (mSortType == SORT_BY_GMT_OFFSET) { TimeZone timezone = TimeZone.getTimeZone(city.mTimeZone); int newOffset = timezone.getOffset(currentTime); if (offset != newOffset) { offset = newOffset; String offsetString = Utils.getGMTHourOffset(timezone, true); sectionHeaders.add(offsetString); sectionPositions.add(filteredList.size()); filteredList.add(new CityObj(null, offsetString, null)); city.isHeader = true; } else { city.isHeader = false; } } } // If the city name begins with the query, add the city into the list. // If the query is empty, the city will automatically be added to the list. String cityName = city.mCityName.trim().toUpperCase(); if (city.mCityId != null && cityName.startsWith(modifiedQuery)) { city.isHeader = false; filteredList.add(city); } } mSectionHeaders = sectionHeaders.toArray(new String[sectionHeaders.size()]); mSectionPositions = sectionPositions.toArray(new Integer[sectionPositions.size()]); results.values = filteredList; results.count = filteredList.size(); return results; } @Override protected void publishResults(CharSequence constraint, FilterResults results) { mDisplayedCitiesList = (ArrayList<CityObj>) results.values; if (mPosition >= 0) { mCitiesList.setSelectionFromTop(mPosition, 0); mPosition = -1; } notifyDataSetChanged(); } }; public CityAdapter( Context context, LayoutInflater factory) { super(); mCalendar = Calendar.getInstance(); mCalendar.setTimeInMillis(System.currentTimeMillis()); mInflater = factory; // Load the cities from xml. mCities = Utils.loadCitiesFromXml(context); // Reload the city name map with the recently parsed city names of the currently // selected language for use with selected cities. mCityNameMap.clear(); for (CityObj city : mCities) { mCityNameMap.put(city.mCityId, city.mCityName); } // Re-organize the selected cities into an array. Collection<CityObj> selectedCities = mUserSelectedCities.values(); mSelectedCities = selectedCities.toArray(new CityObj[selectedCities.size()]); // Override the selected city names in the shared preferences with the // city names in the updated city name map, which will always reflect the // current language. for (CityObj city : mSelectedCities) { String newCityName = mCityNameMap.get(city.mCityId); if (newCityName != null) { city.mCityName = newCityName; } } mPattern24 = "kk:mm"; // There's an RTL layout bug that causes jank when fast-scrolling through // the list in 12-hour mode in an RTL locale. We can work around this by // ensuring the strings are the same length by using "hh" instead of "h". String pattern12 = "h:mm a"; mPattern12 = pattern12; sortCities(mSortType); set24HoursMode(context); } public void refreshSelectedCities() { Collection<CityObj> selectedCities = mUserSelectedCities.values(); mSelectedCities = selectedCities.toArray(new CityObj[selectedCities.size()]); sortCities(mSortType); } public void toggleSort() { if (mSortType == SORT_BY_NAME) { sortCities(SORT_BY_GMT_OFFSET); } else { sortCities(SORT_BY_NAME); } } private void sortCities(final int sortType) { mSortType = sortType; Arrays.sort(mCities, sortType == SORT_BY_NAME ? mSortByNameComparator : mSortByTimeComparator); if (mSelectedCities != null) { Arrays.sort(mSelectedCities, sortType == SORT_BY_NAME ? mSortByNameComparator : mSortByTimeComparator); } mPrefs.edit().putInt(PREF_SORT, sortType).commit(); mFilter.filter(mQueryTextBuffer.toString()); } @Override public int getCount() { return mDisplayedCitiesList != null ? mDisplayedCitiesList.size() : 0; } @Override public Object getItem(int p) { if (mDisplayedCitiesList != null && p >= 0 && p < mDisplayedCitiesList.size()) { return mDisplayedCitiesList.get(p); } return null; } @Override public long getItemId(int p) { return p; } @Override public boolean isEnabled(int p) { return mDisplayedCitiesList != null && mDisplayedCitiesList.get(p).mCityId != null; } @Override public synchronized View getView(int position, View view, ViewGroup parent) { if (mDisplayedCitiesList == null || position < 0 || position >= mDisplayedCitiesList.size()) { return null; } CityObj c = mDisplayedCitiesList.get(position); // Header view: A CityObj with nothing but the first letter as the name if (c.mCityId == null) { if (view == null) { view = mInflater.inflate(R.layout.city_list_header, parent, false); view.setTag(view.findViewById(R.id.header)); } ((TextView) view.getTag()).setText( mSortType == SORT_BY_NAME ? c.mCityName : c.mTimeZone); } else { // City view // Make sure to recycle a City view only if (view == null) { view = mInflater.inflate(R.layout.city_list_item, parent, false); final CityViewHolder holder = new CityViewHolder(); holder.name = (TextView) view.findViewById(R.id.city_name); holder.time = (TextView) view.findViewById(R.id.city_time); holder.selected = (CheckBox) view.findViewById(R.id.city_onoff); holder.selectedPin = (ImageView) view.findViewById(R.id.city_selected_icon); holder.remove = (ImageView) view.findViewById(R.id.city_remove); holder.remove.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { CompoundButton b = holder.selected; onCheckedChanged(b, false); b.setChecked(false); mAdapter.refreshSelectedCities(); } }); view.setTag(holder); } view.setOnClickListener(CitiesActivity.this); CityViewHolder holder = (CityViewHolder) view.getTag(); if (position < mSelectedEndPosition) { holder.selected.setVisibility(View.GONE); holder.time.setVisibility(View.GONE); holder.remove.setVisibility(View.VISIBLE); holder.selectedPin.setVisibility(View.VISIBLE); view.setEnabled(false); } else { holder.selected.setVisibility(View.VISIBLE); holder.time.setVisibility(View.VISIBLE); holder.remove.setVisibility(View.GONE); holder.selectedPin.setVisibility(View.GONE); view.setEnabled(true); } holder.selected.setTag(c); holder.selected.setChecked(mUserSelectedCities.containsKey(c.mCityId)); holder.selected.setOnCheckedChangeListener(CitiesActivity.this); holder.name.setText(c.mCityName, TextView.BufferType.SPANNABLE); holder.time.setText(getTimeCharSequence(c.mTimeZone)); } return view; } private CharSequence getTimeCharSequence(String timeZone) { mCalendar.setTimeZone(TimeZone.getTimeZone(timeZone)); return DateFormat.format(mIs24HoursMode ? mPattern24 : mPattern12, mCalendar); } @Override public int getViewTypeCount() { return 2; } @Override public int getItemViewType(int position) { return (mDisplayedCitiesList.get(position).mCityId != null) ? VIEW_TYPE_CITY : VIEW_TYPE_HEADER; } private class CityViewHolder { TextView name; TextView time; CheckBox selected; ImageView selectedPin; ImageView remove; } public void set24HoursMode(Context c) { mIs24HoursMode = DateFormat.is24HourFormat(c); notifyDataSetChanged(); } @Override public int getPositionForSection(int section) { return !isEmpty(mSectionPositions) ? mSectionPositions[section] : 0; } @Override public int getSectionForPosition(int p) { final Integer[] positions = mSectionPositions; if (!isEmpty(positions)) { for (int i = 0; i < positions.length - 1; i++) { if (p >= positions[i] && p < positions[i + 1]) { return i; } } if (p >= positions[positions.length - 1]) { return positions.length - 1; } } return 0; } @Override public Object[] getSections() { return mSectionHeaders; } @Override public Filter getFilter() { return mFilter; } private boolean isEmpty(Object[] array) { return array == null || array.length == 0; } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mFactory = LayoutInflater.from(this); mPrefs = PreferenceManager.getDefaultSharedPreferences(this); mSortType = mPrefs.getInt(PREF_SORT, SORT_BY_NAME); mSelectedCitiesHeaderString = getString(R.string.selected_cities_label); if (savedInstanceState != null) { mQueryTextBuffer.append(savedInstanceState.getString(KEY_SEARCH_QUERY)); mSearchMode = savedInstanceState.getBoolean(KEY_SEARCH_MODE); mPosition = savedInstanceState.getInt(KEY_LIST_POSITION); } updateLayout(); } @Override public void onSaveInstanceState(Bundle bundle) { super.onSaveInstanceState(bundle); bundle.putString(KEY_SEARCH_QUERY, mQueryTextBuffer.toString()); bundle.putBoolean(KEY_SEARCH_MODE, mSearchMode); bundle.putInt(KEY_LIST_POSITION, mCitiesList.getFirstVisiblePosition()); } private void updateLayout() { setContentView(R.layout.cities_activity); mCitiesList = (ListView) findViewById(R.id.cities_list); setFastScroll(TextUtils.isEmpty(mQueryTextBuffer.toString().trim())); mCitiesList.setScrollBarStyle(View.SCROLLBARS_INSIDE_INSET); mCitiesList.setFastScrollEnabled(true); mUserSelectedCities = Cities.readCitiesFromSharedPrefs( PreferenceManager.getDefaultSharedPreferences(this)); mAdapter = new CityAdapter(this, mFactory); mCitiesList.setAdapter(mAdapter); ActionBar actionBar = getActionBar(); if (actionBar != null) { actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP, ActionBar.DISPLAY_HOME_AS_UP); } } private void setFastScroll(boolean enabled) { if (mCitiesList != null) { mCitiesList.setFastScrollAlwaysVisible(enabled); mCitiesList.setFastScrollEnabled(enabled); } } @Override public void onResume() { super.onResume(); if (mAdapter != null) { mAdapter.set24HoursMode(this); } } @Override public void onPause() { super.onPause(); Cities.saveCitiesToSharedPrefs(PreferenceManager.getDefaultSharedPreferences(this), mUserSelectedCities); Intent i = new Intent(Cities.WORLDCLOCK_UPDATE_INTENT); sendBroadcast(i); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home: Intent intent = new Intent(this, DeskClock.class); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(intent); return true; case R.id.menu_item_settings: startActivity(new Intent(this, SettingsActivity.class)); return true; case R.id.menu_item_help: Intent i = item.getIntent(); if (i != null) { try { startActivity(i); } catch (ActivityNotFoundException e) { // No activity found to match the intent - ignore } } return true; case R.id.menu_item_sort: if (mAdapter != null) { mAdapter.toggleSort(); setFastScroll(TextUtils.isEmpty(mQueryTextBuffer.toString().trim())); } return true; default: break; } return super.onOptionsItemSelected(item); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.cities_menu, menu); MenuItem help = menu.findItem(R.id.menu_item_help); if (help != null) { Utils.prepareHelpMenuItem(this, help); } MenuItem searchMenu = menu.findItem(R.id.menu_item_search); mSearchView = (SearchView) searchMenu.getActionView(); mSearchView.setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI); mSearchView.setOnSearchClickListener(new OnClickListener() { @Override public void onClick(View arg0) { mSearchMode = true; } }); mSearchView.setOnCloseListener(new SearchView.OnCloseListener() { @Override public boolean onClose() { mSearchMode = false; return false; } }); if (mSearchView != null) { mSearchView.setOnQueryTextListener(this); mSearchView.setQuery(mQueryTextBuffer.toString(), false); if (mSearchMode) { mSearchView.requestFocus(); mSearchView.setIconified(false); } } return super.onCreateOptionsMenu(menu); } @Override public boolean onPrepareOptionsMenu(Menu menu) { MenuItem sortMenuItem = menu.findItem(R.id.menu_item_sort); if (mSortType == SORT_BY_NAME) { sortMenuItem.setTitle(getString(R.string.menu_item_sort_by_gmt_offset)); } else { sortMenuItem.setTitle(getString(R.string.menu_item_sort_by_name)); } return super.onPrepareOptionsMenu(menu); } @Override public void onCheckedChanged(CompoundButton b, boolean checked) { CityObj c = (CityObj) b.getTag(); if (checked) { mUserSelectedCities.put(c.mCityId, c); } else { mUserSelectedCities.remove(c.mCityId); } } @Override public void onClick(View v) { CompoundButton b = (CompoundButton) v.findViewById(R.id.city_onoff); boolean checked = b.isChecked(); onCheckedChanged(b, checked); b.setChecked(!checked); mAdapter.refreshSelectedCities(); } @Override public boolean onQueryTextChange(String queryText) { mQueryTextBuffer.setLength(0); mQueryTextBuffer.append(queryText); mCitiesList.setFastScrollEnabled(TextUtils.isEmpty(mQueryTextBuffer.toString().trim())); mAdapter.getFilter().filter(queryText); return true; } @Override public boolean onQueryTextSubmit(String arg0) { return false; } }